Skip to content

fix(canvas): keep a hovered terminal's keyboard after the pointer leaves (sloppy focus) - #1162

Merged
eneskirca merged 1 commit into
mainfrom
fix/sloppy-terminal-focus
Oct 8, 2026
Merged

eneskirca merged 1 commit into
mainfrom
fix/sloppy-terminal-focus

Conversation

@eneskirca

Copy link
Copy Markdown
Owner

Problem

With the default Terminal focus follows pointer (ON), hovering a terminal for the pan-hover delay gave it the keyboard — and moving the pointer off it took the keyboard straight back (onBodyLeave blurred the xterm and dropped the node's active flag + presence focus). Typing into a terminal while the mouse rested on the canvas, the sidebar, or on its way to another card silently went nowhere.

Change — sloppy focus

The hover dwell now only takes the keyboard. The terminal keeps it until:

  • another terminal takes it (its own dwell, or a click), or
  • the user clicks another node, the empty canvas, or a field.

How:

  • onBodyLeave no longer blurs/releases in either mode; in focus-follows-pointer mode it only re-arms the hover guard (pointerLeaveRearmsGuard), so a pass-over scroll still pans the canvas until the next dwell.
  • Release follows DOM focus via the existing useClickToFocus hook (feature request: option to disable X Window focus-follows-pointer (Mac-style click to select what UI element has the input focus) #757), which now runs in both modes (the enabled param is gone). Its setArmed is a no-op in focus-follows-pointer mode (focusDrivesGuard) — the guard stays a pointer contract there, so a ⌘K / sidebar release can't strand an armed guard under a resting pointer, and a header-drag reclaim can't drop it while the pointer is elsewhere.
  • A dwell onto a node whose ⌘M view covers its xterm can't take the keyboard itself, so it blurs the previous terminal's xterm (dwellBlursForeignTerminal) instead of leaving that pane receiving keystrokes while another node reads as active. Stickies/editors/fields keep their caret, as before.
  • Settings → Behavior copy, CLAUDE.md and the module docs updated.

Click to focus (setting OFF) is unchanged.

Surfaces

  • Desktop + Server Edition: same renderer code — both get it.
  • Kanban card modal: N/A (ModalTerminal has no hover guard).
  • Mobile: N/A (no hover).

Tests

  • Pure decisions (terminalFocusMode.test.ts): pointerLeaveRearmsGuard, focusDrivesGuard, dwellBlursForeignTerminal, plus wiring checks that onBodyLeave contains no blur / setActive(id, false) / releaseFocus.
  • Hook (useClickToFocus.test.tsx): a terminal the pointer has left keeps the keyboard and stays active; another terminal's xterm taking focus releases it.
  • Mutation-checked: re-adding the blur in onBodyLeave, or ungating setArmed, turns the suite red.
  • npm run typecheck / full renderer suites on this box: only pre-existing env failures from a stale shared node_modules (ssh2 missing, monaco 0.52 vs ^0.57) — identical on clean main.

Mac verification owed

  1. Hover terminal A until it focuses, move the mouse to the empty canvas, type → text lands in A; A stays active.
  2. With the mouse resting on A's header / the sidebar, type → still A.
  3. Hover terminal B until it focuses → keyboard moves to B; A's active state clears.
  4. Click the empty canvas → keyboard leaves A (Backspace no longer types into it).
  5. Mouse back over A and scroll immediately → canvas pans until the dwell elapses (guard re-armed).
  6. Open ⌘M on B, then hover B from A → typing no longer goes into A.
  7. Drag A by its header after typing in it → Backspace still goes to A, not "delete node".

🤖 Generated with Claude Code

…ves (sloppy focus)

With the default focus-follows-pointer, a hover dwell gave a terminal the keyboard and mouseleave
took it straight back: the xterm was blurred and the node dropped its active flag and presence
focus. Typing into a terminal while the mouse rested on the canvas (or on the sidebar, or on the way
to another card) silently went nowhere.

Now the dwell only TAKES the keyboard. The terminal keeps it until another node takes it (its own
dwell, a click) or the user clicks elsewhere — X11's "sloppy focus". Release follows DOM focus
through the same useClickToFocus hook click-to-focus already uses (#757), which now runs in both
modes. Only the hover guard still follows the pointer in this mode (leaving re-arms it, so a
pass-over scroll keeps panning the canvas), so the hook's setArmed is gated by focusDrivesGuard.

A dwell onto a node whose ⌘M view covers its xterm cannot take the keyboard itself, so it blurs
the previous terminal's xterm (dwellBlursForeignTerminal) instead of leaving it receiving keystrokes
while another node reads as active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eneskirca
eneskirca merged commit 8d26110 into main Oct 8, 2026
5 checks passed
@eneskirca eneskirca mentioned this pull request Oct 9, 2026
nexusatemporal pushed a commit to nexusatemporal/nodeterm that referenced this pull request Oct 10, 2026
Version bump 0.4.2 → 0.4.3 — 20 PRs / 80 commits since v0.4.2.

- **eneskirca#1159 — the one to ship for.** Hold-to-talk could leave a live microphone that typed
  into the selected terminal one line every 0–2 minutes for ~10 minutes, sometimes the
  user's meeting speech. Reported from 0.3.16 and present across many versions. Root
  cause: `startRecording` stored the capture in `captureRef` only AFTER
  `await capture.start()` resolved, so an overlay that closed inside that window (a
  <400 ms Cmd+Alt tap, any Cmd+Opt+<key>, a window blur) left the unmount cleanup with
  nothing to cancel.
- **eneskirca#1118** run node: `.vscode/launch.json` "Run Without Debugging" on the canvas, with
  Flutter hot reload / hot restart / reload-on-save.
- **eneskirca#1154** resize from every edge under Liquid Glass + window-management fixes;
  **eneskirca#1162** sloppy focus keeps a hovered terminal's keyboard after the pointer leaves.
- **eneskirca#1137** Live links: Figma-style create dialog; **eneskirca#1160** Billing & invoices → Stripe
  customer portal.
- **eneskirca#1157** agent confirms offer "don't ask again for agents in <project>"; **eneskirca#1107** ask
  before writing agent config, skills replace AGENTS.md blocks; **eneskirca#1125** chat-view
  prompts typed, not pasted; **eneskirca#1111** bounded concurrency + progress for bulk restarts;
  **eneskirca#836** arrange by lineage and inside a group; **eneskirca#1142** strikethrough in the shared
  GPU renderer; **eneskirca#1155** readable Agents/Accounts settings.
- **eneskirca#701** zustand 4 → 5 major; **eneskirca#1121/eneskirca#1122/eneskirca#1126** share/palette/relay fixes.

docs/release-notes/v0.4.3.md is the Highlights block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant