aiprompts/focus.md
This document explains how the focus system works in Wave Terminal, particularly for terminal blocks.
Wave Terminal uses a multi-layered focus system that coordinates between:
nodeModel.isFocused)When you click on a terminal block, this sequence occurs:
frontend/app/block/block.tsx:219-223
const blockModel: BlockComponentModel2 = {
onClick: setBlockClickedTrue,
onFocusCapture: handleChildFocus,
blockRef: blockRef,
};
frontend/app/block/block.tsx:165-167
When clicked, setBlockClickedTrue sets the blockClicked state to true.
frontend/app/block/block.tsx:151-163
useLayoutEffect(() => {
if (!blockClicked) {
return;
}
setBlockClicked(false);
const focusWithin = focusedBlockId() == nodeModel.blockId;
if (!focusWithin) {
setFocusTarget();
}
if (!isFocused) {
nodeModel.focusNode();
}
}, [blockClicked, isFocused]);
frontend/app/block/block.tsx:211-217
const setFocusTarget = useCallback(() => {
const ok = viewModel?.giveFocus?.();
if (ok) {
return;
}
focusElemRef.current?.focus({ preventScroll: true });
}, []);
The setFocusTarget function:
giveFocus() methodfrontend/app/view/term/term.tsx:414-427
giveFocus(): boolean {
if (this.searchAtoms && globalStore.get(this.searchAtoms.isOpen)) {
return true;
}
let termMode = globalStore.get(this.termMode);
if (termMode == "term") {
if (this.termRef?.current?.terminal) {
this.termRef.current.terminal.focus();
return true;
}
}
return false;
}
The terminal's giveFocus() calls XTerm's terminal.focus() to grant actual DOM focus.
A critical feature is that text selections are preserved when clicking within the same block.
frontend/app/block/block.tsx:156-158
const focusWithin = focusedBlockId() == nodeModel.blockId;
if (!focusWithin) {
setFocusTarget();
}
The key is focusedBlockId() which checks:
export function focusedBlockId(): string {
const focused = document.activeElement;
if (focused instanceof HTMLElement) {
const blockId = findBlockId(focused);
if (blockId) {
return blockId;
}
}
const sel = document.getSelection();
if (sel && sel.anchorNode && sel.rangeCount > 0 && !sel.isCollapsed) {
let anchor = sel.anchorNode;
if (anchor instanceof Text) {
anchor = anchor.parentElement;
}
if (anchor instanceof HTMLElement) {
const blockId = findBlockId(anchor);
if (blockId) {
return blockId;
}
}
}
return null;
}
When making a text selection within a block:
focusWithin returns true (selection exists in the block)setFocusTarget() is skippednodeModel.focusNode() is called to update layout stateThere's an important separation between visual focus (the focus ring) and actual DOM focus.
frontend/app/block/block.tsx:200-209
const handleChildFocus = useCallback(
(event: React.FocusEvent<HTMLDivElement, Element>) => {
if (!isFocused) {
nodeModel.focusNode(); // Updates layout state immediately
}
},
[isFocused]
);
This onFocusCapture handler fires on mousedown (capture phase), immediately updating the visual focus ring.
The actual DOM focus via giveFocus() only happens after click completion, through the onClick → useLayoutEffect path.
When making a selection in terminal 2 while terminal 1 is focused:
onFocusCapture fires → nodeModel.focusNode() updates focus ring
onClick fires → setBlockClickedTrue → triggers useLayoutEffectfocusWithin (now true because selection exists)setFocusTarget(), preserving the selectionResult: Focus ring updates immediately, but DOM focus is only granted after the selection is made, and is protected by the focusWithin check.
The terminal view has three useEffects that call giveFocus():
frontend/app/view/term/term.tsx:970-974
When the search panel closes, focus returns to the terminal.
frontend/app/view/term/term.tsx:1035-1038
When a terminal is recreated while focused (e.g., settings change), focus is restored.
frontend/app/view/term/term.tsx:1046-1052
When switching from vdom mode back to term mode, the terminal receives focus.
frontend/app/block/blocktypes.ts:7-12
export interface BlockNodeModel {
blockId: string;
isFocused: Atom<boolean>;
onClose: () => void;
focusNode: () => void;
}
View models can implement giveFocus(): boolean to handle focus in a view-specific way.
focusedBlockId(): Determines which block has focus or selectionhasSelection(): Checks if there's an active text selectionfindBlockId(): Traverses DOM to find containing blockThe focus system elegantly separates concerns:
giveFocus()This design allows for responsive UI (immediate focus ring updates) while preventing disruption of user interactions like text selection.