Stop fleet activity from taking the caret out of a split pane - #1177
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
renderSessionsruns on everysession/statedelta — a background worker's status tick, a token count, a spinner frame — and schedules a pane-grid rebuild:renderPaneGridrebuilds 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, thenreplaceChildrens the grid. Detaching the node that holdsdocument.activeElementblurs 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
schedulePaneGridRefreshand never rebuilds, which is why this only ever bit the split view.The fix
Two changes, both needed:
renderPaneGridpreserves the typing caret — snapshot before, restore synchronously after, reusing the existingcaptureTypingFocus/restoreTypingFocushelpers (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.schedulePaneGridRefreshskips 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
renderPaneGriddirectly and always repaint.Verification
Throwaway daemon, two sessions in a 2-pane split, driving real
session/statenotifications throughhandleNotification, comparing main's assets against this branch's viadev.set_assets:Terminal surface checked separately with a real xterm mounted in the view stack:
captureTypingFocustakes itsterminalbranch, 8 rebuild-forcing deltas, 0 losses, focus still onxterm-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