6.1 KiB
Terminal Hidden View Parking
Status: Shipped — Phase 1 of the terminal model/view architecture, kill switch
terminalHiddenViewParking (default on). See
terminal-model-view-contract.md for the
invariants this design extends and the full phase list.
Problem
Hidden terminal panes keep a full renderer xterm instance alive (buffer, scrollback, DOM, addons). At many-worktree scale this is the dominant renderer memory cost, and it forces every hidden byte through renderer-side write/skip decisions. The main-process model (daemon + runtime headless emulators) already ingests every byte and can serve restorable snapshots, so the renderer view for a long-hidden pane is redundant state.
A previous attempt shipped and was reverted the same day. The post-mortem finding: parking unmounted the pane component, which also tore down the renderer's PTY byte parsers — and those parsers are the only source of bell notifications, title-transition agent-complete notifications, and tab titles. A parked worktree whose agent finished would never notify. This design keeps those side effects alive while parked.
Design
Park policy (renderer)
A pure policy module decides which hidden terminal tabs may park:
- Cold-park hysteresis: a tab must be hidden for 30s before parking.
- Hot-retain working set: recently visible worktrees/tabs are retained
(15 minutes; up to 8 worktrees / 12 tabs) so quick tab switches never pay a
re-hydrate. The single most-recently-hidden (last-active) worktree/tab is
exempt from both the retain TTL and the count cap, so switching back to the
view you just left is always instant regardless of how long you were away.
See
terminal-cold-park-reveal-cost.mdfor the data behind these caps. - Eligibility excludes: visible panes, hidden-measuring startup probes,
activity-portal panes, tabs with pending startup commands or pending
activation spawns, floating-panel tabs, and any tab whose PTY is not
snapshot-backed (remote-runtime
remote:PTYs and SSH PTYs are excluded). - Kill switch:
settings.terminalHiddenViewParking === falsedisables parking entirely.
Park mechanics
Parking a tab unmounts its TerminalPane React subtree (the overlay layer
renders null for parked tabs). This is the same teardown that tab-group moves
already exercise: transports detach but the PTY session, daemon model, and tab
state all survive. The xterm instance, its buffers, DOM, and WebGL/addon
resources are released.
Parked watcher (the piece the reverted attempt lacked)
While a tab is parked, a pane-less watcher
(parked-terminal-byte-watcher.ts) keeps the pane's side effects alive. Its
consumption mode is decided once at watcher start:
- Main side-effect authority on (default): the watcher is purely
fact-driven — it registers exactly one
pty:sideEffectfact consumer and parses no bytes. Titles, agent working/idle/exited transitions, BEL attention, and PR links arrive as main-tracker facts and drive the same policy callbacks a mounted pane uses. With the hidden-delivery gate also on, the watcher marks the PTY hidden so main stops renderer byte delivery entirely; the DECSET 2031 color-scheme subscribe arrives as main's2031-subscribefact and the watcher replies out-of-band viatransport.sendInput. - Kill switch off: the watcher subscribes to raw bytes through the
dispatcher sidecar mechanism (the same mechanism background agent launches
use) and runs the transport-level byte parsers with no xterm — OSC 0/1/2
titles (all-titles ordering, live-path normalization), the title-transition
agent tracker (completion notification, prompt-cache timer), the OSC-aware
stateful BEL detector, the GitHub PR link scan, and a dedicated DECSET 2031
byte responder (
parked-terminal-mode2031-responder.ts, whosesubscribeToPtyDataregistration doubles as the delivery-interest signal).
The two modes drive one shared policy-callback block, so flipping the kill
switch never changes notification semantics. Main's synthetic
agent-title/permission frames feed the main tracker directly and arrive as
facts; the legacy synthetic pty:data copy exists only in kill-switch-off
mode.
Out of scope while parked: OSC 52 clipboard writes. Terminal queries inside
hidden-dropped chunks are answered by main's model responder
(terminal-query-authority.md); in
kill-switch-off byte mode only the 2031 reply is answered and Command Code
output is not scraped, matching the pre-gate status quo.
Reveal
Revealing a parked tab remounts the pane subtree and rides the existing
reattach path: fresh xterm via openTerminal (unicode provider activation
before any write), daemon model snapshot > relay replay > cold restore
precedence, replay-guarded so snapshot-embedded queries never answer, then
POST_REPLAY_REATTACH_RESET hygiene, fit, and PTY resize. The watcher is
disposed before the pane handlers re-register.
Invariants
- PTY reads never stop; parking only changes renderer-side view lifetime.
- Bell, agent-completion, title, and PR-link side effects keep working while parked (watcher parity tests).
- Reveal shows model-correct output (visual gates: hidden TUI restore, long table, rendering golden) and accepts input immediately.
- Sleep/wake, pane close, and PTY restart while parked must not leak watchers or strand parked state.
- Memory: parked tabs hold no xterm buffers; renderer memory scales with visible panes.
Relation to later phases (all shipped)
Side-effect authority in main (Phase 3) replaced the watcher's byte parsing
with the pty:sideEffect fact consumer; the hidden-delivery gate (Phase 4)
stops hidden byte delivery in main, moving the parked 2031 reply from the
byte sidecar to the 2031-subscribe fact; the model query responder
(Phase 5) answers queries in hidden-dropped chunks. The watcher's byte-parser
mode survives only behind the kill switches. Parking still excludes
remote-runtime and SSH PTYs (no local snapshot to restore from); the watcher
would return as a byte parser only if remote-runtime tabs — whose bytes never
transit local main — ever became parkable.