Repository navigation
perf(workspace): keep terminals alive across Reader, Spotlight and Settings - #757
Conversation
…d Settings Refs #752 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…tings instead of unmounting it MainSurface rendered the focus-takeover surfaces instead of the GlobalEditorShell subtree, so entering Reader Mode with terminal panes disposed every xterm and detached every PTY, and leaving re-attached and replayed up to 512 KiB into each brand-new terminal — a multi-second freeze and lost scrollback per toggle. Global Editor fullscreen already retains the workspace under display:none and tells the terminal ownership boundary that mounted no longer means visible; RetainedWorkspaceSurface applies the same pattern one level up for the surfaces that bypass the shell, with a layout-neutral display:contents wrapper when visible. TileLeaf folds the new hidden flag into its Tail-All mask so un-hiding is a genuine false→true transition that re-pins, and the shell's fullscreen Escape listener stands down while hidden so Escape in Reader Mode exits Reader rather than also collapsing a fullscreen editor underneath. Fixes #752 Refs #745, #749 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…ship Review of #757 found the retention design shadowed in the production tree: the Global Editor slot nests inside the retained surface and provided its own terminal-visibility value, so React's nearest-provider rule hid the "Reader/Spotlight/Settings took the screen" signal from every pane whenever the editor was not fullscreen. Retained panes kept their dimension claim, measured a display:none box, left the PTY at Spotlight's size on exit and kept the inline debug terminal disabled. Visibility now composes: a subtree is visible only if every enclosing shell says so. The retained focused TileLeaf also kept every document-level input router — type-to-focus, paste-to-focus, the bare-Enter submit target, the dictation hotkey, the condition outlet — so typing "yes" + Enter in Reader Mode submitted a prompt through an invisible composer. Input ownership is now focused AND visible, and the caret/xterm refocus effects re-run on reveal, which the remount used to do for free. Shell panes (TerminalLeaf) had no ownership hook at all and would have kept Spotlight's PTY size after exit; they now reset their sent size when hidden and refit on reveal. Feeds skip the sticky-bottom pin while hidden and re-pin on reveal. Comments that described the takeovers as unmounting the workspace are corrected. Refs #752, #760 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
1fc8369 to
a2ea689
Compare
|
Review blockers addressed in a2ea689 (branch rebased onto main):
Re-review requested. |
…ontext and keep Monaco's focus on reveal Re-review of #757: deriving "hidden" from the editor store's global editorFullscreen flag made Spotlight's own visible leaf — rendered beside the shell — refuse Enter/y/n on an approval strip whenever the editor was fullscreen underneath. useInteractiveOwnership now derives interactive and hidden from the composed visibility context, which is scoped to the subtree, and TileLeaf's store read is gone. Reveal refocus (caret, xterm) now only takes an unowned focus: a takeover surface unmounting leaves focus on <body>, which the pane reclaims as the remount used to, but editor-fullscreen exit leaves it in Monaco, which must keep it. Hidden panes also unregister as dictation fallback targets (enabled, not just focused). TerminalLeaf gains the same retention round- trip test as the agent leaf, and the ownership derivation is pinned across the four shell combinations. Refs #752, #760 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
|
Re-review B4 and should-fixes addressed in the latest commit:
|
Juliusolsson05
left a comment
There was a problem hiding this comment.
Adversarial review round 4 (re-run after the pause; worktree HEAD 07521e0).
VERDICT: REQUEST-CHANGES
Rounds 1-3 findings are all correctly and completely fixed (B1 visibility shadowing — provider now ANDs with parent, pinned by real-slot test; B2 hidden-pane keyboard — TileLeaf and AgentTerminalLeaf fully gate type-to-focus/paste/Enter/dictation/condition/caret; B3 shell pane size/subscription — reset, rAF cancel, refit on reveal, no hidden measurement, pinned by retention test; B4 global store read gone). Z-index/pointer-events, teardown, and memory limitations all check out.
One remaining B2-class hole:
- BLOCKER — Hidden focused shell pane still owns the dictation hotkey.
TerminalLeaf.tsx:80-87registersuseComposerDictation({ enabled: dictationEnabled, focused, ... })with no visibility gate, while both siblings gate it (TileLeaf.tsx:662-667:enabled: … && !workspaceHidden, focused: interactive;AgentTerminalLeaf.tsx:100-101:enabled: … && ownerVisible, focused: focused && dimensionActive). The dictation registry prefers the focused target (useComposerDictation.ts:1236-1239, 1255-1290), the hotkey is a native global helper, and the retained tree keepsfocusedSessionIdon the shell pane during a takeover — so pressing the dictation key in Reader/Spotlight/Settings transcribes straight into the invisible shell's PTY. Fix: mirror the siblings —enabled: dictationEnabled && ownerVisible,focused: focused && ownerVisible. - NIT — ungated focus on attach (
AgentTerminalLeaf.tsx:361) runs regardless of visibility; benign today (focus()ondisplay:noneis a no-op) but inconsistent withTerminalLeaf.tsx:380. - NIT — hidden-pane HTML-trace MutationObserver keeps recording (
TileLeaf.tsx:747-779gates onhtmlDebugPanelOpen && focusedbut not visibility). Debug-only cost. - NIT — stray space in import list (
AgentTerminalLeaf.tsx:19).
Commands: 5 new/changed test files 14/14 PASS; feed/global-editor/reader/spotlight/shell/tile-tree suites 20 files 60/60 PASS; tsc -b clean.
TerminalLeaf registered useComposerDictation with raw enabled/focused, so a focused shell pane retained under display:none kept claiming the global dictation hotkey while Reader, Spotlight or Settings owned the screen — the transcript went straight into the invisible pane's PTY. Both sibling leaves already gate on visibility; this was the one leaf kind that didn't, the last B2-class hole from round 3. Addresses the round-4 review blocker.
|
Round-4 blocker fixed (7f6bb76): |
…fixes Preserve keyed PTY delivery, WebGL lifecycle, input engagement and resize coverage while retaining visibility ownership. Twenty focused renderer tests pass after resolving the overlap. Refs #752
Problem
With
agentViewMode = 'terminal', toggling Reader Mode (or Spotlight, or opening Settings) blanked every pane and re-loaded every terminal on exit:MainSurfacerendered the takeover surface instead ofGlobalEditorShell → TileTree, so React unmounted everyAgentTerminalLeaf(xterm disposed, PTY detached) and on exit mounted the tree from scratch — N × (attach + up to 512 KiB replay + parse + fit/resize handshake) on the renderer thread, a multi-second freeze, lost scrollback, and a burst of attach/resize/screen IPC. Owner's report and trace in #752.Behaviour
RetainedWorkspaceSurface(new,src/renderer/src/app/shell/) keeps the shell subtree mounted while a takeover surface is active:display:noneon a wrapper that isdisplay:contents(layout-neutral) otherwise,AgentTerminalOwnerVisibilityProvider visible={false}so hidden panes release their dimension claim and never measure, and aWorkspaceSurfaceHiddenContextfor consumers that need "retained but off-screen".MainSurfacerenders exactly one takeover surface (Settings > Reader > Spotlight, same priority as before) alongside the retained workspace instead of replacing it. The takeover surfaces still bypass the shell for the layout reasons documented there; only the shell's mount lifetime changes.TileLeafadds the hidden flag to its Tail-All visibility mask next to the editor-fullscreen term, so leaving a takeover is a real false→true transition that re-pins the feed.GlobalEditorShell's fullscreen Escape listener stands down while the surface is hidden, so Escape in Reader Mode exits Reader (owned byuseKeybinds) rather than also collapsing a fullscreen editor underneath.Design decisions
AgentTerminalOwnerVisibilityProviderANDs with its parent). Review found the editor slot nests inside the retained surface and provided its own value, so React's nearest-provider rule hid the takeover signal from every pane whenever the editor was not fullscreen — retained panes kept their dimension claim, measured adisplay:nonebox, left the PTY at Spotlight's size on exit and kept the inline debug terminal disabled. Now a subtree is visible only if every enclosing shell says so.useInteractiveOwnership(focused)derivesinteractive/hiddenfrom the composed visibility context — scoped to the subtree, never the editor store's global flag, which made Spotlight's own visible leaf refuse Enter/y/n under a fullscreen editor. A retainedTileLeafis still the workspace-focused one, so every document-level input router (type-to-focus, paste-to-focus, bare-Enter submit target, dictation hotkey and fallback registration, condition outlet) is gated on it; typing "yes" + Enter in Reader Mode no longer submits through an invisible composer. Caret / xterm refocus effects re-run on reveal but only take an unowned focus (focusIsUnowned): a takeover unmounting leaves focus on<body>, editor-fullscreen exit leaves it in Monaco, which keeps it.TerminalLeaf(shell panes) had no ownership hook; it now resets its sent size when hidden and refits on reveal. Feeds skip the sticky-bottom pin while hidden and re-pin on reveal.data-pane-idis therefore duplicated under Spotlight; lookups take the first in DOM order, which is the takeover surface becauseMainSurfacerenders it before the retained workspace (documented at the attribute).Linked issues
Fixes #752. Refs #760 (structural follow-up), #745, #749, #103, #390.
Verification
RetainedWorkspaceSurface.renderer.test.tsx(2, realGlobalEditorWorkspaceSlotinside the surface): the dimension claim is released while hidden and restored on reveal; kept while visible even though the slot provides its own value.AgentTerminalLeaf.dimensionOwnership.renderer.test.tsx(+1) and newTerminalLeaf.retention.renderer.test.tsx: a retained pane under the real slot sends exactly one resize on attach, none while hidden, and exactly one more on reveal at the same size.useInteractiveOwnership.renderer.test.tsx(5): no shell → interactive; hidden surface, fullscreen slot, visible slot inside a hidden surface → not; both visible → interactive, unfocused → not.TerminalDimensionOwnership.renderer.test.tsx(3): the pane terminal stays mounted and hidden under Settings while dimension ownership still hands to the inline debug terminal and returns afterwards (mount count 1, unmounts 0); Reader Mode round trip keeps every pane mounted; Spotlight target-guard adapted to the retained tree.--project rendererforapp/,workspace/tile-tree/,features/global-editor/,features/reader/,features/spotlight/,features/feed/— 20 files, 60 tests passed;npx tsc -b— clean.Limitations / follow-ups
agent-pty-dataandsession:screenand hidden feeds still re-render on every runtimes replacement; suspending delivery/rendering for off-screen sessions is the next structural step (perf(terminal): own xterm instances outside the React tree (per-session terminal registry) #760 covers the terminal half).useKeybindsskips Reader-exit while the fullscreen editor owns the workspace (pre-existing) and the shell's own Escape listener now stands down while hidden. Reader still exits via its toggle command.TileLeafunder Reader Mode; the derivation that gates every router is pinned byuseInteractiveOwnership.renderer.test.tsxacross the four shell combinations, and the wiring into each router is a grep-level review item.LazyEntry's retention observer collapses rows underdisplay:noneand re-mounts the viewport rows on reveal (same as editor fullscreen today); "a style change rather than a rebuild" holds for terminals, which is what perf(workspace): reader mode, spotlight and settings unmount every terminal pane and replay all PTYs on exit #752 is about.🤖 Generated with Claude Code
https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
Current integration verification (2026-09-04)
Integrated merged main feb6690, preserving both retained-visibility behavior and the newer keyed PTY/WebGL/input tests. Head d9b7848 passes both full host CI gates; 20 focused renderer tests pass. Earlier hidden-shell dictation review feedback is fixed in the current head. This retains hidden trees; it does not freeze their React work or implement a global terminal registry. Ready for owner merge confirmation.
Stacked merge order
Final head 878fc37 includes #776, with both wake and retained-terminal regressions preserved. This PR now targets fix/wake-readiness-no-kill; merge #776 first, then this PR into main after GitHub retargeting is confirmed. Fifteen combined wake/host tests pass. Both full host CI gates pass on final stacked head 878fc37 (run 33938163643). Ready for owner merge confirmation.