fix(terminal): prevent paired activity renderer starvation (#13508)

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
Jinwoo Hong
2026-08-10 14:32:08 -07:00
committed by GitHub
co-authored by Neil
parent c0c893d171
commit ec7e3ea477
17 changed files with 2916 additions and 86 deletions
+109 -7
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"updatedAt": "2026-08-09",
"updatedAt": "2026-08-10",
"policy": {
"maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"],
"blockingPromotion": {
@@ -4315,14 +4315,16 @@
"providers": ["paired-runtime", "ssh"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["paired-runtime"],
"coverageNotes": "Deterministic headed macOS runs launch an isolated Orca desktop server and a separate paired web client. A byte-identical headless run uses an isolated `orca serve` host. Both create six real paired host PTYs with bounded high-output scrollback, prove sustained output while all six client xterms are warm-mounted but hidden causes zero renderer scheduler work, ordinary-park five xterms without enabling the lossy retention budget, assert bounded cells/heap/timer lag, then restore the bounded authoritative tail exactly once on the original PTY including output produced while parked and continued input/output. The headed oracle additionally keeps every remote target workspace and terminal unmounted in the host renderer before and after client recovery. A separate headed desktop-client journey relaunches the host main process while retaining the daemon and a daemon-backgrounded PTY, then requires first-observation foreground sync and continued input/output on the same PTY. Unit coverage keeps layout versions out of snapshot output high-waters when a restored snapshot has no output sequence and retries an existing subscriber only after authoritative provider inventory proves the retained PTY exists. Separate headed/headless ACK-starvation tests recover one stalled stream without replacing its PTY. A headed output-drop oracle proves a successful snapshot probe cannot certify a stale live stream when the authoritative PTY sequence advanced beyond the client high-water. Unit coverage requires an unsequenced client to establish a first-probe baseline, remain attached at the same sequence, and recover only after a later probe advances. A capability-disabled run proves legacy hosts retain the prior lossy limit/TTL fallback. Unit tests cover exact-owner capability routing, raw-stream release, singleton side-effect facts with timed handoff cleanup, 64 simultaneous synthetic-title sources with zero decorative client events after convergence, per-client late-subscribe recovery, semantic bell/title transitions, local animation-frame preservation, legacy no-output-pause delivery gating with exact snapshot restore, provider-authoritative snapshots, 128 active streams plus retry after a 129th-stream capacity rejection, capacity-pressure backoff, full split-leaf remint reconciliation, truncation, and manual-disconnect queue fencing. Linux, Windows, live SSH, and production-scale paired hosts remain gaps.",
"coverageNotes": "Deterministic headed macOS runs launch an isolated Orca desktop server and a separate paired web client. A byte-identical headless run uses an isolated `orca serve` host. Both create six real paired host PTYs with bounded high-output scrollback, prove sustained output while all six client xterms are warm-mounted but hidden causes zero renderer scheduler work, then drive 180 timed real-PTY spinner frames through a second production `session.tabs.subscribeAll` transport and the real Zustand store. Each worktree must publish and apply only its exact initial-working and final-idle titles. Both topologies ordinary-park five xterms without enabling the lossy retention budget, assert bounded cells/heap/timer lag, then restore the bounded authoritative tail exactly once on the original PTY including output produced while parked and continued input/output. The headed oracle additionally keeps every remote target workspace and terminal unmounted in the host renderer before and after client recovery. Production-parity React contracts reproduce and remove the independent cold-park watcher/title feedback loop, including memo-cache discard, StrictMode cleanup replay, detached split-PTY replacement, and the zero-park fast path. A deterministic cross-boundary contract drives 24 real PTY trackers in distinct worktrees through 2,880 decorative spinner and bare-native redraw chunks and counts host publications, serialized bytes, renderer apply calls, and store mutations while preserving exact per-PTY raw chunks and per-worktree semantic transitions. Fake-clock continuation beyond the 30-minute stale boundary requires one deduplicated refresh per worktree every 15 minutes, globally spaced by at least 50 ms, with every mirrored working status still fresh. Exact-identical Pi plus rotating normalized Pi/Grok frames renew coarse working/permission state; rich prompt/tool/question fields retain their original 30-minute lease, and `stateStartedAt`/acknowledgement identity remain stable across title/hook source swaps. A separate headed desktop-client journey relaunches the host main process while retaining the daemon and a daemon-backgrounded PTY, then requires first-observation foreground sync and continued input/output on the same PTY. Unit coverage keeps layout versions out of snapshot output high-waters when a restored snapshot has no output sequence and retries an existing subscriber only after authoritative provider inventory proves the retained PTY exists. Separate headed/headless ACK-starvation tests recover one stalled stream without replacing its PTY. A headed output-drop oracle proves a successful snapshot probe cannot certify a stale live stream when the authoritative PTY sequence advanced beyond the client high-water. Unit coverage requires an unsequenced client to establish a first-probe baseline, remain attached at the same sequence, and recover only after a later probe advances. A capability-disabled run proves legacy hosts retain the prior lossy limit/TTL fallback. Unit tests cover exact-owner capability routing, raw-stream release, singleton side-effect facts with timed handoff cleanup, 64 simultaneous synthetic-title sources with zero decorative client events after convergence, per-client late-subscribe recovery, semantic bell/title transitions, local animation-frame preservation, legacy no-output-pause delivery gating with exact snapshot restore, provider-authoritative snapshots, 128 active streams plus retry after a 129th-stream capacity rejection, capacity-pressure backoff, full split-leaf remint reconciliation, truncation, and manual-disconnect queue fencing. Linux, Windows, live SSH, and production-scale paired hosts remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/8652",
"https://github.com/stablyai/orca/pull/10625"
],
"invariant": "A host advertising terminal.paired-parking.v1 keeps the PTY and bounded authoritative history alive while an ordinary hidden-view park destroys the client xterm and releases its raw per-PTY stream. Reveal must restore up to the requested 5,000 rows, parked-time side effects/output, the same PTY identity, and continued input/output. After a host main-process relaunch, unknown local delivery-sync state must not be treated as proof that a surviving daemon PTY is already foregrounded. If subscription precedes provider readiness, authoritative inventory must reconsider that existing subscriber without spawning, resizing, or changing reconnect state. A snapshot without an output high-water must keep it unknown rather than borrowing a layout version that can suppress newer live output. Hosts without the capability must gate hidden raw output before xterm scheduling and repaint from the authoritative snapshot on reveal while retaining the existing limit/TTL force-parking fallback. After a paired client's first semantic title state, decorative spinner frequency must add zero encrypted client-event frames; local title animation and semantic title, status, bell, completion, and query facts remain intact. A paired terminal stream whose delivery credits stop progressing must replace only that stream; command silence first probes authoritative state and replaces the stream if the probe times out or proves the PTY advanced beyond the client's delivered output sequence. When no comparable delivery high-water exists, the first sequenced probe establishes a baseline and only later advancement proves staleness. A same-sequence snapshot remains valid proof of a responsive silent command. A successful status probe may replace a pre-ready shared-control socket without rejecting or duplicating calls already waiting for that transport. Manual disconnect must retain pairing while preventing queued or passive calls and subscriptions from recreating transport until explicit Connect.",
"oracle": "Run one byte-identical six-terminal oracle against an isolated headed desktop host and an isolated headless `orca serve` host. Stage at least 1,000,000 xterm cells, enable ordinary parking with the lossy retention budget disabled, require exactly one mounted manager and five parked tabs, at most 45% retained cells, no more than 16 MiB heap growth, and under 500 ms timer drift. In headed mode, require the host renderer to remain on its original workspace with zero target terminal managers mounted throughout client park, reveal, and live I/O. While a tab is parked, require authoritative terminal.read to observe new PTY output; reveal it and require the original PTY, a marker within the requested 5,000-row history, the parked marker, and post-reveal input/output. Drive 64 host PTYs through ten 80 ms synthetic title frames, require zero paired client events after the first frame per PTY while all 640 local frames remain observable, attach a late client and require one current frame per PTY, then require semantic bell and idle transitions on both clients. Reconstruct a legacy subscribed stream with no outputPause capability, hide a chatty pane, require no hidden xterm writes, reveal it, and require one authoritative snapshot plus continued live output. Drop and acknowledge output only for one original paired-client stream, prove the fixture process consumed input while the host model advanced and the client stayed stale, then require the command snapshot probe to replace that stream, repaint exact fixture output, preserve authoritative PTY identity and target-tab cardinality, and resume live I/O. For a client without a delivered sequence, require the first numeric probe to establish a baseline without replacement, a same-sequence probe to remain attached, and a later advanced probe to replace only that stream. Withhold the first encrypted shared-control ready frame, start one RPC, trigger a status-probe refresh, and require two connections, one host delivery, successful response, and zero retained request bytes. Admit 128 active streams, reject the 129th as retryable, release one stream, then require the retry to attach and publish a snapshot without multiplying retained subscribers. Disable terminal.paired-parking.v1 and require the same oracle to fail before parking, while the legacy limit-one fallback separately passes. Also preserve truncated first paint, ACK-starved same-PTY recovery, responsive silent-command snapshot probes, dead-stream probe timeout recovery, and queued manual-disconnect fencing.",
"invariant": "A host advertising terminal.paired-parking.v1 keeps the PTY and bounded authoritative history alive while an ordinary hidden-view park destroys the client xterm and releases its raw per-PTY stream. Reveal must restore up to the requested 5,000 rows, parked-time side effects/output, the same PTY identity, and continued input/output. Parked watcher synchronization compares parking semantics rather than fresh object identity, owns setup and cleanup across StrictMode replay, reconciles real PTY/layout changes, and performs no terminal-state scan when nothing is parked. After a host main-process relaunch, unknown local delivery-sync state must not be treated as proof that a surviving daemon PTY is already foregrounded. If subscription precedes provider readiness, authoritative inventory must reconsider that existing subscriber without spawning, resizing, or changing reconnect state. A snapshot without an output high-water must keep it unknown rather than borrowing a layout version that can suppress newer live output. Hosts without the capability must gate hidden raw output before xterm scheduling and repaint from the authoritative snapshot on reveal while retaining the existing limit/TTL force-parking fallback. After the first semantic title state, decorative spinner frequency must add zero paired client-event frames and zero full session-tabs work between status-freshness leases regardless of hidden worktree count. Continuous working or permission evidence may refresh each affected worktree once per 15 minutes through one globally spaced FIFO so current and legacy viewers do not decay before 30 minutes; sibling PTYs share that worktree refresh, while local title animation, raw output, and semantic title, status, bell, completion, and query facts remain intact. A paired terminal stream whose delivery credits stop progressing must replace only that stream; command silence first probes authoritative state and replaces the stream if the probe times out or proves the PTY advanced beyond the client's delivered output sequence. When no comparable delivery high-water exists, the first sequenced probe establishes a baseline and only later advancement proves staleness. A same-sequence snapshot remains valid proof of a responsive silent command. A successful status probe may replace a pre-ready shared-control socket without rejecting or duplicating calls already waiting for that transport. Manual disconnect must retain pairing while preventing queued or passive calls and subscriptions from recreating transport until explicit Connect.",
"oracle": "Run one byte-identical six-terminal oracle against an isolated headed desktop host and an isolated headless `orca serve` host. Stage at least 1,000,000 xterm cells, hide all six warm managers, emit an initial working title followed by 30 timed real-PTY spinner frames and a final idle title per worktree, and observe a second real session-tabs subscription plus the production renderer store. Require exactly two transport envelopes and two title mutations per worktree, no decorative title crossing either boundary, and positive decoded-envelope bytes. Enable ordinary parking with the lossy retention budget disabled, require exactly one mounted manager and five parked tabs, at most 45% retained cells, no more than 16 MiB heap growth, and under 500 ms timer drift. In headed mode, require the host renderer to remain on its original workspace with zero target terminal managers mounted throughout client park, reveal, and live I/O. While a tab is parked, require authoritative terminal.read to observe new PTY output; reveal it and require the original PTY, a marker within the requested 5,000-row history, the parked marker, and post-reveal input/output. Drive 24 real PTY title trackers in distinct worktrees through 40 decorative spinner frames each, then 40 spinner frames alternating with Cursor's bare native identity redraw, using fake clocks. Require all 2,880 raw chunks exactly and zero host session-tabs publications, serialized bytes, renderer apply calls, and renderer store mutations. Continue decorative evidence beyond 30 minutes, require exactly one refresh per worktree per 15-minute lease through a FIFO spaced by at least 50 ms, and require all 24 viewer statuses to remain working and fresh; then require one exact semantic idle transition and one visible-output chunk per PTY. Require ordinary and interior-Braille title changes to publish. Drive 64 host PTYs through ten 80 ms synthetic title frames, require zero paired client events after the first frame per PTY while all 640 local frames remain observable, attach a late client and require one current frame per PTY, then require semantic bell and idle transitions on both clients. Reconstruct a legacy subscribed stream with no outputPause capability, hide a chatty pane, require no hidden xterm writes, reveal it, and require one authoritative snapshot plus continued live output. Drop and acknowledge output only for one original paired-client stream, prove the fixture process consumed input while the host model advanced and the client stayed stale, then require the command snapshot probe to replace that stream, repaint exact fixture output, preserve authoritative PTY identity and target-tab cardinality, and resume live I/O. For a client without a delivered sequence, require the first numeric probe to establish a baseline without replacement, a same-sequence probe to remain attached, and a later advanced probe to replace only that stream. Withhold the first encrypted shared-control ready frame, start one RPC, trigger a status-probe refresh, and require two connections, one host delivery, successful response, and zero retained request bytes. Admit 128 active streams, reject the 129th as retryable, release one stream, then require the retry to attach and publish a snapshot without multiplying retained subscribers. Disable terminal.paired-parking.v1 and require the same oracle to fail before parking, while the legacy limit-one fallback separately passes. Also preserve truncated first paint, ACK-starved same-PTY recovery, responsive silent-command snapshot probes, dead-stream probe timeout recovery, and queued manual-disconnect fencing.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts --maxWorkers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts --maxWorkers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/runtime/rpc/terminal-subscribe-buffer.test.ts src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.test.ts src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts src/renderer/src/components/terminal-pane/terminal-parked-watcher-partial-reconciliation.test.ts src/renderer/src/components/terminal-pane/terminal-parking-e2e-overrides.test.ts src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts src/renderer/src/runtime/runtime-client-events.test.ts src/renderer/src/web/web-preload-api.test.ts src/main/ipc/runtime-environments.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/remote-runtime-shared-control-connection.test.ts",
@@ -4335,11 +4337,20 @@
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-stall-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-probe-gap-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-stall-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/terminal-parked-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1"
],
"testFiles": [
"src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts",
"src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts",
"tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts",
"tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts",
"src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx",
"src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx",
"src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx",
"src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts",
"src/main/runtime/orca-runtime.test.ts",
"src/renderer/src/web/web-preload-api.test.ts",
"src/main/ipc/runtime-environments.test.ts",
@@ -4371,6 +4382,59 @@
"tests/e2e/paired-remote-terminal-host-restart-background-sync.spec.ts"
],
"assertionRefs": [
{
"file": "src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts",
"assertions": [
"one globally spaced FIFO refreshes each continuously active worktree before status expiry",
"sibling PTYs deduplicate to one worktree refresh while semantic transitions bypass the queue",
"sole-source PTY teardown removes queued work while sibling PTY sources remain deduplicated",
"worktree, pending-queue, and timer state is removed on teardown"
]
},
{
"file": "src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts",
"assertions": [
"a live pending question remains visible under the current live shell title but cannot cross a later working interval, identity-only owner title, or provider generation",
"inherited tool names, stale rows, resume-only rows, and unconfirmed restored rows cannot reopen a shell-reclaimed pane"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx",
"assertions": [
"parked terminal and unified-label title republication settles without a maximum-depth loop",
"equivalent semantic inputs remain stable when React discards memoized identities"
]
},
{
"file": "src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx",
"assertions": [
"StrictMode cleanup replay restores watcher ownership",
"detached split-PTY and layout replacement reconciles",
"unparked unrelated writes perform zero PTY-map reads"
]
},
{
"file": "tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts",
"assertions": [
"2,880 decorative spinner and bare-native real-PTY OSC chunks publish zero session-tabs snapshots or serialized bytes after semantic convergence",
"the viewer performs zero snapshot applies and zero store mutations during decorative activity",
"after 30 minutes, one globally spaced refresh per worktree keeps every mirrored working status fresh",
"exact Pi and rotating normalized Pi/Grok frames renew explicit and fallback status without changing stateStartedAt or acknowledgement semantics",
"matching title evidence renews retained OSC hook status without resetting stateStartedAt, then drops unverifiable prompt metadata after the hook row expires",
"renderer-authored rich status keeps its original lease, then renews only coarse title-confirmed state and provider identity",
"ordinary-shell title transitions replace working stateStartedAt with the completion transition time",
"decorative heartbeats remain blocked until unknown foreground ownership resolves, then resume at the bounded interval",
"tracked terminal state reset clears delayed foreground ownership markers",
"raw chunks, semantic idle transitions, and visible output remain exact"
]
},
{
"file": "tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts",
"assertions": [
"pending renderer, retained OSC, and live-hook questions survive the shell title that currently obscures them",
"a new working interval, provider generation, or identity owner strips the prior question"
]
},
{
"file": "src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts",
"assertions": ["retries an existing subscriber when provider inventory becomes ready"]
@@ -4425,6 +4489,7 @@
"assertions": [
"ordinary-parks paired terminals and restores authoritative host scrollback",
"warm-mounted hidden paired streams schedule zero renderer output work under sustained host output",
"six hidden real PTYs publish and apply only exact working and idle title transitions across paired transport",
"client recovery completes while every target terminal remains unmounted in the host renderer"
]
},
@@ -4433,6 +4498,7 @@
"assertions": [
"ordinary-parks paired terminals against an isolated headless Orca host",
"cold-activates only visible paired terminals against an isolated headless host",
"the headless host bounds real-PTY decorative title transport and production store application",
"the headless host shares the zero-hidden-renderer-work and exact-restoration contract"
]
},
@@ -4527,6 +4593,42 @@
}
],
"evidenceRuns": [
{
"date": "2026-08-10",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts --maxWorkers=1",
"result": "passed",
"durationSeconds": 1.56,
"summary": "All 16 production-parity parking/watcher contracts passed on the combined candidate, covering the maximum-depth loop, semantic stability, StrictMode ownership, split-PTY reconciliation, and zero-park fast path."
},
{
"date": "2026-08-10",
"runner": "local",
"platform": "macos",
"command": "ORCA_E2E_WEB_CLIENT=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"result": "passed",
"durationSeconds": 27.6,
"summary": "The isolated headed desktop host and separate encrypted paired client drove 180 timed decorative real-PTY titles across six hidden worktrees and observed exactly 12 semantic transport envelopes and 12 production store title mutations, then reduced six managers to one, stayed within heap/cell/timer budgets, restored the bounded host tail exactly, and continued input/output while the host renderer retained zero target managers."
},
{
"date": "2026-08-10",
"runner": "local",
"platform": "macos",
"command": "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"result": "passed",
"durationSeconds": 33.9,
"summary": "The isolated headless `orca serve` host passed the same six-worktree real-title transport/store bound, parking, and exact-restoration oracle; a second scenario proved cold activation mounted only the visible paired terminal."
},
{
"date": "2026-08-10",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts --maxWorkers=1",
"result": "passed",
"durationSeconds": 5.62,
"summary": "Forty-two deterministic contracts passed. The real-PTY cross-boundary contract delivered all 2,880 decorative spinner and bare-native redraw chunks with zero hot-path host publications, serialized bytes, renderer applies, or store mutations; continued past 30 minutes with one globally spaced freshness refresh per worktree every 15 minutes; kept all 24 mirrored statuses working; and preserved stable stateStartedAt. Exact-identical Pi and rotating normalized Pi/Grok frames renewed renderer-authored and retained-hook status without changing acknowledgement semantics. Rich status could not cross task completion, permission, provider-generation, or identity-owner boundaries, while neutral titles preserved same-task prompt and acknowledgement identity until authoritative shell evidence. Fresh renderer, retained OSC, and live-hook questions remained visible under the current shell title, but a later working interval, identity-only owner title, or provider generation stripped them; inherited tools and stale, resume-only, restored, or prior-interval questions stayed suppressed. After each rich source's original lease expired, title evidence kept only coarse working state and provider identity fresh while dropping unverifiable prompts. Unknown foreground ownership held publications at zero until resolution, then resumed the bounded heartbeat; tracked terminal state reset removed its delayed marker. The focused heartbeat contracts proved worktree deduplication, FIFO fairness, semantic bypass, PTY-source teardown, and timer cleanup. Causal-disable controls made ownership, teardown, retained source-switch, renderer rich-TTL, completion-transition, boundary-ordering, and live-question oracles fail; restoring the production corrections returned green."
},
{
"date": "2026-08-07",
"runner": "local",
@@ -4691,11 +4793,11 @@
},
"redGreenEvidence": {
"status": "complete",
"evidence": "The six-PTY ordinary-parking oracle was red in two independently observed stages: the parked paired remount first used detached attach and restored only rows 5,9635,999, then the requested snapshot still lost to the current-screen replay until park reveals explicitly entered the capability-gated reattach coordinator. The candidate is green on headed and headless hosts, while disabling terminal.paired-parking.v1 makes the byte-identical oracle fail at its capability precondition and leaves the separately tested legacy lossy fallback green. The 64-PTY decorative-title control delivered 576 client events after convergence on unmodified main; the candidate delivers zero while preserving all 640 local frames, one current frame to a late client, and bell/idle transitions to both clients. The legacy no-output-pause control failed to request a reveal snapshot and kept hidden bytes on the xterm path; the candidate gates those bytes locally and restores one authoritative snapshot before live output. With stalled-stream recovery disabled, the host cursor advanced but the client remained frozen; enabling it repainted the marker with the same PTY and tab. The sequence-gap unit and headed paired oracle are red on current main and with only the new probe decision reverted: the authoritative host advances while the client stays stale after a successful probe. The candidate replaces the stream, repaints the marker, preserves PTY identity, and resumes I/O. Reverting the active-stream limit to 64 makes the exact 128-stream contract fail at 64; restoring 128 admits every intended stream, rejects 129, and reattaches it after one slot releases. The host-relaunch oracle fails when unknown delivery state is coerced to foregrounded or a layout version becomes an output high-water; the candidate foregrounds the preserved daemon PTY once and resumes same-PTY input/output. When authoritative inventory overlaps a refused in-flight attach, removing only the post-settlement retry leaves the existing subscriber without daemon output; the candidate retains one waiter across repeated inventory publications, retries exactly once, and restores live output."
"evidence": "The six-PTY ordinary-parking oracle was red in two independently observed stages: the parked paired remount first used detached attach and restored only rows 5,9635,999, then the requested snapshot still lost to the current-screen replay until park reveals explicitly entered the capability-gated reattach coordinator. The candidate is green on headed and headless hosts, while disabling terminal.paired-parking.v1 makes the byte-identical oracle fail at its capability precondition and leaves the separately tested legacy lossy fallback green. The parked-title production-parity control throws Maximum update depth exceeded on the affected parking synchronization and settles with semantic dependencies; forced memo-cache discard, StrictMode cleanup replay, and split-PTY replacement are independently red before their corrections and green on the combined candidate. The real-PTY session-tabs oracle produces 96 full snapshots, 64,032 serialized bytes, 96 renderer applies, and 96 store mutations on unmodified main and on a one-line causal revert before reaching the alternating native-title phase; the candidate produces zero in all four dimensions while preserving all 2,880 redraw chunks and 24 exact semantic transitions. The 64-PTY decorative-title control delivered 576 client events after convergence on unmodified main; the candidate delivers zero while preserving all 640 local frames, one current frame to a late client, and bell/idle transitions to both clients. The legacy no-output-pause control failed to request a reveal snapshot and kept hidden bytes on the xterm path; the candidate gates those bytes locally and restores one authoritative snapshot before live output. With stalled-stream recovery disabled, the host cursor advanced but the client remained frozen; enabling it repainted the marker with the same PTY and tab. The sequence-gap unit and headed paired oracle are red on current main and with only the new probe decision reverted: the authoritative host advances while the client stays stale after a successful probe. The candidate replaces the stream, repaints the marker, preserves PTY identity, and resumes I/O. Reverting the active-stream limit to 64 makes the exact 128-stream contract fail at 64; restoring 128 admits every intended stream, rejects 129, and reattaches it after one slot releases. The host-relaunch oracle fails when unknown delivery state is coerced to foregrounded or a layout version becomes an output high-water; the candidate foregrounds the preserved daemon PTY once and resumes same-PTY input/output. When authoritative inventory overlaps a refused in-flight attach, removing only the post-settlement retry leaves the existing subscriber without daemon output; the candidate retains one waiter across repeated inventory publications, retries exactly once, and restores live output."
},
"performanceBudget": {
"required": true,
"evidence": "Before ordinary parking, six warm-mounted hidden paired xterms receive zero raw frames and schedule zero renderer output drains while host models continue ingesting sustained output. Ordinary parking then retains the host PTY and bounded 5,000-row provider model while destroying five client xterms. Decorative title ticks are collapsed per paired subscriber before JSON serialization and encryption; semantic facts remain on the singleton client-event stream. Headed and headless oracles require exact reduction from six managers to one, at least 55% staged xterm-cell release, no more than 16 MiB heap growth, and under 500 ms timer drift. Legacy hosts retain the prior 12-worktree/45-minute lossy force-parking policy but now gate hidden raw output before xterm scheduling and restore from a bounded snapshot on reveal. Stream recovery adds no polling and is scoped to one stream. Provider-ready reconciliation runs only for PTYs returned by a successful controller inventory, requires an existing remote subscriber, and retains at most one post-settlement waiter per PTY while attach deduplication remains active."
"evidence": "Before ordinary parking, six warm-mounted hidden paired xterms receive zero raw frames and schedule zero renderer output drains while host models continue ingesting sustained output. The headed and headless production transports each admit only the initial and final semantic title snapshots per worktree across 180 timed decorative frames, and the real store performs only those 12 exact title mutations. Ordinary parking then retains the host PTY and bounded 5,000-row provider model while destroying five client xterms. Parked watcher synchronization is keyed by bounded parking semantics rather than fresh object identities; unrelated store writes perform zero PTY-map reads when no tab is parked, while parked reconciliation remains bounded by the worktree's terminal tabs and assignments. Decorative title ticks are collapsed both on the paired client-event channel and at the authoritative real-PTY session-tabs classifier before snapshot construction, serialization, transport, recovery, or store application; semantic facts remain exact. Headed and headless oracles require exact reduction from six managers to one, at least 55% staged xterm-cell release, no more than 16 MiB heap growth, and under 500 ms timer drift. The forced hidden-worktree retention policy keeps at most four otherwise un-parkable worktrees mounted and force-parks all after 15 minutes; the separate ordinary hot-retain policy keeps at most four recently visible worktrees warm for five minutes. Legacy hosts still gate hidden raw output before xterm scheduling and restore from a bounded snapshot on reveal. Stream recovery adds no polling and is scoped to one stream. Provider-ready reconciliation runs only for PTYs returned by a successful controller inventory, requires an existing remote subscriber, and retains at most one post-settlement waiter per PTY while attach deduplication remains active."
},
"promotionCriteria": [
"Collect 100 consecutive CI passes or 14 days of soak history for the headed and headless parking commands.",
@@ -4712,7 +4814,7 @@
"The legacy protocol shape is deterministic unit coverage; a packaged pre-v1.4.163 host has not yet run the full headed memory oracle.",
"The live windows-issues incident recorded two fast snapshot-probe successes while fresh output remained stale, but that release did not log the returned snapshot sequence; the deterministic sequence-gap oracle matches the observed false-health boundary without proving the field transport's lower-level drop mechanism."
],
"demotionRule": "Keep experimental or demote to protection none if decorative title frequency changes paired client-event volume after convergence, a semantic fact is dropped, paired first paint is blank, passive work reconnects after manual disconnect, a capable hidden paired manager survives ordinary parking, a legacy hidden pane schedules raw xterm output or bypasses its fallback, reveal loses requested bounded history or live PTY identity, retained buffer cells do not fall, or either headed/headless scenario flakes."
"demotionRule": "Keep experimental or demote to protection none if decorative title frequency changes paired client-event or session-tabs publication volume after convergence, a semantic fact is dropped, paired first paint is blank, passive work reconnects after manual disconnect, a capable hidden paired manager survives ordinary parking, a legacy hidden pane schedules raw xterm output or bypasses its fallback, reveal loses requested bounded history or live PTY identity, retained buffer cells do not fall, or either headed/headless scenario flakes."
},
{
"id": "terminal-performance.no-hot-list-sessions",
@@ -0,0 +1,192 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
createMobileSessionTabsAgentStatusHeartbeat,
SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS,
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
} from './mobile-session-tabs-agent-status-heartbeat'
describe('mobile session-tabs agent-status heartbeat', () => {
const worktreesByPtyId = new Map<string, string[]>()
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(0)
worktreesByPtyId.clear()
})
afterEach(() => {
vi.useRealTimers()
})
it('refreshes continuously active PTYs through one globally capped FIFO', () => {
const emitted: { worktreeId: string; at: number }[] = []
const heartbeat = createMobileSessionTabsAgentStatusHeartbeat(
(ptyId) => worktreesByPtyId.get(ptyId) ?? [],
(worktreeId) => emitted.push({ worktreeId, at: Date.now() })
)
const ptyIds = Array.from({ length: 24 }, (_, index) => `pty-${index}`)
for (const ptyId of ptyIds) {
worktreesByPtyId.set(ptyId, [ptyId.replace('pty', 'worktree')])
heartbeat.observeSemanticTitle(ptyId)
}
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS - 1)
for (const ptyId of ptyIds) {
heartbeat.scheduleDecorativeHeartbeat(ptyId)
}
vi.runOnlyPendingTimers()
expect(emitted).toEqual([])
vi.advanceTimersByTime(1)
for (const ptyId of ptyIds) {
heartbeat.scheduleDecorativeHeartbeat(ptyId)
}
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS * (ptyIds.length - 1))
expect(emitted.map(({ worktreeId }) => worktreeId)).toEqual(
ptyIds.map((ptyId) => ptyId.replace('pty', 'worktree'))
)
for (let index = 1; index < emitted.length; index += 1) {
expect(emitted[index]!.at - emitted[index - 1]!.at).toBeGreaterThanOrEqual(
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
)
}
for (const ptyId of ptyIds) {
heartbeat.scheduleDecorativeHeartbeat(ptyId)
}
vi.runOnlyPendingTimers()
expect(emitted).toHaveLength(ptyIds.length)
heartbeat.dispose()
expect(vi.getTimerCount()).toBe(0)
})
it('lets semantic transitions bypass and cancel queued heartbeats', () => {
const emitted: string[] = []
worktreesByPtyId.set('pty-1', ['worktree-1'])
worktreesByPtyId.set('pty-2', ['worktree-2'])
const heartbeat = createMobileSessionTabsAgentStatusHeartbeat(
(ptyId) => worktreesByPtyId.get(ptyId) ?? [],
(worktreeId) => emitted.push(worktreeId)
)
heartbeat.observeSemanticTitle('pty-1')
heartbeat.observeSemanticTitle('pty-2')
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
heartbeat.scheduleDecorativeHeartbeat('pty-2')
heartbeat.observeSemanticTitle('pty-2')
vi.runAllTimers()
expect(emitted).toEqual(['worktree-1'])
heartbeat.removePty('pty-1')
heartbeat.removePty('pty-2')
expect(vi.getTimerCount()).toBe(0)
})
it('deduplicates sibling PTYs because one snapshot refreshes their worktree', () => {
const emitted: string[] = []
worktreesByPtyId.set('pty-1', ['worktree-1'])
worktreesByPtyId.set('pty-2', ['worktree-1'])
const heartbeat = createMobileSessionTabsAgentStatusHeartbeat(
(ptyId) => worktreesByPtyId.get(ptyId) ?? [],
(worktreeId) => emitted.push(worktreeId)
)
heartbeat.observeSemanticTitle('pty-1')
heartbeat.observeSemanticTitle('pty-2')
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
heartbeat.scheduleDecorativeHeartbeat('pty-2')
heartbeat.removePty('pty-1')
vi.runAllTimers()
expect(emitted).toEqual(['worktree-1'])
})
it('does not slip spacing-delayed worktrees to the next stale boundary', () => {
const emitted: { worktreeId: string; at: number }[] = []
worktreesByPtyId.set('pty-1', ['worktree-1'])
worktreesByPtyId.set('pty-2', ['worktree-2'])
const heartbeat = createMobileSessionTabsAgentStatusHeartbeat(
(ptyId) => worktreesByPtyId.get(ptyId) ?? [],
(worktreeId) => emitted.push({ worktreeId, at: Date.now() })
)
heartbeat.observeSemanticTitle('pty-1')
heartbeat.observeSemanticTitle('pty-2')
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
heartbeat.scheduleDecorativeHeartbeat('pty-2')
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS)
expect(emitted).toEqual([
{ worktreeId: 'worktree-1', at: SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS },
{
worktreeId: 'worktree-2',
at:
SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS +
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
}
])
vi.advanceTimersByTime(
SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS -
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
heartbeat.scheduleDecorativeHeartbeat('pty-2')
vi.runOnlyPendingTimers()
expect(emitted.at(-1)).toEqual({
worktreeId: 'worktree-1',
at: SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS * 2
})
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS - 1)
heartbeat.scheduleDecorativeHeartbeat('pty-2')
vi.runOnlyPendingTimers()
expect(emitted).toHaveLength(3)
vi.advanceTimersByTime(1)
heartbeat.scheduleDecorativeHeartbeat('pty-2')
vi.runOnlyPendingTimers()
expect(emitted.at(-1)).toEqual({
worktreeId: 'worktree-2',
at:
SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS * 2 +
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
})
})
it('drops removed worktrees and pending work during teardown', () => {
const emitted: string[] = []
worktreesByPtyId.set('pty-1', ['worktree-1'])
worktreesByPtyId.set('pty-2', ['worktree-2'])
const heartbeat = createMobileSessionTabsAgentStatusHeartbeat(
(ptyId) => worktreesByPtyId.get(ptyId) ?? [],
(worktreeId) => emitted.push(worktreeId)
)
heartbeat.observeSemanticTitle('pty-1')
heartbeat.observeSemanticTitle('pty-2')
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
expect(vi.getTimerCount()).toBe(1)
heartbeat.removePty('pty-1')
vi.runAllTimers()
expect(emitted).toEqual([])
expect(vi.getTimerCount()).toBe(0)
heartbeat.scheduleDecorativeHeartbeat('pty-2')
expect(vi.getTimerCount()).toBe(1)
heartbeat.removeWorktree('worktree-2')
expect(vi.getTimerCount()).toBe(0)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
expect(vi.getTimerCount()).toBe(1)
heartbeat.cancelPending()
expect(vi.getTimerCount()).toBe(0)
heartbeat.scheduleDecorativeHeartbeat('pty-1')
heartbeat.dispose()
vi.runAllTimers()
expect(emitted).toEqual([])
expect(vi.getTimerCount()).toBe(0)
})
})
@@ -0,0 +1,133 @@
import { AGENT_STATUS_STALE_AFTER_MS } from '../../shared/agent-status-types'
// Refresh halfway through the stale lease so a bounded global queue has ample drain time.
export const SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS = AGENT_STATUS_STALE_AFTER_MS / 2
export const SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS = 50
export type MobileSessionTabsAgentStatusHeartbeat = {
observeSemanticTitle: (ptyId: string) => void
scheduleDecorativeHeartbeat: (ptyId: string) => void
removePty: (ptyId: string) => void
removeWorktree: (worktreeId: string) => void
cancelPending: () => void
dispose: () => void
}
export function createMobileSessionTabsAgentStatusHeartbeat(
resolveWorktreeIds: (ptyId: string) => Iterable<string>,
emit: (worktreeId: string) => void
): MobileSessionTabsAgentStatusHeartbeat {
const lastEligibilityCheckAtByPtyId = new Map<string, number>()
const lastRefreshAtByWorktreeId = new Map<string, number>()
const pendingPtyIdsByWorktreeId = new Map<string, Set<string>>()
let lastGlobalHeartbeatAt: number | null = null
let timer: ReturnType<typeof setTimeout> | null = null
const clearTimer = (): void => {
if (timer !== null) {
clearTimeout(timer)
timer = null
}
}
const arm = (): void => {
if (timer !== null || pendingPtyIdsByWorktreeId.size === 0) {
return
}
const now = Date.now()
const delay =
lastGlobalHeartbeatAt === null
? 0
: Math.max(
0,
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS - (now - lastGlobalHeartbeatAt)
)
timer = setTimeout(() => {
timer = null
const worktreeId = pendingPtyIdsByWorktreeId.keys().next().value
if (typeof worktreeId !== 'string') {
return
}
const pendingPtyIds = pendingPtyIdsByWorktreeId.get(worktreeId)
pendingPtyIdsByWorktreeId.delete(worktreeId)
const emittedAt = Date.now()
lastRefreshAtByWorktreeId.set(worktreeId, emittedAt)
for (const ptyId of pendingPtyIds ?? []) {
lastEligibilityCheckAtByPtyId.set(ptyId, emittedAt)
}
lastGlobalHeartbeatAt = emittedAt
emit(worktreeId)
arm()
}, delay)
if (typeof timer.unref === 'function') {
timer.unref()
}
}
return {
observeSemanticTitle(ptyId: string): void {
const observedAt = Date.now()
lastEligibilityCheckAtByPtyId.set(ptyId, observedAt)
for (const worktreeId of resolveWorktreeIds(ptyId)) {
lastRefreshAtByWorktreeId.set(worktreeId, observedAt)
pendingPtyIdsByWorktreeId.delete(worktreeId)
}
if (pendingPtyIdsByWorktreeId.size === 0) {
clearTimer()
}
},
scheduleDecorativeHeartbeat(ptyId: string): void {
const now = Date.now()
const lastEligibilityCheckAt = lastEligibilityCheckAtByPtyId.get(ptyId)
if (
lastEligibilityCheckAt !== undefined &&
now - lastEligibilityCheckAt < SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS
) {
return
}
lastEligibilityCheckAtByPtyId.set(ptyId, now)
for (const worktreeId of resolveWorktreeIds(ptyId)) {
const lastRefreshAt = lastRefreshAtByWorktreeId.get(worktreeId)
if (
lastRefreshAt === undefined ||
now - lastRefreshAt >= SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS
) {
const pendingPtyIds = pendingPtyIdsByWorktreeId.get(worktreeId) ?? new Set<string>()
pendingPtyIds.add(ptyId)
pendingPtyIdsByWorktreeId.set(worktreeId, pendingPtyIds)
}
}
arm()
},
removePty(ptyId: string): void {
lastEligibilityCheckAtByPtyId.delete(ptyId)
for (const [worktreeId, pendingPtyIds] of pendingPtyIdsByWorktreeId) {
pendingPtyIds.delete(ptyId)
if (pendingPtyIds.size === 0) {
pendingPtyIdsByWorktreeId.delete(worktreeId)
}
}
if (pendingPtyIdsByWorktreeId.size === 0) {
clearTimer()
}
},
removeWorktree(worktreeId: string): void {
lastRefreshAtByWorktreeId.delete(worktreeId)
pendingPtyIdsByWorktreeId.delete(worktreeId)
if (pendingPtyIdsByWorktreeId.size === 0) {
clearTimer()
}
},
cancelPending(): void {
clearTimer()
pendingPtyIdsByWorktreeId.clear()
},
dispose(): void {
clearTimer()
pendingPtyIdsByWorktreeId.clear()
lastEligibilityCheckAtByPtyId.clear()
lastRefreshAtByWorktreeId.clear()
lastGlobalHeartbeatAt = null
}
}
}
@@ -230,6 +230,65 @@ describe('headless hook agent-status projection (#11761)', () => {
)
})
it('does not carry a shell-obscured question into the next working interval', async () => {
const rows = [hookRow()]
const runtime = await createRuntimeWithHookRows(rows)
const questionAt = rows[0]!.receivedAt
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(questionAt + 1)
try {
const initial = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
const initialTab = initial.tabs[0]
if (initialTab?.type !== 'terminal' || !initialTab.terminal) {
throw new Error('expected a live terminal handle')
}
await runtime.renameTerminal(initialTab.terminal, 'bash')
observePaneTitle(runtime, 'bash')
let result = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
let tab = result.tabs[0]
expect(tab?.type === 'terminal' && tab.agentStatus).toEqual(
expect.objectContaining({ state: 'waiting', interactivePrompt: ASK_PROMPT })
)
dateNow.mockReturnValue(questionAt + 2)
observePaneTitle(runtime, '⠋ Claude')
result = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
tab = result.tabs[0]
expect(tab?.type === 'terminal' && tab.agentStatus).toEqual(
expect.objectContaining({ state: 'working', prompt: '' })
)
expect(tab?.type === 'terminal' && tab.agentStatus).not.toHaveProperty('interactivePrompt')
} finally {
dateNow.mockRestore()
}
})
it('does not carry a hook question across a provider generation reset', async () => {
const runtime = await createRuntimeWithHookRows([hookRow()])
const internals = runtime as unknown as {
ptysById: Map<string, { title: string | null }>
resetTrackedTerminalStateForProviderGeneration: (ptyId: string) => void
}
internals.ptysById.get(PTY_ID)!.title = 'bash'
internals.resetTrackedTerminalStateForProviderGeneration(PTY_ID)
const result = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
const tab = result.tabs[0]
expect(tab?.type === 'terminal' && tab.agentStatus).not.toHaveProperty('interactivePrompt')
})
it('does not carry a hook question across an identity-only owner title', async () => {
const runtime = await createRuntimeWithHookRows([hookRow()])
const internals = runtime as unknown as {
ptysById: Map<string, { title: string | null }>
}
internals.ptysById.get(PTY_ID)!.title = 'bash'
observePaneTitle(runtime, 'Cursor Agent')
const result = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
const tab = result.tabs[0]
expect(tab?.type === 'terminal' && tab.agentStatus).not.toHaveProperty('interactivePrompt')
})
// The title path is refreshed live; an older hook `done` must not erase it.
it('keeps the title-derived working state when the hook row predates the title', async () => {
const now = Date.now()
+10 -6
View File
@@ -24549,7 +24549,7 @@ describe('OrcaRuntimeService', () => {
unsubscribe()
})
it('keeps same-status Pi-compatible title changes queued behind the foreground owner probe', async () => {
it('keeps decorative Pi frames queued behind the foreground owner probe', async () => {
const foregroundProcess = deferred<string | null>()
const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-omp' })
const getForegroundProcess = vi.fn(() => foregroundProcess.promise)
@@ -24568,9 +24568,9 @@ describe('OrcaRuntimeService', () => {
const events: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot))
runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi ready\x07', 123)
runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi\x07', 123)
await new Promise<void>((resolve) => setImmediate(resolve))
runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi idle\x07', 124)
runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi\x07', 124)
await new Promise<void>((resolve) => setImmediate(resolve))
expect(getForegroundProcess).toHaveBeenCalledTimes(1)
@@ -24585,18 +24585,22 @@ describe('OrcaRuntimeService', () => {
tabs: [
expect.objectContaining({
type: 'terminal',
title: 'OMP ready',
title: 'OMP',
agentStatus: expect.objectContaining({
state: 'done',
state: 'working',
agentType: 'omp',
terminalHandle: terminal.handle,
terminalTitle: 'OMP ready'
terminalTitle: 'OMP'
})
})
]
})
])
runtime.onPtyData('pty-typed-omp', '\x1b]0;⠹ Pi\x07', 125)
await new Promise<void>((resolve) => setImmediate(resolve))
expect(events).toHaveLength(1)
unsubscribe()
})
+210 -54
View File
@@ -27,10 +27,10 @@ import type {
} from './remote-terminal-source-range-consumer'
import {
createTerminalTitleTracker,
stripBrailleSpinnerGlyphs,
type TerminalTitleFactMeta,
type TerminalTitleTracker
} from '../../shared/terminal-output-side-effects'
import { getDecorativeAgentTitleSignature } from '../../shared/agent-decorative-title-signature'
import { createCommandCodeOutputStatusDetector } from '../../shared/command-code-output-status'
import type {
TerminalSideEffectBatch,
@@ -981,6 +981,10 @@ import {
createMobileSessionTabsNotifyCoalescer,
type MobileSessionTabsNotifyCoalescer
} from './mobile-session-tabs-notify-coalescer'
import {
createMobileSessionTabsAgentStatusHeartbeat,
type MobileSessionTabsAgentStatusHeartbeat
} from './mobile-session-tabs-agent-status-heartbeat'
import { TerminalFocusNavigationCoalescer } from './terminal-focus-navigation-coalescer'
import {
appendRecentPtyPathCandidates,
@@ -1300,6 +1304,9 @@ type RuntimePtyWorktreeRecord = {
lastAgentStatus: AgentStatus | null
/** False until a live OSC frame sets the status; restore seeds never set it. */
lastAgentStatusObservedLive: boolean
lastAgentStatusStartedAtEpochMs: number | null
// A later semantic title interval cannot inherit rich fields from an earlier task.
lastAgentStatusRichInvalidatedAtEpochMs: number | null
lastOscTitle: string | null
lastOscTitleAt: number | null
// Why a second stamp: `lastOscTitleAt` is a title-observation sequence number,
@@ -1509,12 +1516,8 @@ type RuntimePtyTitleTrackerEntry = {
tracker: TerminalTitleTracker
// Why: onPtyData batches the mobile session-tab touch to once per chunk;
// the stale-working-title timer fires between chunks and must touch
// immediately. These flags route the tracker callback to the right mode.
// immediately. This flag routes the tracker callback to the right mode.
applyingChunk: boolean
// Why: synthetic spinner ticks arrive ~12.5x/sec per working pane; the
// synthetic path gates mobile snapshot fan-out on a non-decorative title
// change (spinner glyph + status comparison key kept below).
applyingSyntheticFrame: boolean
lastMobileTitleGateKey: string | null
chunkTouchedSessionTabs: boolean
// Why: facts observed while applying a chunk are batched into one
@@ -2746,6 +2749,11 @@ export class OrcaRuntimeService {
createMobileSessionTabsNotifyCoalescer((worktreeId) =>
this.notifyMobileSessionTabsChangedNow(worktreeId)
)
private readonly mobileSessionTabsAgentStatusHeartbeat: MobileSessionTabsAgentStatusHeartbeat =
createMobileSessionTabsAgentStatusHeartbeat(
(ptyId) => this.getMobileSessionWorktreeIdsForPty(ptyId),
(worktreeId) => this.touchMobileSessionTabsForWorktree(worktreeId)
)
// Why: concurrent host terminal.focus storms (CLI switch fan-out / bulk open)
// each await a full host reveal; only one terminal can be focused, so latest-wins
// single-flight bounds host work. Does not replace cheaper activation or
@@ -6534,6 +6542,21 @@ export class OrcaRuntimeService {
}
}
private getMobileSessionWorktreeIdsForPty(ptyId: string): string[] {
const worktreeIds: string[] = []
for (const [worktreeId, snapshot] of this.mobileSessionTabsByWorktree) {
const hasPtyBackedTab = snapshot.tabs.some(
(tab) =>
tab.type === 'terminal' &&
(tab.ptyId === ptyId || tab.parentLayout?.ptyIdsByLeafId?.[tab.leafId] === ptyId)
)
if (hasPtyBackedTab) {
worktreeIds.push(worktreeId)
}
}
return worktreeIds
}
/** Bump the snapshot version and emit, coalesced unless `immediate`.
* Why the bump: clients gate mirrored snapshots on a strictly increasing
* `snapshotVersion`, so a re-emit at the same version is silently dropped. */
@@ -9081,6 +9104,9 @@ export class OrcaRuntimeService {
// subscriber closing mid-window still receives the latest settled state.
this.mobileSessionTabsNotifyCoalescer.flushAll()
this.mobileSessionTabListeners.delete(subscription)
if (this.mobileSessionTabListeners.size === 0) {
this.mobileSessionTabsAgentStatusHeartbeat.cancelPending()
}
}
}
@@ -9867,13 +9893,11 @@ export class OrcaRuntimeService {
ingestSyntheticTitleFrame(ptyId: string, data: string): void {
const entry = this.getOrCreatePtyTitleTrackerEntry(ptyId)
entry.applyingChunk = true
entry.applyingSyntheticFrame = true
entry.chunkTouchedSessionTabs = false
try {
entry.tracker.applySyntheticTitleFrame(data)
} finally {
entry.applyingChunk = false
entry.applyingSyntheticFrame = false
this.flushPendingTerminalSideEffectFacts(ptyId, entry)
}
if (entry.chunkTouchedSessionTabs) {
@@ -10111,13 +10135,13 @@ export class OrcaRuntimeService {
return { ...snapshot, lastTitle: tracked }
}
/** Decorative comparison key: spinner frame glyphs stripped, derived agent
* status kept so a workingidle flip with an otherwise-equal label still
* counts as a change. */
/** Decorative comparison key: only recognized agent titles fold leading spinner frames. */
private makeDecorativeTitleGateKey(rawTitle: string, normalizedTitle: string): string {
return `${detectAgentStatusFromTitle(rawTitle) ?? ''}\u0000${stripBrailleSpinnerGlyphs(
normalizedTitle
)}`
// Stable Pi/Gemini/Grok display normalization also defines their semantic gate.
const normalizedSignature =
rawTitle === normalizedTitle ? null : getDecorativeAgentTitleSignature(normalizedTitle)
const signature = normalizedSignature ?? getDecorativeAgentTitleSignature(rawTitle)
return signature === null ? `literal\u0000${normalizedTitle}` : `agent\u0000${signature}`
}
private getOrCreatePtyTitleTrackerEntry(ptyId: string): RuntimePtyTitleTrackerEntry {
@@ -10148,28 +10172,42 @@ export class OrcaRuntimeService {
...(meta?.staleWorkingTitleClear ? { staleWorkingTitleClear: true } : {})
})
const changed = this.applyTrackedPtyTitle(ptyId, rawTitle, normalizedTitle, meta)
// Why: an identity-only cursor title records nothing, so on a fresh pane `changed` is
// false — but the tracker title is that pane's only Cursor identity and still has to
// fan out to the sidebar and mobile (#10258).
if (!changed && !this.isLiveCursorNativeTitle(rawTitle, meta)) {
return
}
const identityOnlyTitle = this.isLiveCursorNativeTitle(rawTitle, meta)
const live = this.ptyTitleTrackersByPtyId.get(ptyId)
const gateKey = this.makeDecorativeTitleGateKey(rawTitle, normalizedTitle)
const decorativeOnly = live?.lastMobileTitleGateKey === gateKey
if (live) {
live.lastMobileTitleGateKey = gateKey
}
const tracksReplicatedStatus =
live?.applyingChunk === true && this.mobileSessionTabListeners.size > 0
const titleStatus = tracksReplicatedStatus ? detectAgentStatusFromTitle(rawTitle) : null
if (
tracksReplicatedStatus &&
decorativeOnly &&
!this.ptyDelayedForegroundSnapshotTitleObservations.has(ptyId) &&
(titleStatus === 'working' || titleStatus === 'permission')
) {
// Normalized Pi/Gemini/Grok frames still renew the replicated status lease.
this.mobileSessionTabsAgentStatusHeartbeat.scheduleDecorativeHeartbeat(ptyId)
}
// Why: an identity-only cursor title records nothing, but the tracker
// title is that pane's only Cursor identity and must still fan out (#10258).
if (!changed && !identityOnlyTitle) {
return
}
if (live?.applyingChunk) {
// Why: synthetic spinner ticks change only the braille glyph
// ~12.5x/sec; fanning out full mobile session snapshots per frame
// is pure churn. Raw lastOscTitle updates above stay cheap.
if (!(live.applyingSyntheticFrame && decorativeOnly)) {
if (!decorativeOnly) {
this.mobileSessionTabsAgentStatusHeartbeat.observeSemanticTitle(ptyId)
live.chunkTouchedSessionTabs = true
}
} else {
// Stale-working-title timer path — fires between chunks, so the
// per-chunk batching in onPtyData cannot pick it up.
this.mobileSessionTabsAgentStatusHeartbeat.observeSemanticTitle(ptyId)
this.touchMobileSessionSnapshotsForPty(ptyId)
}
},
@@ -10213,7 +10251,6 @@ export class OrcaRuntimeService {
const entry: RuntimePtyTitleTrackerEntry = {
tracker,
applyingChunk: false,
applyingSyntheticFrame: false,
lastMobileTitleGateKey: null,
chunkTouchedSessionTabs: false,
pendingFacts: [],
@@ -10255,11 +10292,22 @@ export class OrcaRuntimeService {
const prevStatus = pty.lastAgentStatus
const prevTitle = pty.lastOscTitle
const observedAt = this.nextTitleObservationSequence()
const observedAtEpochMs = identityOnlyTitle ? null : Date.now()
pty.lastOscTitle = recordedTitle
pty.lastOscTitleAt = identityOnlyTitle ? null : observedAt
pty.lastOscTitleEpochMs = identityOnlyTitle ? null : Date.now()
pty.lastOscTitleEpochMs = observedAtEpochMs
pty.lastAgentStatus = agentStatus
pty.lastAgentStatusObservedLive = true
if (prevStatus !== agentStatus) {
pty.lastAgentStatusStartedAtEpochMs = observedAtEpochMs
}
if (
identityOnlyTitle ||
terminalTitleBlocksExplicitAgentStatus(recordedTitle) ||
(prevStatus !== null && agentStatus !== null && prevStatus !== agentStatus)
) {
pty.lastAgentStatusRichInvalidatedAtEpochMs = observedAtEpochMs ?? Date.now()
}
if (identityOnlyTitle) {
pty.managementTitle = null
pty.managementTitleAt = null
@@ -10339,6 +10387,8 @@ export class OrcaRuntimeService {
private disposePtyTitleTracker(ptyId: string): void {
this.ptyTitleTrackersByPtyId.get(ptyId)?.tracker.dispose()
this.ptyTitleTrackersByPtyId.delete(ptyId)
this.ptyDelayedForegroundSnapshotTitleObservations.delete(ptyId)
this.mobileSessionTabsAgentStatusHeartbeat.removePty(ptyId)
for (const titleGateKeys of this.terminalSideEffectTitleGateKeysByClientEventListener.values()) {
titleGateKeys.delete(ptyId)
}
@@ -10360,6 +10410,8 @@ export class OrcaRuntimeService {
// Why: the prior process's live frames say nothing about the replacement,
// so the seed a same-id restore applies must not inherit its authority.
pty.lastAgentStatusObservedLive = false
pty.lastAgentStatusStartedAtEpochMs = null
pty.lastAgentStatusRichInvalidatedAtEpochMs = Date.now()
pty.managementTitle = null
pty.managementTitleAt = null
}
@@ -16916,6 +16968,9 @@ export class OrcaRuntimeService {
return
}
this.ptyDelayedForegroundSnapshotTitleObservations.delete(ptyId)
if (this.mobileSessionTabListeners.size > 0) {
this.mobileSessionTabsAgentStatusHeartbeat.observeSemanticTitle(ptyId)
}
if (!foregroundAgentChanged) {
this.touchMobileSessionSnapshotsForPty(ptyId)
}
@@ -23975,6 +24030,7 @@ export class OrcaRuntimeService {
store.removeWorktreeMeta(worktreeId)
}
this.mobileSessionTabsByWorktree.delete(worktreeId)
this.mobileSessionTabsAgentStatusHeartbeat.removeWorktree(worktreeId)
this.acceptedRendererMobileSnapshotByWorktree.delete(worktreeId)
advertisedUrlWatcher.forgetWorktree(worktreeId)
deleteWorktreeHistoryDir(worktreeId)
@@ -29012,6 +29068,8 @@ export class OrcaRuntimeService {
lastExitCode: null,
lastAgentStatus: null,
lastAgentStatusObservedLive: false,
lastAgentStatusStartedAtEpochMs: null,
lastAgentStatusRichInvalidatedAtEpochMs: null,
lastOscTitle: null,
lastOscTitleAt: null,
lastOscTitleEpochMs: null,
@@ -29763,6 +29821,7 @@ export class OrcaRuntimeService {
nextWorktrees.add(worktreeId)
} else {
this.mobileSessionTabsByWorktree.delete(worktreeId)
this.mobileSessionTabsAgentStatusHeartbeat.removeWorktree(worktreeId)
this.acceptedRendererMobileSnapshotByWorktree.delete(worktreeId)
// Why: drop any pending coalesced notify so a stale snapshot can't land after the removed frame.
this.mobileSessionTabsNotifyCoalescer.cancel(worktreeId)
@@ -30299,16 +30358,21 @@ export class OrcaRuntimeService {
(hookAgentStatus.providerSessionReceivedAt ?? -1) >= tab.agentStatus.updatedAt)
? hookAgentStatus.providerSession
: tab.agentStatus?.providerSession
const normalizedTabAgentStatus = tab.agentStatus
? normalizeCompatibleAgentStatusEntryForOwner(
{
...tab.agentStatus,
...(hookProviderSession ? { providerSession: hookProviderSession } : {})
},
ownerAgent
)
: null
// Why: keep rich hook status on a live prompt/tool (authoritative even under a non-agent title), else interactivePrompt is lost.
const statusPty = liveLeafPty ?? mobileStatusPty
const normalizedTabAgentStatus = this.renewMobileAgentStatusFromPtyTitle(
tab.agentStatus
? normalizeCompatibleAgentStatusEntryForOwner(
{
...tab.agentStatus,
...(hookProviderSession ? { providerSession: hookProviderSession } : {})
},
ownerAgent
)
: null,
statusPty,
{ preserveQuestionUnderShellTitle: true }
)
// Why: keep rich status on a live prompt/tool, or interactivePrompt is lost under a non-agent title.
const hasLiveAgentSignal =
normalizedTabAgentStatus?.interactivePrompt != null ||
normalizedTabAgentStatus?.toolName != null
@@ -30328,8 +30392,10 @@ export class OrcaRuntimeService {
agentStatus: {
state: 'done' as const,
prompt: '',
updatedAt: normalizedTabAgentStatus.updatedAt,
stateStartedAt: normalizedTabAgentStatus.stateStartedAt,
updatedAt: statusPty?.lastOscTitleEpochMs ?? normalizedTabAgentStatus.updatedAt,
stateStartedAt:
statusPty?.lastAgentStatusStartedAtEpochMs ??
normalizedTabAgentStatus.stateStartedAt,
paneKey: normalizedTabAgentStatus.paneKey,
stateHistory: [],
agentType: normalizedTabAgentStatus.agentType,
@@ -30422,6 +30488,91 @@ export class OrcaRuntimeService {
}
}
private renewMobileAgentStatusFromPtyTitle(
status: AgentStatusEntry | null,
pty: RuntimePtyWorktreeRecord | null,
options: { preserveQuestionUnderShellTitle?: boolean } = {}
): AgentStatusEntry | null {
if (!status || !pty) {
return status
}
if (
options.preserveQuestionUnderShellTitle &&
status.interactivePrompt != null &&
terminalTitleBlocksExplicitAgentStatus(pty.lastOscTitle)
) {
return status
}
const richStatusCanOwnTitleInterval =
pty.lastAgentStatusRichInvalidatedAtEpochMs === null ||
status.updatedAt > pty.lastAgentStatusRichInvalidatedAtEpochMs
const titleEvidenceAt = pty.lastOscTitleEpochMs
if (titleEvidenceAt === null) {
return richStatusCanOwnTitleInterval ? status : null
}
const buildTitleOnlyStatus = (
state: AgentStatusEntry['state'],
updatedAt: number,
stateStartedAt: number
): AgentStatusEntry => ({
state,
prompt: '',
updatedAt,
stateStartedAt,
paneKey: status.paneKey,
stateHistory: [],
...(status.agentType ? { agentType: status.agentType } : {}),
...(status.terminalHandle ? { terminalHandle: status.terminalHandle } : {}),
...(status.worktreeId ? { worktreeId: status.worktreeId } : {}),
...(status.tabId ? { tabId: status.tabId } : {}),
...(status.terminalTitle ? { terminalTitle: status.terminalTitle } : {}),
...(status.providerSession ? { providerSession: status.providerSession } : {})
})
const titleConfirmsState =
(pty.lastAgentStatus === 'working' && status.state === 'working') ||
(pty.lastAgentStatus === 'permission' &&
(status.state === 'blocked' || status.state === 'waiting'))
if (!titleConfirmsState) {
if (richStatusCanOwnTitleInterval && status.updatedAt >= titleEvidenceAt) {
return status
}
if (
pty.lastAgentStatus === null &&
!terminalTitleBlocksExplicitAgentStatus(pty.lastOscTitle)
) {
return status
}
const titleState =
pty.lastAgentStatus === 'working'
? 'working'
: pty.lastAgentStatus === 'permission'
? 'blocked'
: 'done'
return buildTitleOnlyStatus(
titleState,
titleEvidenceAt,
pty.lastAgentStatusStartedAtEpochMs ?? titleEvidenceAt
)
}
const richStatusIsFresh = Date.now() - status.updatedAt <= AGENT_STATUS_STALE_AFTER_MS
const richStatusOwnsCurrentState = richStatusIsFresh && richStatusCanOwnTitleInterval
// Fresh explicit evidence from this title interval owns acknowledgement identity.
const stateStartedAt = richStatusOwnsCurrentState
? status.stateStartedAt
: (pty.lastAgentStatusStartedAtEpochMs ?? status.stateStartedAt)
if (richStatusOwnsCurrentState) {
pty.lastAgentStatusStartedAtEpochMs = stateStartedAt
}
const updatedAt = Math.max(status.updatedAt, titleEvidenceAt)
if (!richStatusOwnsCurrentState) {
return buildTitleOnlyStatus(status.state, updatedAt, stateStartedAt)
}
if (updatedAt === status.updatedAt && stateStartedAt === status.stateStartedAt) {
return status
}
return { ...status, updatedAt, stateStartedAt }
}
/** Mobile-friendly status entry for a PTY, aligning agentType and titles with the active owner. */
private buildPtyMobileAgentStatus(
pty: RuntimePtyWorktreeRecord | null,
@@ -30496,24 +30647,29 @@ export class OrcaRuntimeService {
// Why: OSC 9999 hook payload carries real state/prompt/agent; without preferring it, hook-only transitions never surfaced (#7970).
const liveRow = retained ?? this.resolveHookLiveAgentRow(hookRow.live, pty, nonAgentTitle)
if (liveRow) {
return {
agentStatus: normalizeCompatibleAgentStatusEntryForOwner(
{
...liveRow.payload,
paneKey,
updatedAt: liveRow.updatedAt,
stateStartedAt: liveRow.stateStartedAt,
stateHistory: [],
...(terminalHandle ? { terminalHandle } : {}),
...((pty?.worktreeId ?? liveRow.worktreeId)
? { worktreeId: pty?.worktreeId ?? liveRow.worktreeId }
: {}),
tabId: tab.parentTabId,
terminalTitle,
...providerSession
},
ownerAgent
)
const liveStatus = normalizeCompatibleAgentStatusEntryForOwner(
{
...liveRow.payload,
paneKey,
updatedAt: liveRow.updatedAt,
stateStartedAt: liveRow.stateStartedAt,
stateHistory: [],
...(terminalHandle ? { terminalHandle } : {}),
...((pty?.worktreeId ?? liveRow.worktreeId)
? { worktreeId: pty?.worktreeId ?? liveRow.worktreeId }
: {}),
tabId: tab.parentTabId,
terminalTitle,
...providerSession
},
ownerAgent
)
// A live question outranks only the shell title that currently obscures it.
const renewedStatus = this.renewMobileAgentStatusFromPtyTitle(liveStatus, pty, {
preserveQuestionUnderShellTitle: true
})
if (renewedStatus) {
return { agentStatus: renewedStatus }
}
}
// Last resort: the pane's hook evidence is identity only (resume rows, stale
@@ -30534,7 +30690,7 @@ export class OrcaRuntimeService {
: 'done',
prompt: '',
updatedAt: evidenceAt,
stateStartedAt: evidenceAt,
stateStartedAt: pty?.lastAgentStatusStartedAtEpochMs ?? evidenceAt,
paneKey,
...(terminalHandle ? { terminalHandle } : {}),
...(agentType ? { agentType } : {}),
@@ -0,0 +1,342 @@
/** @vitest-environment happy-dom */
import { act, StrictMode } from 'react'
import type * as ReactModule from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const harness = vi.hoisted(() => ({
worktreeId: 'repo::/cold-park-pre-gate',
discardMemoCaches: false,
coverageCalls: 0,
syncCalls: 0,
slotRenders: 0,
renderedParkedSets: [] as string[][]
}))
vi.mock('react', async () => {
const actual = await vi.importActual<typeof ReactModule>('react')
return {
...actual,
useMemo: ((calculate, dependencies) =>
harness.discardMemoCaches
? calculate()
: actual.useMemo(calculate, dependencies)) as typeof actual.useMemo
}
})
vi.mock('../../store', async () => {
const { create } = await import('zustand')
const useAppStore = create(() => ({
activeGroupIdByWorktree: {} as Record<string, string | undefined>,
groupsByWorktree: {} as Record<string, TabGroup[]>,
pendingStartupByTabId: {} as Record<string, unknown>,
ptyIdsByTabId: {} as Record<string, string[]>,
runtimeStatusByEnvironmentId: new Map<string, unknown>(),
runtimePaneTitlesByTabId: {} as Record<string, Record<number, string>>,
settings: {} as Record<string, unknown>,
sleepingAgentSessionsByPaneKey: {} as Record<string, unknown>,
tabsByWorktree: {} as Record<string, TerminalTab[]>,
terminalLayoutsByTabId: {} as Record<string, unknown>,
unifiedTabsByWorktree: {} as Record<string, Tab[]>,
consumeSuppressedPtyExit: () => false,
focusGroup: () => {},
reconcileWorktreeTabModel: () => ({ renderableTabCount: 2 }),
setActiveWorktree: () => {}
}))
return { useAppStore }
})
vi.mock('../native-chat/use-native-chat-toggle-shortcut', () => ({
useNativeChatToggleShortcut: () => {}
}))
vi.mock('./TerminalOverlaySlot', () => ({
TerminalOverlaySlot: () => {
harness.slotRenders += 1
return null
}
}))
vi.mock('./terminal-parked-tab-watchers', async () => {
const { useAppStore } = await import('../../store')
return {
canWatcherCoverParkedTerminalTab: () => {
harness.coverageCalls += 1
return harness.coverageCalls % 2 === 1
},
disposeParkedTerminalWatchersForWorktree: () => {},
syncParkedTerminalTabWatchers: (args: {
worktreeId: string
parkedTabIds: ReadonlySet<string>
}) => {
harness.syncCalls += 1
harness.renderedParkedSets.push([...args.parkedTabIds].sort())
if (args.parkedTabIds.size === 0) {
return
}
;(
useAppStore as unknown as {
setState: (update: (state: ParkingStoreState) => Partial<ParkingStoreState>) => void
}
).setState((state) => ({
tabsByWorktree: {
...state.tabsByWorktree,
[args.worktreeId]: state.tabsByWorktree[args.worktreeId].map((tab) => ({
...tab,
title: `${tab.id}-${harness.syncCalls}`
}))
},
unifiedTabsByWorktree: {
...state.unifiedTabsByWorktree,
[args.worktreeId]: state.unifiedTabsByWorktree[args.worktreeId].map((tab) => ({
...tab,
label: `${tab.entityId}-${harness.syncCalls}`
}))
}
}))
}
}
})
vi.mock('./terminal-parking-e2e-overrides', () => ({
getTerminalParkingPolicyOverrides: () => ({ coldParkDelayMs: 0, hotRetainMs: 0 })
}))
vi.mock('@/lib/crash-breadcrumb-recorder', () => ({
recordRendererCrashBreadcrumb: vi.fn()
}))
import { useAppStore } from '../../store'
import TerminalPaneOverlayLayer from './TerminalPaneOverlayLayer'
const TAB_IDS = ['tab-a', 'tab-b'] as const
const GROUP_ID = 'group-a'
type ParkingStoreState = {
groupsByWorktree: Record<string, TabGroup[]>
tabsByWorktree: Record<string, TerminalTab[]>
unifiedTabsByWorktree: Record<string, Tab[]>
}
const parkingStore = useAppStore as unknown as {
getState: () => ParkingStoreState
setState: (partial: Record<string, unknown>) => void
}
function terminalTab(id: string): TerminalTab {
return {
id,
worktreeId: harness.worktreeId,
ptyId: `${harness.worktreeId}@@session-${id}`,
title: id,
generation: 0
} as TerminalTab
}
function unifiedTerminalTab(id: string): Tab {
return {
id: `unified-${id}`,
entityId: id,
worktreeId: harness.worktreeId,
groupId: GROUP_ID,
contentType: 'terminal',
label: id,
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
function renderProductionLayer(root: Root, coldParkTerminalPanes = true): unknown {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
let thrown: unknown = null
try {
act(() => {
root.render(
<StrictMode>
<TerminalPaneOverlayLayer
worktreeId={harness.worktreeId}
worktreePath="cold-park-pre-gate"
isWorktreeActive={false}
coldParkTerminalPanes={coldParkTerminalPanes}
/>
</StrictMode>
)
})
} catch (error) {
thrown = error
}
consoleError.mockRestore()
return thrown
}
describe('TerminalPaneOverlayLayer cold-park pre-gate loop', () => {
let container: HTMLDivElement
let root: Root | undefined
beforeEach(() => {
harness.discardMemoCaches = false
harness.coverageCalls = 0
harness.syncCalls = 0
harness.slotRenders = 0
harness.renderedParkedSets.length = 0
const tabs = TAB_IDS.map(terminalTab)
const unifiedTabs = TAB_IDS.map(unifiedTerminalTab)
parkingStore.setState({
activeGroupIdByWorktree: { [harness.worktreeId]: GROUP_ID },
groupsByWorktree: {
[harness.worktreeId]: [
{
id: GROUP_ID,
worktreeId: harness.worktreeId,
activeTabId: unifiedTabs[0].id,
tabOrder: unifiedTabs.map((tab) => tab.id),
recentTabIds: [unifiedTabs[0].id]
}
]
},
tabsByWorktree: { [harness.worktreeId]: tabs },
unifiedTabsByWorktree: { [harness.worktreeId]: unifiedTabs }
})
container = document.createElement('div')
document.body.appendChild(container)
})
afterEach(() => {
try {
act(() => root?.unmount())
} catch {
// A failed initial commit leaves no mounted tree to clean up.
}
root = undefined
container.remove()
})
it('settles title publications without suppressing PTY rechecks', () => {
root = createRoot(container)
const thrown = renderProductionLayer(root)
expect(thrown).toBeNull()
expect(harness.coverageCalls).toBe(2)
expect(harness.syncCalls).toBeLessThan(10)
expect(harness.renderedParkedSets).not.toHaveLength(0)
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
new Set([TAB_IDS.join(',')])
)
expect(harness.slotRenders).toBe(0)
const settledSyncCalls = harness.syncCalls
const state = parkingStore.getState()
act(() => {
parkingStore.setState({
tabsByWorktree: {
...state.tabsByWorktree,
[harness.worktreeId]: state.tabsByWorktree[harness.worktreeId].map((tab) =>
tab.id === TAB_IDS[1] ? { ...tab, ptyId: `${tab.ptyId}-replacement` } : tab
)
}
})
})
expect(harness.coverageCalls).toBe(3)
expect(harness.syncCalls).toBeGreaterThan(settledSyncCalls)
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
new Set([TAB_IDS.join(',')])
)
expect(harness.slotRenders).toBe(0)
const beforePendingSpawnSyncCalls = harness.syncCalls
const pendingSpawnState = parkingStore.getState()
act(() => {
parkingStore.setState({
tabsByWorktree: {
...pendingSpawnState.tabsByWorktree,
[harness.worktreeId]: pendingSpawnState.tabsByWorktree[harness.worktreeId].map((tab) =>
tab.id === TAB_IDS[1] ? { ...tab, pendingActivationSpawn: 1 } : tab
)
}
})
})
expect(harness.syncCalls).toBeGreaterThan(beforePendingSpawnSyncCalls)
const beforeTabOrderSyncCalls = harness.syncCalls
const tabOrderState = parkingStore.getState()
act(() => {
parkingStore.setState({
tabsByWorktree: {
...tabOrderState.tabsByWorktree,
[harness.worktreeId]: tabOrderState.tabsByWorktree[harness.worktreeId].toReversed()
}
})
})
expect(harness.syncCalls).toBeGreaterThan(beforeTabOrderSyncCalls)
const beforeActiveTabChangeSyncCalls = harness.syncCalls
const activeTabState = parkingStore.getState()
act(() => {
parkingStore.setState({
groupsByWorktree: {
...activeTabState.groupsByWorktree,
[harness.worktreeId]: activeTabState.groupsByWorktree[harness.worktreeId].map(
(group) => ({
...group,
activeTabId: `unified-${TAB_IDS[1]}`
})
)
}
})
})
expect(harness.syncCalls).toBeGreaterThan(beforeActiveTabChangeSyncCalls)
const beforeGroupChangeSyncCalls = harness.syncCalls
const groupState = parkingStore.getState()
const nextGroupId = 'group-b'
act(() => {
parkingStore.setState({
groupsByWorktree: {
...groupState.groupsByWorktree,
[harness.worktreeId]: [
...groupState.groupsByWorktree[harness.worktreeId],
{
id: nextGroupId,
worktreeId: harness.worktreeId,
activeTabId: `unified-${TAB_IDS[1]}`,
tabOrder: [`unified-${TAB_IDS[1]}`],
recentTabIds: [`unified-${TAB_IDS[1]}`]
}
]
},
unifiedTabsByWorktree: {
...groupState.unifiedTabsByWorktree,
[harness.worktreeId]: groupState.unifiedTabsByWorktree[harness.worktreeId].map((tab) =>
tab.entityId === TAB_IDS[1] ? { ...tab, groupId: nextGroupId } : tab
)
}
})
})
expect(harness.syncCalls).toBeGreaterThan(beforeGroupChangeSyncCalls)
expect(renderProductionLayer(root, false)).toBeNull()
expect(harness.slotRenders).toBeGreaterThan(0)
expect(renderProductionLayer(root)).toBeNull()
expect(harness.renderedParkedSets.at(-1)).toEqual([...TAB_IDS])
})
it('settles equivalent publications after memo cache discard', () => {
harness.discardMemoCaches = true
root = createRoot(container)
const thrown = renderProductionLayer(root)
expect(thrown).toBeNull()
expect(harness.syncCalls).toBeLessThan(10)
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
new Set([TAB_IDS.join(',')])
)
})
})
@@ -31,6 +31,7 @@ vi.mock('../../store', async () => {
const { create } = await import('zustand')
const useAppStore = create(() => ({
pendingStartupByTabId: {} as Record<string, unknown>,
ptyIdsByTabId: {} as Record<string, string[]>,
runtimeStatusByEnvironmentId: new Map<string, unknown>(),
settings: {} as Record<string, unknown>,
terminalLayoutsByTabId: {} as Record<string, unknown>,
@@ -0,0 +1,251 @@
/** @vitest-environment happy-dom */
import { act, StrictMode, useEffect } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const WORKTREE_ID = 'repo::/parked-watcher-sync'
const TAB_ID = 'tab-1'
const FIRST_LEAF_ID = '11111111-1111-4111-8111-111111111111'
const SECOND_LEAF_ID = '22222222-2222-4222-8222-222222222222'
const FIRST_PTY_ID = `${WORKTREE_ID}@@session-1`
const OLD_SECOND_PTY_ID = `${WORKTREE_ID}@@session-2`
const NEW_SECOND_PTY_ID = `${WORKTREE_ID}@@session-3`
const harness = vi.hoisted(() => ({
syncCalls: 0,
disposeCalls: 0,
reconciliationPtyReads: 0,
watchedPtyIds: new Set<string>()
}))
vi.mock('../../store', async () => {
const { create } = await import('zustand')
const useAppStore = create(() => ({
ptyIdsByTabId: {} as Record<string, string[]>,
runtimePaneTitlesByTabId: {} as Record<string, Record<number, string>>,
terminalLayoutsByTabId: {} as Record<string, TerminalLayoutSnapshot>
}))
return { useAppStore }
})
vi.mock('./terminal-parked-tab-watchers', async () => {
const { useAppStore } = await import('../../store')
return {
disposeParkedTerminalWatchersForWorktree: () => {
harness.disposeCalls += 1
harness.watchedPtyIds.clear()
},
syncParkedTerminalTabWatchers: (args: {
tabs: readonly TerminalTab[]
parkedTabIds: ReadonlySet<string>
}) => {
harness.syncCalls += 1
const state = useAppStore.getState()
harness.watchedPtyIds.clear()
for (const tab of args.tabs) {
if (!args.parkedTabIds.has(tab.id)) {
continue
}
const layoutPtyIds = Object.values(
state.terminalLayoutsByTabId[tab.id]?.ptyIdsByLeafId ?? {}
)
for (const ptyId of layoutPtyIds.length > 0 ? layoutPtyIds : [tab.ptyId]) {
if (ptyId) {
harness.watchedPtyIds.add(ptyId)
}
}
}
}
}
})
import { useAppStore } from '../../store'
import { disposeParkedTerminalWatchersForWorktree } from './terminal-parked-tab-watchers'
import {
captureParkedTerminalPaneCandidates,
capturedPanesByTabId
} from './terminal-parked-watcher-registry'
import { useParkedTerminalWatcherSynchronization } from './use-parked-terminal-watcher-synchronization'
const terminalTabs = [{ id: TAB_ID, ptyId: FIRST_PTY_ID }] as TerminalTab[]
const parkedTabIds = new Set([TAB_ID])
const EMPTY_PARKED_TAB_IDS = new Set<string>()
const firstUnparkedTabs = ['tab-a', 'tab-b', 'tab-c'].map(
(id) => ({ id, ptyId: `${WORKTREE_ID}@@${id}` }) as TerminalTab
)
const secondUnparkedTabs = ['tab-d', 'tab-e', 'tab-f'].map(
(id) => ({ id, ptyId: `${WORKTREE_ID}@@${id}` }) as TerminalTab
)
function splitLayout(secondPtyId: string): TerminalLayoutSnapshot {
return {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: FIRST_LEAF_ID },
second: { type: 'leaf', leafId: SECOND_LEAF_ID }
},
activeLeafId: FIRST_LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: {
[FIRST_LEAF_ID]: FIRST_PTY_ID,
[SECOND_LEAF_ID]: secondPtyId
}
}
}
function WatcherSynchronizationHarness(): null {
useParkedTerminalWatcherSynchronization({
worktreeId: WORKTREE_ID,
terminalTabs,
inputsKey: JSON.stringify([[TAB_ID, FIRST_PTY_ID, null]]),
assignmentsKey: JSON.stringify([[TAB_ID, 'group-1', false]]),
parkedTabIds
})
// Models the pre-fix sibling disposal effect that StrictMode replays after synchronization.
useEffect(() => () => disposeParkedTerminalWatchersForWorktree(WORKTREE_ID), [])
return null
}
function UnparkedWatcherSynchronizationHarness({
worktreeId,
tabs
}: {
worktreeId: string
tabs: readonly TerminalTab[]
}): null {
useParkedTerminalWatcherSynchronization({
worktreeId,
terminalTabs: tabs,
inputsKey: worktreeId,
assignmentsKey: worktreeId,
parkedTabIds: EMPTY_PARKED_TAB_IDS
})
return null
}
describe('parked terminal watcher synchronization', () => {
let container: HTMLDivElement
let root: Root | undefined
beforeEach(() => {
harness.syncCalls = 0
harness.disposeCalls = 0
harness.reconciliationPtyReads = 0
harness.watchedPtyIds.clear()
useAppStore.setState({
ptyIdsByTabId: { [TAB_ID]: [FIRST_PTY_ID, OLD_SECOND_PTY_ID] },
runtimePaneTitlesByTabId: { [TAB_ID]: { 1: 'active', 2: 'split' } },
terminalLayoutsByTabId: { [TAB_ID]: splitLayout(OLD_SECOND_PTY_ID) }
})
container = document.createElement('div')
document.body.appendChild(container)
})
afterEach(() => {
act(() => root?.unmount())
root = undefined
capturedPanesByTabId.delete(TAB_ID)
container.remove()
})
it('reinstates parked watchers after StrictMode effect replay cleanup', () => {
root = createRoot(container)
act(() => {
root?.render(
<StrictMode>
<WatcherSynchronizationHarness />
</StrictMode>
)
})
expect(harness.disposeCalls).toBeGreaterThanOrEqual(1)
expect(harness.syncCalls).toBe(2)
expect(harness.watchedPtyIds).toEqual(new Set([FIRST_PTY_ID, OLD_SECOND_PTY_ID]))
})
it('resynchronizes a parked split when only its detached PTY and layout change', () => {
root = createRoot(container)
act(() => {
root?.render(<WatcherSynchronizationHarness />)
})
expect(harness.watchedPtyIds).toContain(OLD_SECOND_PTY_ID)
act(() => {
useAppStore.setState({
ptyIdsByTabId: { [TAB_ID]: [FIRST_PTY_ID, NEW_SECOND_PTY_ID] },
terminalLayoutsByTabId: { [TAB_ID]: splitLayout(NEW_SECOND_PTY_ID) }
})
})
expect(harness.syncCalls).toBe(2)
expect(harness.watchedPtyIds).toEqual(new Set([FIRST_PTY_ID, NEW_SECOND_PTY_ID]))
act(() => {
useAppStore.setState({
runtimePaneTitlesByTabId: { [TAB_ID]: { 1: '⠋ active', 2: 'new split title' } }
})
})
expect(harness.syncCalls).toBe(2)
act(() => {
useAppStore.setState({
terminalLayoutsByTabId: {
[TAB_ID]: { ...splitLayout(NEW_SECOND_PTY_ID), activeLeafId: SECOND_LEAF_ID }
}
})
})
expect(harness.syncCalls).toBe(3)
captureParkedTerminalPaneCandidates(TAB_ID, WORKTREE_ID, [
{ ptyId: FIRST_PTY_ID, paneId: 10, leafId: FIRST_LEAF_ID, drivesTabTitle: false },
{ ptyId: NEW_SECOND_PTY_ID, paneId: 11, leafId: SECOND_LEAF_ID, drivesTabTitle: true }
])
act(() => {
root?.render(<WatcherSynchronizationHarness />)
})
expect(harness.syncCalls).toBe(4)
})
it('does not scan unparked worktrees on an unrelated store write', () => {
const capturedPaneGet = vi.spyOn(capturedPanesByTabId, 'get')
const ptyIdsByTabId = new Proxy({} as Record<string, string[]>, {
get(target, property, receiver) {
if (typeof property === 'string') {
harness.reconciliationPtyReads += 1
}
return Reflect.get(target, property, receiver)
}
})
useAppStore.setState({ ptyIdsByTabId })
root = createRoot(container)
act(() => {
root?.render(
<>
<UnparkedWatcherSynchronizationHarness
worktreeId="repo::/unparked-a"
tabs={firstUnparkedTabs}
/>
<UnparkedWatcherSynchronizationHarness
worktreeId="repo::/unparked-b"
tabs={secondUnparkedTabs}
/>
</>
)
})
harness.reconciliationPtyReads = 0
const syncCallsAfterMount = harness.syncCalls
act(() => {
useAppStore.setState({ sortEpoch: 1 })
})
expect(harness.reconciliationPtyReads).toBe(0)
expect(capturedPaneGet).not.toHaveBeenCalled()
expect(harness.syncCalls).toBe(syncCallsAfterMount)
})
})
@@ -0,0 +1,167 @@
import { useEffect, useRef } from 'react'
import { useShallow } from 'zustand/react/shallow'
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types'
import { useAppStore } from '../../store'
import {
disposeParkedTerminalWatchersForWorktree,
syncParkedTerminalTabWatchers
} from './terminal-parked-tab-watchers'
import { capturedPanesByTabId } from './terminal-parked-watcher-registry'
type TerminalParkingAssignment = {
groupId: string
isActiveInGroup: boolean
}
const EMPTY_PTY_IDS: readonly string[] = []
type WatcherReconciliationStoreInputs = readonly (
| readonly string[]
| TerminalLayoutSnapshot
| string
| null
)[]
const EMPTY_WATCHER_RECONCILIATION_INPUTS: WatcherReconciliationStoreInputs = Object.freeze([])
export function getTerminalParkingInputsKey(terminalTabs: readonly TerminalTab[]): string {
return JSON.stringify(terminalTabs.map((tab) => [tab.id, tab.ptyId, tab.pendingActivationSpawn]))
}
export function getTerminalParkingAssignmentsKey(
assignments: ReadonlyMap<string, TerminalParkingAssignment>
): string {
return JSON.stringify(
Array.from(assignments, ([tabId, assignment]) => [
tabId,
assignment.groupId,
assignment.isActiveInGroup
])
)
}
function getWatcherSynchronizationKey(args: {
worktreeId: string
inputsKey: string
assignmentsKey: string
parkedTabIds: ReadonlySet<string>
activationDeferredMountTabIds?: ReadonlySet<string> | null
reconciliationKey: string
}): string {
return JSON.stringify([
args.worktreeId,
args.inputsKey,
args.assignmentsKey,
Array.from(args.parkedTabIds),
Array.from(args.activationDeferredMountTabIds ?? []).sort(),
args.reconciliationKey
])
}
function getCapturedPaneKey(terminalTabs: readonly TerminalTab[]): unknown[] {
return terminalTabs.map((tab) => {
const capture = capturedPanesByTabId.get(tab.id)
return [
tab.id,
capture?.worktreeId ?? null,
capture?.panes.map((pane) => [pane.ptyId, pane.paneId, pane.leafId, pane.drivesTabTitle]) ??
[]
]
})
}
function getWatcherReconciliationKey(
terminalTabs: readonly TerminalTab[],
inputs: WatcherReconciliationStoreInputs
): string {
if (inputs.length === 0) {
return ''
}
const tabs = terminalTabs.map((tab, index) => {
const offset = index * 3
const ptyIds = inputs[offset] as readonly string[]
const layout = inputs[offset + 1] as TerminalLayoutSnapshot | null
const titleSlotKey = inputs[offset + 2] as string
return [
tab.id,
ptyIds,
layout?.root ?? null,
layout?.activeLeafId ?? null,
layout?.ptyIdsByLeafId ?? null,
titleSlotKey
]
})
return JSON.stringify([tabs, getCapturedPaneKey(terminalTabs)])
}
export function useParkedTerminalWatcherSynchronization(args: {
worktreeId: string
terminalTabs: readonly TerminalTab[]
inputsKey: string
assignmentsKey: string
parkedTabIds: ReadonlySet<string>
activationDeferredMountTabIds?: ReadonlySet<string> | null
}): void {
const {
worktreeId,
terminalTabs,
inputsKey,
assignmentsKey,
parkedTabIds,
activationDeferredMountTabIds
} = args
const reconciliationStoreInputs = useAppStore(
useShallow((state) =>
// Why: an empty committed park set has no live watcher state for store writes to reconcile.
parkedTabIds.size === 0
? EMPTY_WATCHER_RECONCILIATION_INPUTS
: terminalTabs.flatMap((tab) => [
state.ptyIdsByTabId[tab.id] ?? EMPTY_PTY_IDS,
state.terminalLayoutsByTabId[tab.id] ?? null,
Object.keys(state.runtimePaneTitlesByTabId[tab.id] ?? {}).join(',')
])
)
) as WatcherReconciliationStoreInputs
const reconciliationKey = getWatcherReconciliationKey(terminalTabs, reconciliationStoreInputs)
const synchronizationKey = getWatcherSynchronizationKey({ ...args, reconciliationKey })
const synchronizationKeyRef = useRef<string | null>(null)
useEffect(
() => () => {
synchronizationKeyRef.current = null
disposeParkedTerminalWatchersForWorktree(worktreeId)
},
[worktreeId]
)
useEffect(() => {
if (synchronizationKeyRef.current === synchronizationKey) {
return
}
syncParkedTerminalTabWatchers({
worktreeId,
tabs: terminalTabs,
parkedTabIds,
// Why: activation-deferred tabs have no prior pane-owned title slot.
restoreTitleOnStartTabIds: activationDeferredMountTabIds ?? undefined
})
// Why: capture cleanup mutates the registry during commit; store its post-commit key.
synchronizationKeyRef.current = getWatcherSynchronizationKey({
worktreeId,
inputsKey,
assignmentsKey,
parkedTabIds,
activationDeferredMountTabIds,
reconciliationKey: getWatcherReconciliationKey(terminalTabs, reconciliationStoreInputs)
})
}, [
activationDeferredMountTabIds,
assignmentsKey,
inputsKey,
parkedTabIds,
reconciliationStoreInputs,
synchronizationKey,
terminalTabs,
worktreeId
])
}
@@ -6,7 +6,9 @@ import type { TerminalTab } from '../../../../shared/types'
const mocks = vi.hoisted(() => ({
storeState: {
pendingStartupByTabId: {} as Record<string, unknown>,
ptyIdsByTabId: {} as Record<string, string[]>,
runtimeStatusByEnvironmentId: new Map(),
runtimePaneTitlesByTabId: {} as Record<string, Record<number, string>>,
settings: {} as Record<string, unknown>,
terminalLayoutsByTabId: {} as Record<string, { ptyIdsByLeafId?: Record<string, string> }>,
sleepingAgentSessionsByPaneKey: {} as Record<
@@ -31,11 +31,12 @@ import {
selectEvictionExemptTerminalTabLayoutKey
} from './terminal-eviction-exempt-tabs'
import { selectSleepingRecordParkExemptTabIds } from './sleeping-record-park-exemption'
import { canWatcherCoverParkedTerminalTab } from './terminal-parked-tab-watchers'
import {
canWatcherCoverParkedTerminalTab,
disposeParkedTerminalWatchersForWorktree,
syncParkedTerminalTabWatchers
} from './terminal-parked-tab-watchers'
getTerminalParkingAssignmentsKey,
getTerminalParkingInputsKey,
useParkedTerminalWatcherSynchronization
} from './use-parked-terminal-watcher-synchronization'
type TerminalOverlayTabAssignment = {
groupId: string
@@ -86,6 +87,14 @@ export function useTerminalTabColdParking(args: {
activityTerminalPortals,
activationDeferredMountTabIds
} = args
const terminalParkingInputsKey = getTerminalParkingInputsKey(terminalTabs)
const terminalParkingAssignmentsKey = getTerminalParkingAssignmentsKey(assignments)
const terminalParkingTabsDependency = coldParkTerminalPanes
? terminalParkingInputsKey
: terminalTabs
const terminalParkingAssignmentsDependency = coldParkTerminalPanes
? terminalParkingAssignmentsKey
: assignments
const pendingStartupByTabId = useAppStore((state) => state.pendingStartupByTabId)
const terminalParkingEnabled = useAppStore(
(state) => state.settings?.terminalHiddenViewParking !== false
@@ -247,17 +256,18 @@ export function useTerminalTabColdParking(args: {
timers.set(tabId, timer)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- semantic keys own the tab and assignment dependencies.
}, [
activityTerminalPortals,
assignments,
isWorktreeActive,
pendingStartupByTabId,
pairedRuntimeParkingEnvironmentIds,
runtimeStatusByEnvironmentId,
shouldMeasureHiddenWorktree,
terminalParkingEnabled,
terminalSshParkingEnabled,
terminalTabParkingRevision,
terminalTabs,
terminalParkingAssignmentsDependency,
terminalParkingTabsDependency,
worktreeId
])
@@ -357,18 +367,14 @@ export function useTerminalTabColdParking(args: {
// panes — watcher disposal therefore lands before any PTY data IPC can
// reach a freshly remounted pane, and watcher start lands after the parked
// pane's unmount capture.
useEffect(() => {
syncParkedTerminalTabWatchers({
worktreeId,
tabs: terminalTabs,
parkedTabIds: parkedTerminalTabIds,
// Why: activation-deferred tabs have no prior pane-owned title slot;
// pull main's title-only snapshot when their watcher starts.
restoreTitleOnStartTabIds: activationDeferredMountTabIds ?? undefined
})
}, [activationDeferredMountTabIds, parkedTerminalTabIds, terminalTabs, worktreeId])
useEffect(() => () => disposeParkedTerminalWatchersForWorktree(worktreeId), [worktreeId])
useParkedTerminalWatcherSynchronization({
worktreeId,
terminalTabs,
assignmentsKey: terminalParkingAssignmentsKey,
inputsKey: terminalParkingInputsKey,
parkedTabIds: parkedTerminalTabIds,
activationDeferredMountTabIds
})
return parkedTerminalTabIds
}
@@ -4,6 +4,9 @@ import path from 'node:path'
const FILL_ROWS = 6_000
const FLOOD_ROWS = 4_000
const TITLE_FRAME_INTERVAL_MS = 70
const TITLE_FRAME_COUNT = 30
const TITLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
function shellQuote(value: string): string {
return `'${value.replaceAll("'", `'\\''`)}'`
@@ -31,6 +34,29 @@ export function createPairedTerminalParkingFixture(): {
"process.stdin.setEncoding('utf8')",
"process.stdin.on('data', (data) => {",
' for (const command of data.split(/\\r\\n|\\r|\\n/).filter(Boolean)) {',
" if (command.startsWith('TITLE_START:')) {",
" const token = command.slice('TITLE_START:'.length)",
" process.stdout.write('\\u001b]0;⠋ Cursor Agent\\u0007')",
' process.stdout.write(`TITLE_STARTED:${token}\\r\\n`)',
' continue',
' }',
" if (command.startsWith('TITLE_ANIMATE:')) {",
" const token = command.slice('TITLE_ANIMATE:'.length)",
` const frames = ${JSON.stringify(TITLE_FRAMES)}`,
' let frameIndex = 0',
' const emitFrame = () => {',
` if (frameIndex < ${TITLE_FRAME_COUNT}) {`,
' process.stdout.write(`\\u001b]0;${frames[frameIndex % frames.length]} Cursor Agent\\u0007`)',
' frameIndex += 1',
` setTimeout(emitFrame, ${TITLE_FRAME_INTERVAL_MS})`,
' return',
' }',
" process.stdout.write('\\u001b]0;Cursor ready\\u0007')",
' process.stdout.write(`TITLE_DONE:${token}\\r\\n`)',
' }',
` setTimeout(emitFrame, ${TITLE_FRAME_INTERVAL_MS})`,
' continue',
' }',
" if (command.startsWith('FLOOD:')) {",
" const token = command.slice('FLOOD:'.length)",
` for (let row = 0; row < ${FLOOD_ROWS}; row += 1) process.stdout.write(\`flood-${'${token}'}-${'${row}'}-${'x'.repeat(80)}\\r\\n\`)`,
@@ -12,6 +12,7 @@ import {
import { expect } from './orca-app'
import { verifyHiddenPairedTerminalOutputSuppression } from './paired-terminal-hidden-output-oracle'
import { createPairedTerminalParkingFixture } from './paired-terminal-parking-fixture'
import { verifyPairedTerminalTitleFanout } from './paired-terminal-title-fanout-oracle'
import { getTerminalContent, waitForActivePanePtyId } from './terminal'
const TARGET_WORKTREE_COUNT = 6
@@ -135,6 +136,7 @@ export async function runPairedTerminalParkingOracle(
await expectHostTerminalsUnmounted(options.hostPage, seed.fallbackWorktreeId, remoteTabs)
const hiddenFloodTokens = await verifyHiddenPairedTerminalOutputSuppression(page, remoteTabs)
await verifyPairedTerminalTitleFanout(page, remoteTabs)
const baseline = await readPairedRetentionSample(
page,
remoteTabs.map((tab) => tab.tabId)
@@ -0,0 +1,310 @@
import type { Page } from '@stablyai/playwright-test'
import type { RuntimeTerminalRead } from '../../../src/shared/runtime-types'
import { TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version'
import { expect } from './orca-app'
const INITIAL_TITLE = '⠋ Cursor Agent'
const FINAL_TITLE = 'Cursor ready'
type TitleTarget = {
tabId: string
terminal: string
worktreeId: string
}
type TitleTransportEvent = {
decodedEnvelopeBytes: number
snapshotVersion: number
title: string
worktreeId: string
}
type TitleStoreChange = {
title: string
worktreeId: string
}
type TitleProbeSample = {
storeChanges: TitleStoreChange[]
transportEvents: TitleTransportEvent[]
}
type BrowserTitleProbe = TitleProbeSample & {
lastTitles: Record<string, string | null>
responseCount: number
subscription: null | { unsubscribe: () => void }
unsubscribeStore: null | (() => void)
}
async function callRuntime<TResult>(page: Page, method: string, params: unknown): Promise<TResult> {
return page.evaluate(
async ({ method, params }) => {
const response = await window.api.runtime.call({ method, params })
if (!response.ok) {
throw new Error(`${response.error.code}: ${response.error.message}`)
}
return response.result
},
{ method, params }
) as Promise<TResult>
}
export async function verifyPairedTerminalTitleFanout(
page: Page,
targets: TitleTarget[]
): Promise<void> {
const tokens = targets.map((_, index) => `TITLE_FANOUT_${index}_${Date.now()}`)
await installTitleProbe(page, targets)
try {
await expect.poll(() => readProbeResponseCount(page), { timeout: 30_000 }).toBeGreaterThan(0)
await resetTitleProbe(page, targets)
await Promise.all(
targets.map((target, index) =>
sendFixtureCommand(page, target, `TITLE_START:${tokens[index]}`)
)
)
await expectTitles(page, targets, INITIAL_TITLE)
await expect
.poll(async () => (await readTitleProbe(page)).transportEvents.length, { timeout: 30_000 })
.toBeGreaterThanOrEqual(targets.length)
await Promise.all(
targets.map((target, index) =>
sendFixtureCommand(page, target, `TITLE_ANIMATE:${tokens[index]}`)
)
)
await expectFixtureCompletion(page, targets, tokens)
await expectTitles(page, targets, FINAL_TITLE)
await expect
.poll(async () => (await readTitleProbe(page)).transportEvents.length, { timeout: 30_000 })
.toBeGreaterThanOrEqual(targets.length * 2)
const sample = await readTitleProbe(page)
expectTitleFanoutSample(sample, targets)
} finally {
await disposeTitleProbe(page)
}
}
async function installTitleProbe(page: Page, targets: TitleTarget[]): Promise<void> {
await page.evaluate(
async ({ targets, capability }) => {
const store = window.__store
if (!store) {
throw new Error('Renderer store is unavailable')
}
const environmentId = Array.from(
store.getState().runtimeStatusByEnvironmentId.entries()
).find(([, entry]) => entry.status?.capabilities?.includes(capability))?.[0]
if (!environmentId) {
throw new Error('Paired runtime environment is unavailable')
}
const titleFor = (
state: ReturnType<typeof store.getState>,
target: (typeof targets)[number]
) =>
state.tabsByWorktree[target.worktreeId]?.find((tab) => tab.id === target.tabId)?.title ??
null
const probe = {
responseCount: 0,
storeChanges: [] as TitleStoreChange[],
transportEvents: [] as TitleTransportEvent[],
lastTitles: Object.fromEntries(
targets.map((target) => [target.worktreeId, titleFor(store.getState(), target)])
),
subscription: null as null | { unsubscribe: () => void },
unsubscribeStore: null as null | (() => void)
}
probe.unsubscribeStore = store.subscribe((state) => {
for (const target of targets) {
const title = titleFor(state, target)
if (title === null || title === probe.lastTitles[target.worktreeId]) {
continue
}
probe.lastTitles[target.worktreeId] = title
probe.storeChanges.push({ worktreeId: target.worktreeId, title })
}
})
probe.subscription = await window.api.runtimeEnvironments.subscribe(
{ selector: environmentId, method: 'session.tabs.subscribeAll', params: {} },
{
onResponse: (response) => {
probe.responseCount += 1
if (!response.ok) {
return
}
const result = response.result as {
type?: string
worktree?: string
snapshotVersion?: number
tabs?: { type?: string; title?: string }[]
snapshots?: {
worktree?: string
snapshotVersion?: number
tabs?: { type?: string; title?: string }[]
}[]
}
const snapshots = result.type === 'snapshots' ? (result.snapshots ?? []) : [result]
const decodedEnvelopeBytes = new TextEncoder().encode(
JSON.stringify(response)
).byteLength
for (const snapshot of snapshots) {
if (!targets.some((target) => target.worktreeId === snapshot.worktree)) {
continue
}
const title = snapshot.tabs?.find((tab) => tab.type === 'terminal')?.title
if (
typeof snapshot.worktree === 'string' &&
typeof snapshot.snapshotVersion === 'number' &&
typeof title === 'string'
) {
probe.transportEvents.push({
worktreeId: snapshot.worktree,
snapshotVersion: snapshot.snapshotVersion,
title,
decodedEnvelopeBytes
})
}
}
}
}
)
Object.assign(globalThis, { __pairedTitleFanoutProbe: probe })
},
{ targets, capability: TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY }
)
}
async function resetTitleProbe(page: Page, targets: TitleTarget[]): Promise<void> {
await page.evaluate((targets) => {
const probe = (
globalThis as typeof globalThis & { __pairedTitleFanoutProbe?: BrowserTitleProbe }
).__pairedTitleFanoutProbe
if (!probe) {
throw new Error('Paired title fanout probe is unavailable')
}
probe.transportEvents.length = 0
probe.storeChanges.length = 0
const state = window.__store?.getState()
for (const target of targets) {
probe.lastTitles[target.worktreeId] =
state?.tabsByWorktree[target.worktreeId]?.find((tab) => tab.id === target.tabId)?.title ??
null
}
}, targets)
}
async function readProbeResponseCount(page: Page): Promise<number> {
return page.evaluate(() => {
const probe = (
globalThis as typeof globalThis & { __pairedTitleFanoutProbe?: BrowserTitleProbe }
).__pairedTitleFanoutProbe
if (!probe) {
throw new Error('Paired title fanout probe is unavailable')
}
return probe.responseCount
})
}
async function readTitleProbe(page: Page): Promise<TitleProbeSample> {
return page.evaluate(() => {
const probe = (
globalThis as typeof globalThis & { __pairedTitleFanoutProbe?: BrowserTitleProbe }
).__pairedTitleFanoutProbe
if (!probe) {
throw new Error('Paired title fanout probe is unavailable')
}
return {
storeChanges: [...probe.storeChanges],
transportEvents: [...probe.transportEvents]
}
})
}
async function disposeTitleProbe(page: Page): Promise<void> {
await page
.evaluate(() => {
const probe = (
globalThis as typeof globalThis & { __pairedTitleFanoutProbe?: BrowserTitleProbe }
).__pairedTitleFanoutProbe
if (!probe) {
return
}
probe.subscription?.unsubscribe()
probe.unsubscribeStore?.()
delete (globalThis as typeof globalThis & { __pairedTitleFanoutProbe?: unknown })
.__pairedTitleFanoutProbe
})
.catch(() => undefined)
}
async function sendFixtureCommand(page: Page, target: TitleTarget, text: string): Promise<void> {
await callRuntime(page, 'terminal.send', {
terminal: target.terminal,
text,
enter: true,
client: { id: 'paired-title-fanout-e2e', type: 'desktop' }
})
}
async function expectFixtureCompletion(
page: Page,
targets: TitleTarget[],
tokens: string[]
): Promise<void> {
await expect
.poll(
async () =>
Promise.all(
targets.map(async (target, index) => {
const result = await callRuntime<{ terminal: RuntimeTerminalRead }>(
page,
'terminal.read',
{ terminal: target.terminal, limit: 1_000 }
)
return result.terminal.tail.join('\n').includes(`TITLE_DONE:${tokens[index]}`)
})
),
{ timeout: 30_000 }
)
.toEqual(Array(targets.length).fill(true))
}
async function expectTitles(page: Page, targets: TitleTarget[], expected: string): Promise<void> {
await expect
.poll(
() =>
page.evaluate(
({ targets, expected }) =>
targets.map(
(target) =>
window.__store
?.getState()
.tabsByWorktree[target.worktreeId]?.find((tab) => tab.id === target.tabId)
?.title === expected
),
{ targets, expected }
),
{ timeout: 30_000 }
)
.toEqual(Array(targets.length).fill(true))
}
function expectTitleFanoutSample(sample: TitleProbeSample, targets: TitleTarget[]): void {
expect(sample.transportEvents).toHaveLength(targets.length * 2)
expect(sample.storeChanges).toHaveLength(targets.length * 2)
for (const target of targets) {
const transport = sample.transportEvents.filter(
(event) => event.worktreeId === target.worktreeId
)
expect(transport.map((event) => event.title)).toEqual([INITIAL_TITLE, FINAL_TITLE])
expect(transport[1]?.snapshotVersion).toBeGreaterThan(transport[0]?.snapshotVersion ?? 0)
expect(transport.every((event) => event.decodedEnvelopeBytes > 0)).toBe(true)
expect(
sample.storeChanges
.filter((change) => change.worktreeId === target.worktreeId)
.map((change) => change.title)
).toEqual([INITIAL_TITLE, FINAL_TITLE])
}
}
@@ -0,0 +1,772 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type {
RuntimeMobileSessionTabsResult,
RuntimeMobileSessionTabsSnapshot
} from '../../src/shared/runtime-types'
import { detectAgentStatusFromTitle } from '../../src/shared/agent-detection'
import { AGENT_STATUS_STALE_AFTER_MS } from '../../src/shared/agent-status-types'
import { isExplicitAgentStatusFresh } from '../../src/renderer/src/lib/agent-status'
import { OrcaRuntimeService } from '../../src/main/runtime/orca-runtime'
import {
SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS,
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
} from '../../src/main/runtime/mobile-session-tabs-agent-status-heartbeat'
import {
applyFreshWebSessionTabsSnapshot,
resetWebSessionTabsSnapshotFreshnessForTests,
type WebSessionTabsSyncState
} from '../../src/renderer/src/runtime/web-session-tabs-sync'
vi.mock('../../src/renderer/src/store', () => ({
useAppStore: {
setState: vi.fn()
}
}))
const ENVIRONMENT_ID = 'paired-runtime'
const WORKTREE_COUNT = 24
const DECORATIVE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
type RuntimeInternals = {
mobileSessionTabsByWorktree: Map<string, RuntimeMobileSessionTabsSnapshot>
ptysById: Map<string, { launchAgent: 'grok-build' | 'pi' | null }>
ptyDelayedForegroundSnapshotTitleObservations: Map<string, number>
resetTrackedTerminalStateForProviderGeneration: (ptyId: string) => void
}
type FanoutCounters = {
hostPublications: number
serializedBytes: number
rendererApplyCalls: number
rendererStoreMutations: number
rawTerminalChunks: number
}
type FanoutEvidence = {
publishedByWorktree: Map<string, RuntimeMobileSessionTabsResult[]>
rawChunksByPty: Map<string, string[]>
}
function makeViewerState(): WebSessionTabsSyncState {
return {
activeBrowserTabId: null,
activeBrowserTabIdByWorktree: {},
activeFileId: null,
activeFileIdByWorktree: {},
activeGroupIdByWorktree: {},
activeTabId: null,
activeTabIdByWorktree: {},
activeTabType: 'terminal',
activeTabTypeByWorktree: {},
activeWorktreeId: 'workspace-0',
agentStatusByPaneKey: {},
agentStatusEpoch: 0,
browserCertificateFailuresByPageId: {},
browserPagesByWorkspace: {},
browserTabsByWorktree: {},
groupsByWorktree: {},
layoutByWorktree: {},
openFiles: [],
ptyIdsByTabId: {},
remoteBrowserPageHandlesByPageId: {},
tabBarOrderByWorktree: {},
tabsByWorktree: {},
terminalLayoutsByTabId: {},
unifiedTabsByWorktree: {},
unreadTerminalTabs: {},
sortEpoch: 0
}
}
function seedWorktree(runtime: OrcaRuntimeService, index: number): string {
const worktreeId = `workspace-${index}`
const ptyId = `pty-${index}`
const tabId = `host-tab-${index}`
const leafId = `00000000-0000-4000-8000-${index.toString().padStart(12, '0')}`
runtime.registerPty(ptyId, worktreeId)
;(runtime as unknown as RuntimeInternals).mobileSessionTabsByWorktree.set(worktreeId, {
worktree: worktreeId,
publicationEpoch: 'epoch-1',
snapshotVersion: 1,
activeGroupId: `group-${index}`,
activeTabId: `${tabId}::${leafId}`,
activeTabType: 'terminal',
tabs: [
{
type: 'terminal',
id: `${tabId}::${leafId}`,
parentTabId: tabId,
leafId,
ptyId,
title: 'Cursor Agent',
parentLayout: {
root: { type: 'leaf', leafId },
activeLeafId: leafId,
expandedLeafId: null,
ptyIdsByLeafId: { [leafId]: ptyId }
},
isActive: true
}
]
})
return ptyId
}
function resetCounters(counters: FanoutCounters): void {
counters.hostPublications = 0
counters.serializedBytes = 0
counters.rendererApplyCalls = 0
counters.rendererStoreMutations = 0
counters.rawTerminalChunks = 0
}
function resetEvidence(evidence: FanoutEvidence): void {
evidence.publishedByWorktree.clear()
evidence.rawChunksByPty.clear()
}
function recordByKey<T>(map: Map<string, T[]>, key: string, value: T): void {
const entries = map.get(key) ?? []
entries.push(value)
map.set(key, entries)
}
describe('real PTY decorative session-tabs fanout', () => {
beforeEach(() => {
vi.useFakeTimers()
resetWebSessionTabsSnapshotFreshnessForTests()
})
afterEach(() => {
vi.useRealTimers()
})
it('bounds host publication and renderer application across remote worktrees', () => {
const runtime = new OrcaRuntimeService()
const ptyIds = Array.from({ length: WORKTREE_COUNT }, (_, index) =>
seedWorktree(runtime, index)
)
const counters: FanoutCounters = {
hostPublications: 0,
serializedBytes: 0,
rendererApplyCalls: 0,
rendererStoreMutations: 0,
rawTerminalChunks: 0
}
const evidence: FanoutEvidence = {
publishedByWorktree: new Map(),
rawChunksByPty: new Map()
}
let viewerState = makeViewerState()
const dataUnsubscribes = ptyIds.map((ptyId) =>
runtime.subscribeToTerminalData(ptyId, (data) => {
counters.rawTerminalChunks += 1
recordByKey(evidence.rawChunksByPty, ptyId, data)
})
)
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
counters.hostPublications += 1
counters.serializedBytes += Buffer.byteLength(JSON.stringify(snapshot))
recordByKey(evidence.publishedByWorktree, snapshot.worktree, structuredClone(snapshot))
counters.rendererApplyCalls += 1
const patch = applyFreshWebSessionTabsSnapshot(
viewerState,
snapshot,
ENVIRONMENT_ID,
Date.now()
)
if (patch !== viewerState) {
counters.rendererStoreMutations += 1
viewerState = { ...viewerState, ...patch }
}
})
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;⠋ Cursor Agent\x07', Date.now())
}
vi.advanceTimersByTime(50)
expect(counters.hostPublications).toBe(WORKTREE_COUNT)
expect(counters.rendererStoreMutations).toBe(WORKTREE_COUNT)
resetCounters(counters)
resetEvidence(evidence)
for (let cycle = 0; cycle < 4; cycle += 1) {
for (const frame of DECORATIVE_FRAMES) {
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, `\x1b]0;${frame} Cursor Agent\x07`, Date.now())
}
vi.advanceTimersByTime(30)
}
}
vi.advanceTimersByTime(50)
expect(counters).toEqual({
hostPublications: 0,
serializedBytes: 0,
rendererApplyCalls: 0,
rendererStoreMutations: 0,
rawTerminalChunks: WORKTREE_COUNT * DECORATIVE_FRAMES.length * 4
})
for (const ptyId of ptyIds) {
expect(evidence.rawChunksByPty.get(ptyId)).toEqual(
Array.from({ length: 4 }, () =>
DECORATIVE_FRAMES.map((frame) => `\x1b]0;${frame} Cursor Agent\x07`)
).flat()
)
}
expect(evidence.publishedByWorktree.size).toBe(0)
resetCounters(counters)
resetEvidence(evidence)
for (let cycle = 0; cycle < 4; cycle += 1) {
for (const frame of DECORATIVE_FRAMES) {
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;Cursor Agent\x07', Date.now())
runtime.onPtyData(ptyId, `\x1b]0;${frame} Cursor Agent\x07`, Date.now())
}
vi.advanceTimersByTime(30)
}
}
vi.advanceTimersByTime(50)
expect(counters).toEqual({
hostPublications: 0,
serializedBytes: 0,
rendererApplyCalls: 0,
rendererStoreMutations: 0,
rawTerminalChunks: WORKTREE_COUNT * DECORATIVE_FRAMES.length * 8
})
for (const ptyId of ptyIds) {
expect(evidence.rawChunksByPty.get(ptyId)).toEqual(
Array.from({ length: 4 }, () =>
DECORATIVE_FRAMES.flatMap((frame) => [
'\x1b]0;Cursor Agent\x07',
`\x1b]0;${frame} Cursor Agent\x07`
])
).flat()
)
}
expect(evidence.publishedByWorktree.size).toBe(0)
resetCounters(counters)
resetEvidence(evidence)
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;Cursor ready\x07', Date.now())
}
vi.advanceTimersByTime(50)
expect(counters.hostPublications).toBe(WORKTREE_COUNT)
expect(counters.rendererApplyCalls).toBe(WORKTREE_COUNT)
expect(counters.rendererStoreMutations).toBe(WORKTREE_COUNT)
expect(counters.serializedBytes).toBeGreaterThan(0)
for (let index = 0; index < WORKTREE_COUNT; index += 1) {
const worktreeId = `workspace-${index}`
const snapshots = evidence.publishedByWorktree.get(worktreeId)
expect(snapshots).toHaveLength(1)
const terminal = snapshots?.[0]?.tabs[0]
expect(terminal).toMatchObject({
type: 'terminal',
parentTabId: `host-tab-${index}`,
leafId: `00000000-0000-4000-8000-${index.toString().padStart(12, '0')}`,
ptyId: `pty-${index}`,
title: 'Cursor ready'
})
expect(terminal?.type === 'terminal' ? terminal.agentStatus?.state : undefined).toBe('done')
expect(viewerState.tabsByWorktree[worktreeId]).toEqual([
expect.objectContaining({ title: 'Cursor ready', worktreeId })
])
expect(
detectAgentStatusFromTitle(viewerState.tabsByWorktree[worktreeId]?.[0]?.title ?? '')
).toBe('idle')
expect(evidence.rawChunksByPty.get(`pty-${index}`)).toEqual(['\x1b]0;Cursor ready\x07'])
}
resetCounters(counters)
resetEvidence(evidence)
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, 'visible output\r\n', Date.now())
}
vi.advanceTimersByTime(50)
expect(counters).toEqual({
hostPublications: 0,
serializedBytes: 0,
rendererApplyCalls: 0,
rendererStoreMutations: 0,
rawTerminalChunks: WORKTREE_COUNT
})
for (const ptyId of ptyIds) {
expect(evidence.rawChunksByPty.get(ptyId)).toEqual(['visible output\r\n'])
}
expect(evidence.publishedByWorktree.size).toBe(0)
unsubscribe()
for (const dispose of dataUnsubscribes) {
dispose()
}
})
it.each([
['build ⠁', 'build ⠂'],
['Codex working task ⠁', 'Codex working task ⠂']
])('publishes meaningful real-title changes from %j to %j', (firstTitle, secondTitle) => {
const runtime = new OrcaRuntimeService()
seedWorktree(runtime, 0)
const published: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => published.push(snapshot))
runtime.onPtyData('pty-0', `\x1b]0;${firstTitle}\x07`, Date.now())
vi.advanceTimersByTime(50)
published.length = 0
runtime.onPtyData('pty-0', `\x1b]0;${secondTitle}\x07`, Date.now())
vi.advanceTimersByTime(50)
expect(published).toHaveLength(1)
expect(published[0]?.tabs[0]).toMatchObject({ title: secondTitle, ptyId: 'pty-0' })
unsubscribe()
})
it('globally caps decorative freshness while every working status stays fresh', () => {
const runtime = new OrcaRuntimeService()
const firstWorkingTitleAt = Date.now()
const ptyIds = Array.from({ length: WORKTREE_COUNT }, (_, index) =>
seedWorktree(runtime, index)
)
const publications: {
worktreeId: string
at: number
updatedAt: number
stateStartedAt: number
}[] = []
let viewerState = makeViewerState()
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
const terminal = snapshot.tabs[0]
if (terminal?.type !== 'terminal') {
return
}
publications.push({
worktreeId: snapshot.worktree,
at: Date.now(),
updatedAt: terminal.agentStatus?.updatedAt ?? 0,
stateStartedAt: terminal.agentStatus?.stateStartedAt ?? 0
})
const patch = applyFreshWebSessionTabsSnapshot(
viewerState,
snapshot,
ENVIRONMENT_ID,
Date.now()
)
viewerState = { ...viewerState, ...patch }
})
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;⠋ Cursor Agent\x07', Date.now())
}
vi.advanceTimersByTime(50)
const initialStateStartedAtByWorktree = new Map(
publications.map(({ worktreeId, stateStartedAt }) => [worktreeId, stateStartedAt])
)
publications.length = 0
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS - 51)
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;⠙ Cursor Agent\x07', Date.now())
}
vi.advanceTimersByTime(50)
expect(publications).toEqual([])
vi.advanceTimersByTime(1)
const heartbeatStartedAt = Date.now()
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;⠹ Cursor Agent\x07', Date.now())
}
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS * WORKTREE_COUNT + 50)
expect(publications.map(({ worktreeId }) => worktreeId).sort()).toEqual(
Array.from({ length: WORKTREE_COUNT }, (_, index) => `workspace-${index}`).sort()
)
for (let index = 1; index < publications.length; index += 1) {
expect(publications[index]!.at - publications[index - 1]!.at).toBeGreaterThanOrEqual(
SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS
)
}
for (const publication of publications) {
expect(publication.updatedAt).toBeGreaterThanOrEqual(heartbeatStartedAt)
expect(Date.now() - publication.updatedAt).toBeLessThan(AGENT_STATUS_STALE_AFTER_MS)
expect(publication.stateStartedAt).toBe(
initialStateStartedAtByWorktree.get(publication.worktreeId)
)
}
publications.length = 0
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;⠸ Cursor Agent\x07', Date.now())
}
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_SPACING_MS * WORKTREE_COUNT + 50)
expect(publications).toHaveLength(WORKTREE_COUNT)
expect(Date.now() - firstWorkingTitleAt).toBeGreaterThan(AGENT_STATUS_STALE_AFTER_MS)
const viewerAgentStatuses = Object.values(viewerState.agentStatusByPaneKey)
expect(viewerAgentStatuses).toHaveLength(WORKTREE_COUNT)
expect(
viewerAgentStatuses.every(
(entry) =>
entry.state === 'working' &&
isExplicitAgentStatusFresh(entry, Date.now(), AGENT_STATUS_STALE_AFTER_MS)
)
).toBe(true)
publications.length = 0
const completionAt = Date.now()
for (const ptyId of ptyIds) {
runtime.onPtyData(ptyId, '\x1b]0;Cursor ready\x07', Date.now())
}
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(WORKTREE_COUNT)
expect(publications.every(({ updatedAt }) => updatedAt === completionAt)).toBe(true)
unsubscribe()
})
it.each([
{
agent: 'pi' as const,
firstTitle: '⠋ π - project',
heartbeatTitles: ['⠋ π - project', '⠙ π - project'],
expectedTitle: '⠋ Pi'
},
{
agent: 'grok-build' as const,
firstTitle: '⠋ - Waiting for response… - grok',
heartbeatTitles: ['⠴ - Thinking - grok', '⠦ - Sleep 2s then echo hello… - grok'],
expectedTitle: '⠋ Grok'
}
])('renews exact and normalized $agent frames beyond the viewer stale boundary', (testCase) => {
const runtime = new OrcaRuntimeService()
const firstWorkingTitleAt = Date.now()
const ptyId = seedWorktree(runtime, 0)
const internals = runtime as unknown as RuntimeInternals
internals.ptysById.get(ptyId)!.launchAgent = testCase.agent
const seededTab = internals.mobileSessionTabsByWorktree.get('workspace-0')?.tabs[0]
if (seededTab?.type !== 'terminal') {
throw new Error('expected seeded terminal')
}
seededTab.agentStatus = {
state: 'working',
prompt: 'task',
updatedAt: firstWorkingTitleAt,
stateStartedAt: firstWorkingTitleAt,
agentType: testCase.agent,
paneKey: seededTab.id,
stateHistory: []
}
const publications: RuntimeMobileSessionTabsResult[] = []
let viewerState = makeViewerState()
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
publications.push(structuredClone(snapshot))
const patch = applyFreshWebSessionTabsSnapshot(
viewerState,
snapshot,
ENVIRONMENT_ID,
Date.now()
)
viewerState = { ...viewerState, ...patch }
})
runtime.onPtyData(ptyId, `\x1b]0;${testCase.firstTitle}\x07`, Date.now())
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(1)
const initialTerminal = publications[0]?.tabs[0]
const initialAgentStatus =
initialTerminal?.type === 'terminal' ? initialTerminal.agentStatus : undefined
expect(initialAgentStatus?.state).toBe('working')
publications.length = 0
for (const title of testCase.heartbeatTitles) {
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
runtime.onPtyData(ptyId, `\x1b]0;${title}\x07`, Date.now())
vi.advanceTimersByTime(50)
}
expect(publications).toHaveLength(2)
expect(
publications.every((snapshot) => {
const terminal = snapshot.tabs[0]
return terminal?.type === 'terminal' && terminal.title === testCase.expectedTitle
})
).toBe(true)
const finalTerminal = publications.at(-1)?.tabs[0]
const finalAgentStatus =
finalTerminal?.type === 'terminal' ? finalTerminal.agentStatus : undefined
expect(finalAgentStatus?.updatedAt).toBeGreaterThan(initialAgentStatus!.updatedAt)
expect(finalAgentStatus?.stateStartedAt).toBe(initialAgentStatus?.stateStartedAt)
expect(finalAgentStatus?.prompt).toBe('')
expect(finalAgentStatus?.agentType).toBe(initialAgentStatus?.agentType)
const acknowledgedAt = initialAgentStatus!.updatedAt + 1
expect(acknowledgedAt).toBeGreaterThanOrEqual(finalAgentStatus!.stateStartedAt)
expect(Date.now() - firstWorkingTitleAt).toBeGreaterThan(AGENT_STATUS_STALE_AFTER_MS)
const viewerAgentStatus = Object.values(viewerState.agentStatusByPaneKey)[0]
expect(viewerAgentStatus?.state).toBe('working')
expect(
isExplicitAgentStatusFresh(viewerAgentStatus!, Date.now(), AGENT_STATUS_STALE_AFTER_MS)
).toBe(true)
publications.length = 0
vi.advanceTimersByTime(1)
runtime.onPtyData(ptyId, '\x1b]0;bash\x07', Date.now())
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(1)
const completedTerminal = publications[0]?.tabs[0]
const completedStatus =
completedTerminal?.type === 'terminal' ? completedTerminal.agentStatus : undefined
expect(completedStatus).toMatchObject({ state: 'done', prompt: '' })
expect(completedStatus?.stateStartedAt).toBeGreaterThan(finalAgentStatus!.stateStartedAt)
unsubscribe()
})
it('does not publish a stored working row over a later permission title', () => {
const runtime = new OrcaRuntimeService()
const ptyId = seedWorktree(runtime, 0)
const internals = runtime as unknown as RuntimeInternals
const seededTab = internals.mobileSessionTabsByWorktree.get('workspace-0')?.tabs[0]
if (seededTab?.type !== 'terminal') {
throw new Error('expected seeded terminal')
}
seededTab.agentStatus = {
state: 'working',
prompt: 'previous task',
updatedAt: Date.now(),
stateStartedAt: Date.now(),
agentType: 'cursor',
paneKey: seededTab.id,
stateHistory: []
}
const publications: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
publications.push(structuredClone(snapshot))
})
runtime.onPtyData(ptyId, '\x1b]0;⠋ Cursor Agent\x07', Date.now())
vi.advanceTimersByTime(50)
publications.length = 0
vi.advanceTimersByTime(1)
const permissionAt = Date.now()
runtime.onPtyData(ptyId, '\x1b]0;Cursor Agent waiting\x07', permissionAt)
vi.advanceTimersByTime(50)
const terminal = publications.at(-1)?.tabs[0]
const status = terminal?.type === 'terminal' ? terminal.agentStatus : undefined
expect(status).toMatchObject({
state: 'blocked',
prompt: '',
updatedAt: permissionAt,
stateStartedAt: permissionAt
})
unsubscribe()
})
it('does not publish a prior done row over a newer working title', () => {
const runtime = new OrcaRuntimeService()
const ptyId = seedWorktree(runtime, 0)
const internals = runtime as unknown as RuntimeInternals
const seededTab = internals.mobileSessionTabsByWorktree.get('workspace-0')?.tabs[0]
if (seededTab?.type !== 'terminal') {
throw new Error('expected seeded terminal')
}
seededTab.agentStatus = {
state: 'working',
prompt: 'first task',
updatedAt: Date.now(),
stateStartedAt: Date.now(),
agentType: 'cursor',
paneKey: seededTab.id,
stateHistory: []
}
const publications: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
publications.push(structuredClone(snapshot))
})
runtime.onPtyData(ptyId, '\x1b]0;⠋ Cursor Agent\x07', Date.now())
vi.advanceTimersByTime(50)
vi.advanceTimersByTime(1)
runtime.onPtyData(ptyId, '\x1b]0;bash\x07', Date.now())
vi.advanceTimersByTime(50)
vi.advanceTimersByTime(1)
const doneAt = Date.now()
seededTab.agentStatus = {
state: 'done',
prompt: 'first task',
updatedAt: doneAt,
stateStartedAt: doneAt,
agentType: 'cursor',
paneKey: seededTab.id,
stateHistory: []
}
publications.length = 0
vi.advanceTimersByTime(1)
const nextWorkingAt = Date.now()
runtime.onPtyData(ptyId, '\x1b]0;⠙ Cursor Agent\x07', nextWorkingAt)
vi.advanceTimersByTime(50)
const terminal = publications.at(-1)?.tabs[0]
const status = terminal?.type === 'terminal' ? terminal.agentStatus : undefined
expect(status).toMatchObject({
state: 'working',
prompt: '',
updatedAt: nextWorkingAt,
stateStartedAt: nextWorkingAt
})
unsubscribe()
})
it('holds decorative heartbeats behind unresolved foreground ownership', async () => {
let resolveForegroundProcess: (agent: string | null) => void = () => undefined
const foregroundProcess = new Promise<string | null>((resolve) => {
resolveForegroundProcess = resolve
})
const getForegroundProcess = vi.fn(() => foregroundProcess)
const runtime = new OrcaRuntimeService()
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-0' }),
write: () => true,
kill: () => true,
getForegroundProcess
})
const ptyId = seedWorktree(runtime, 0)
const publications: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
publications.push(structuredClone(snapshot))
})
runtime.onPtyData(ptyId, '\x1b]0;⠋ Pi\x07', Date.now())
runtime.onPtyData(ptyId, '\x1b]0;⠙ Pi\x07', Date.now())
await vi.advanceTimersByTimeAsync(50)
expect(getForegroundProcess).toHaveBeenCalledTimes(1)
expect(publications).toHaveLength(0)
resolveForegroundProcess('pi')
await vi.advanceTimersByTimeAsync(50)
expect(publications).toHaveLength(1)
expect(publications[0]?.tabs[0]).toMatchObject({
type: 'terminal',
title: '⠋ Pi',
agentStatus: { state: 'working' }
})
runtime.onPtyData(ptyId, '\x1b]0;⠹ Pi\x07', Date.now())
await vi.advanceTimersByTimeAsync(50)
expect(publications).toHaveLength(1)
await vi.advanceTimersByTimeAsync(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
runtime.onPtyData(ptyId, '\x1b]0;⠸ Pi\x07', Date.now())
await vi.advanceTimersByTimeAsync(50)
expect(publications).toHaveLength(2)
unsubscribe()
})
it('clears delayed ownership markers when tracked terminal state resets', async () => {
let resolveForegroundProcess: (agent: string | null) => void = () => undefined
const foregroundProcess = new Promise<string | null>((resolve) => {
resolveForegroundProcess = resolve
})
const runtime = new OrcaRuntimeService()
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-0' }),
write: () => true,
kill: () => true,
getForegroundProcess: () => foregroundProcess
})
const ptyId = seedWorktree(runtime, 0)
const internals = runtime as unknown as RuntimeInternals
runtime.onPtyData(ptyId, '\x1b]0;⠋ Pi\x07', Date.now())
await vi.advanceTimersByTimeAsync(0)
expect(internals.ptyDelayedForegroundSnapshotTitleObservations.has(ptyId)).toBe(true)
internals.resetTrackedTerminalStateForProviderGeneration(ptyId)
expect(internals.ptyDelayedForegroundSnapshotTitleObservations.has(ptyId)).toBe(false)
resolveForegroundProcess(null)
await vi.advanceTimersByTimeAsync(0)
})
it('renews retained hook status without resetting its state start', () => {
const runtime = new OrcaRuntimeService()
const ptyId = seedWorktree(runtime, 0)
const internals = runtime as unknown as RuntimeInternals
const seededTab = internals.mobileSessionTabsByWorktree.get('workspace-0')?.tabs[0]
if (seededTab?.type !== 'terminal') {
throw new Error('expected seeded terminal')
}
runtime.registerPty(ptyId, 'workspace-0', null, {
tabId: seededTab.parentTabId,
leafId: seededTab.leafId
})
const publications: RuntimeMobileSessionTabsResult[] = []
let viewerState = makeViewerState()
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
publications.push(structuredClone(snapshot))
const patch = applyFreshWebSessionTabsSnapshot(
viewerState,
snapshot,
ENVIRONMENT_ID,
Date.now()
)
viewerState = { ...viewerState, ...patch }
})
runtime.onPtyData(
ptyId,
'\x1b]9999;{"state":"working","prompt":"retained work","agentType":"codex"}\x07',
Date.now()
)
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(1)
const initialTerminal = publications[0]?.tabs[0]
const initialStatus =
initialTerminal?.type === 'terminal' ? initialTerminal.agentStatus : undefined
expect(initialStatus).toMatchObject({ state: 'working', prompt: 'retained work' })
publications.length = 0
vi.advanceTimersByTime(10)
runtime.onPtyData(ptyId, '\x1b]0;⠋ Codex working\x07', Date.now())
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(1)
const titledTerminal = publications[0]?.tabs[0]
const titledStatus =
titledTerminal?.type === 'terminal' ? titledTerminal.agentStatus : undefined
expect(titledStatus).toMatchObject({ state: 'working', prompt: 'retained work' })
expect(titledStatus?.stateStartedAt).toBe(initialStatus?.stateStartedAt)
publications.length = 0
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
runtime.onPtyData(ptyId, '\x1b]0;⠙ Codex working\x07', Date.now())
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(1)
const refreshedTerminal = publications[0]?.tabs[0]
const refreshedStatus =
refreshedTerminal?.type === 'terminal' ? refreshedTerminal.agentStatus : undefined
expect(refreshedStatus).toMatchObject({ state: 'working', prompt: 'retained work' })
expect(refreshedStatus?.updatedAt).toBeGreaterThan(initialStatus!.updatedAt)
expect(refreshedStatus?.stateStartedAt).toBe(initialStatus?.stateStartedAt)
expect(initialStatus!.updatedAt + 1).toBeGreaterThanOrEqual(refreshedStatus!.stateStartedAt)
publications.length = 0
vi.advanceTimersByTime(SESSION_TABS_AGENT_STATUS_HEARTBEAT_INTERVAL_MS)
runtime.onPtyData(ptyId, '\x1b]0;⠹ Codex working\x07', Date.now())
vi.advanceTimersByTime(50)
expect(publications).toHaveLength(1)
const expiredHookTerminal = publications[0]?.tabs[0]
const titleOnlyStatus =
expiredHookTerminal?.type === 'terminal' ? expiredHookTerminal.agentStatus : undefined
expect(titleOnlyStatus).toMatchObject({ state: 'working', prompt: '' })
expect(titleOnlyStatus?.updatedAt).toBeGreaterThan(refreshedStatus!.updatedAt)
expect(titleOnlyStatus?.stateStartedAt).toBe(initialStatus?.stateStartedAt)
expect(Date.now() - initialStatus!.updatedAt).toBeGreaterThan(AGENT_STATUS_STALE_AFTER_MS)
const viewerStatus = Object.values(viewerState.agentStatusByPaneKey)[0]
expect(viewerStatus?.prompt).toBe('')
expect(isExplicitAgentStatusFresh(viewerStatus!, Date.now(), AGENT_STATUS_STALE_AFTER_MS)).toBe(
true
)
unsubscribe()
})
})
@@ -0,0 +1,305 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from '../../src/main/runtime/orca-runtime'
import type {
RuntimeMobileSessionTabsResult,
RuntimeMobileSessionTabsSnapshot
} from '../../src/shared/runtime-types'
const PTY_ID = 'pty-0'
const WORKTREE_ID = 'workspace-0'
const TAB_ID = 'host-tab-0'
const LEAF_ID = '00000000-0000-4000-8000-000000000000'
type TerminalTab = Extract<RuntimeMobileSessionTabsSnapshot['tabs'][number], { type: 'terminal' }>
type RuntimeInternals = {
mobileSessionTabsByWorktree: Map<string, RuntimeMobileSessionTabsSnapshot>
ptysById: Map<string, { lastAgentStatusRichInvalidatedAtEpochMs: number | null }>
resetTrackedTerminalStateForProviderGeneration: (ptyId: string) => void
}
type Harness = {
internals: RuntimeInternals
publications: RuntimeMobileSessionTabsResult[]
runtime: OrcaRuntimeService
tab: TerminalTab
unsubscribe: () => void
}
function createHarness(): Harness {
const runtime = new OrcaRuntimeService()
runtime.registerPty(PTY_ID, WORKTREE_ID)
const tab: TerminalTab = {
type: 'terminal',
id: `${TAB_ID}::${LEAF_ID}`,
parentTabId: TAB_ID,
leafId: LEAF_ID,
ptyId: PTY_ID,
title: 'Cursor Agent',
parentLayout: {
root: { type: 'leaf', leafId: LEAF_ID },
activeLeafId: LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [LEAF_ID]: PTY_ID }
},
isActive: true
}
const internals = runtime as unknown as RuntimeInternals
internals.mobileSessionTabsByWorktree.set(WORKTREE_ID, {
worktree: WORKTREE_ID,
publicationEpoch: 'epoch-1',
snapshotVersion: 1,
activeGroupId: 'group-0',
activeTabId: tab.id,
activeTabType: 'terminal',
tabs: [tab]
})
const publications: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
publications.push(structuredClone(snapshot))
})
return { internals, publications, runtime, tab, unsubscribe }
}
function setRichStatus(
tab: TerminalTab,
args: { agentType?: 'claude' | 'cursor'; prompt: string; at: number }
): void {
tab.agentStatus = {
state: 'working',
prompt: args.prompt,
updatedAt: args.at,
stateStartedAt: args.at,
agentType: args.agentType ?? 'cursor',
paneKey: tab.id,
stateHistory: []
}
}
function emitTitle(harness: Harness, title: string, at = Date.now()): void {
harness.runtime.onPtyData(PTY_ID, `\x1b]0;${title}\x07`, at)
vi.advanceTimersByTime(50)
}
function latestStatus(harness: Harness) {
const terminal = harness.publications.at(-1)?.tabs[0]
return terminal?.type === 'terminal' ? terminal.agentStatus : undefined
}
describe('session-tabs rich-status boundaries', () => {
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())
it('does not renew a previous task across a done-to-working title boundary', () => {
const harness = createHarness()
const firstWorkingAt = Date.now()
setRichStatus(harness.tab, { prompt: 'previous task', at: firstWorkingAt })
emitTitle(harness, '⠋ Cursor Agent', firstWorkingAt)
vi.advanceTimersByTime(1)
emitTitle(harness, 'bash')
harness.publications.length = 0
vi.advanceTimersByTime(1)
const nextWorkingAt = Date.now()
emitTitle(harness, '⠙ Cursor Agent', nextWorkingAt)
expect(latestStatus(harness)).toMatchObject({
state: 'working',
prompt: '',
updatedAt: nextWorkingAt,
stateStartedAt: nextWorkingAt
})
harness.unsubscribe()
})
it('preserves the same task across a neutral title', () => {
const harness = createHarness()
const workingAt = Date.now()
setRichStatus(harness.tab, { prompt: 'current task', at: workingAt })
emitTitle(harness, '⠋ Cursor Agent', workingAt)
harness.publications.length = 0
vi.advanceTimersByTime(1)
emitTitle(harness, 'Terminal')
expect(latestStatus(harness)).toMatchObject({
state: 'working',
prompt: 'current task',
updatedAt: workingAt,
stateStartedAt: workingAt
})
harness.publications.length = 0
vi.advanceTimersByTime(1)
const resumedWorkingAt = Date.now()
emitTitle(harness, '⠙ Cursor Agent', resumedWorkingAt)
expect(latestStatus(harness)).toMatchObject({
state: 'working',
prompt: 'current task',
updatedAt: resumedWorkingAt,
stateStartedAt: workingAt
})
harness.unsubscribe()
})
it('does not resurrect a task after neutral then shell titles', () => {
const harness = createHarness()
setRichStatus(harness.tab, { prompt: 'completed task', at: Date.now() })
emitTitle(harness, '⠋ Cursor Agent')
vi.advanceTimersByTime(1)
emitTitle(harness, 'Terminal')
vi.advanceTimersByTime(1)
emitTitle(harness, 'bash')
harness.publications.length = 0
vi.advanceTimersByTime(1)
const nextWorkingAt = Date.now()
emitTitle(harness, '⠙ Cursor Agent', nextWorkingAt)
expect(latestStatus(harness)).toMatchObject({
state: 'working',
prompt: '',
updatedAt: nextWorkingAt,
stateStartedAt: nextWorkingAt
})
harness.unsubscribe()
})
it('does not carry a stale question into the next title interval', () => {
const harness = createHarness()
const firstWorkingAt = Date.now()
harness.tab.agentStatus = {
state: 'waiting',
prompt: 'previous question',
interactivePrompt: '{"question":"previous question"}',
updatedAt: firstWorkingAt,
stateStartedAt: firstWorkingAt,
agentType: 'cursor',
paneKey: harness.tab.id,
stateHistory: []
}
emitTitle(harness, 'Cursor Agent needs confirmation', firstWorkingAt)
vi.advanceTimersByTime(1)
emitTitle(harness, 'bash')
harness.publications.length = 0
vi.advanceTimersByTime(1)
emitTitle(harness, '⠋ Cursor Agent')
expect(latestStatus(harness)).toMatchObject({ state: 'working', prompt: '' })
expect(latestStatus(harness)).not.toHaveProperty('interactivePrompt')
harness.unsubscribe()
})
it('keeps a renderer question visible under a shell title until the next agent interval', () => {
const harness = createHarness()
const questionAt = Date.now()
harness.tab.agentStatus = {
state: 'waiting',
prompt: 'current question',
interactivePrompt: '{"question":"current question"}',
updatedAt: questionAt,
stateStartedAt: questionAt,
agentType: 'cursor',
paneKey: harness.tab.id,
stateHistory: []
}
emitTitle(harness, 'bash')
expect(latestStatus(harness)).toMatchObject({
state: 'waiting',
interactivePrompt: '{"question":"current question"}'
})
harness.publications.length = 0
vi.advanceTimersByTime(1)
emitTitle(harness, '⠋ Cursor Agent')
expect(latestStatus(harness)).toMatchObject({ state: 'working', prompt: '' })
expect(latestStatus(harness)).not.toHaveProperty('interactivePrompt')
harness.unsubscribe()
})
it('keeps a retained OSC question visible under a shell title until the next agent interval', () => {
const harness = createHarness()
harness.runtime.registerPty(PTY_ID, WORKTREE_ID, null, {
tabId: harness.tab.parentTabId,
leafId: harness.tab.leafId
})
harness.runtime.onPtyData(
PTY_ID,
'\x1b]9999;{"state":"waiting","prompt":"current question","interactivePrompt":"question-payload","agentType":"cursor"}\x07',
Date.now()
)
emitTitle(harness, 'bash')
expect(latestStatus(harness)).toMatchObject({
state: 'waiting',
interactivePrompt: 'question-payload'
})
harness.publications.length = 0
vi.advanceTimersByTime(1)
emitTitle(harness, '⠋ Cursor Agent')
expect(latestStatus(harness)).toMatchObject({ state: 'working', prompt: '' })
expect(latestStatus(harness)).not.toHaveProperty('interactivePrompt')
harness.unsubscribe()
})
it('does not resurrect retained status after an identity-only owner change', () => {
const harness = createHarness()
harness.runtime.registerPty(PTY_ID, WORKTREE_ID, null, {
tabId: harness.tab.parentTabId,
leafId: harness.tab.leafId
})
harness.runtime.onPtyData(
PTY_ID,
'\x1b]9999;{"state":"working","prompt":"retained task","agentType":"codex"}\x07',
Date.now()
)
vi.advanceTimersByTime(50)
emitTitle(harness, '⠋ Codex working')
harness.publications.length = 0
vi.advanceTimersByTime(1)
emitTitle(harness, 'Cursor Agent')
expect(latestStatus(harness)).toMatchObject({ state: 'done', prompt: '' })
harness.unsubscribe()
})
it('does not renew rich status from a replaced provider generation', () => {
const harness = createHarness()
const firstWorkingAt = Date.now()
setRichStatus(harness.tab, { prompt: 'previous provider task', at: firstWorkingAt })
emitTitle(harness, '⠋ Cursor Agent', firstWorkingAt)
harness.tab.agentStatus!.updatedAt = Date.now()
harness.internals.resetTrackedTerminalStateForProviderGeneration(PTY_ID)
harness.publications.length = 0
vi.advanceTimersByTime(1)
const replacementWorkingAt = Date.now()
emitTitle(harness, '⠙ Cursor Agent', replacementWorkingAt)
expect(latestStatus(harness)).toMatchObject({
state: 'working',
prompt: '',
updatedAt: replacementWorkingAt,
stateStartedAt: replacementWorkingAt
})
harness.unsubscribe()
})
it('keeps invalidation across an identity-only owner change', () => {
const harness = createHarness()
setRichStatus(harness.tab, {
prompt: 'previous owner task',
at: Date.now(),
agentType: 'claude'
})
emitTitle(harness, '⠋ Claude working')
harness.publications.length = 0
vi.advanceTimersByTime(1)
const identityChangeAt = Date.now()
emitTitle(harness, 'Cursor Agent', identityChangeAt)
expect(harness.internals.ptysById.get(PTY_ID)?.lastAgentStatusRichInvalidatedAtEpochMs).toBe(
identityChangeAt
)
expect(latestStatus(harness)).toBeUndefined()
harness.unsubscribe()
})
})