Skip to content

Stop fleet activity from taking the caret out of a split pane - #1177

Merged
edwin-zvs merged 1 commit into
mainfrom
webui-split-focus
Aug 2, 2026
Merged

Stop fleet activity from taking the caret out of a split pane#1177
edwin-zvs merged 1 commit into
mainfrom
webui-split-focus

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

The bug

In the desktop split view, typing into a pane loses focus at random — mid-prompt, repeatedly — and you have to click the pane again to keep going. Reported as "quite unusable."

Root cause

renderSessions runs on every session/state delta — a background worker's status tick, a token count, a spinner frame — and schedules a pane-grid rebuild:

session/state (any session) -> scheduleRenderSessions -> renderSessions
                            -> schedulePaneGridRefresh -> renderPaneGrid

renderPaneGrid rebuilds the whole pane DOM. It moves #viewStack — which holds the focused pane's composer and its xterm helper textarea — into a freshly built pane element, then replaceChildrens the grid. Detaching the node that holds document.activeElement blurs it, and re-attaching does not bring focus back.

So a session you cannot even see kills the caret of the session you are typing into. Single-session mode early-returns out of schedulePaneGridRefresh and never rebuilds, which is why this only ever bit the split view.

The fix

Two changes, both needed:

  1. renderPaneGrid preserves the typing caret — snapshot before, restore synchronously after, reusing the existing captureTypingFocus / restoreTypingFocus helpers (already written for this same bug class around the suggestion orb). Synchronous, so no keystroke lands in the gap, and later deliberate focus moves — clicking a pane to select another session — still get the last word.

  2. schedulePaneGridRefresh skips rebuilds nothing needs — compares a signature of everything the grid actually draws (tree shape, ratios, focus, zoom, per-pane title and view mode) and returns early when none of it moved. Restoring focus alone is not enough: a blur/refocus kills an in-progress IME composition, so composed scripts would still lose syllables to a status tick that changes nothing on screen.

Deliberate user actions (split, close, resize, focus move, async transcript/xterm arrival) call renderPaneGrid directly and always repaint.

Verification

Throwaway daemon, two sessions in a 2-pane split, driving real session/state notifications through handleNotification, comparing main's assets against this branch's via dev.set_assets:

rebuilds focus losses
before — 10 deltas 10 10 (first at 107 ms)
after — 10 title changes 10 0
after — 10 status ticks 0 0

Terminal surface checked separately with a real xterm mounted in the view stack: captureTypingFocus takes its terminal branch, 8 rebuild-forcing deltas, 0 losses, focus still on xterm-helper-textarea.

The underlying DOM behavior was confirmed independently in Chrome — a bare re-parent leaves activeElement=BODY; wrapped in capture/restore it stays on the field.

Regression checks: pane titles still track renames, clicking an unfocused pane still moves focus there and selects its session, split -> close still round-trips (2 -> 3 -> 2 panes).

Spec

Adds specs/0184-a-repaint-never-takes-the-caret.md — a repaint caused by other people's activity must not move this user's caret, and must not happen at all when nothing it draws has changed.

Binaries

Webui-only change (crates/daemon/assets/index.html), embedded in the daemon, so the relevant binary is:

/Users/moon/construct/.claude/worktrees/webui-split-focus/target/debug/construct

Typing into a pane while any other session was busy dropped the caret
mid-word: the user had to click the pane again, over and over, which made
the split view effectively unusable for composing a prompt.

`renderSessions` runs on every `session/state` delta — a background
worker's status tick, a token count, a spinner frame — and schedules a
pane-grid rebuild. The rebuild moves `#viewStack`, and with it whichever
composer or xterm helper textarea had focus, into a freshly built pane
element. Detaching the node that holds `document.activeElement` blurs it,
and re-attaching does not bring focus back. So a session the user could
not even see killed the caret of a session they were typing into. Single-
session mode never rebuilds the grid, which is why this only ever bit the
split view.

Two changes, and both are needed:

- `renderPaneGrid` snapshots the typing caret and restores it
  synchronously around the rebuild, so no keystroke lands in the gap and
  later deliberate focus moves (clicking a pane to select another session)
  still have the last word.
- `schedulePaneGridRefresh` compares a signature of everything the grid
  actually draws and skips the rebuild when none of it moved. Restoring
  focus alone is not enough: a blur/refocus kills an in-progress IME
  composition, so composed scripts would still lose syllables to a status
  tick that changes nothing on screen.

Verified against a throwaway daemon with two sessions in a split, driving
real `session/state` notifications:

  before   10 deltas -> 10 rebuilds -> 10 focus losses (first at 107ms)
  after    10 title changes -> 10 rebuilds ->  0 focus losses
           10 status ticks  ->  0 rebuilds ->  0 focus losses

Pane titles still track renames, clicking an unfocused pane still moves
focus there, and split/close still work.
@edwin-zvs
edwin-zvs merged commit 736f533 into main Aug 2, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the webui-split-focus branch August 2, 2026 22:24
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