refactor(workspace-layout): organize concrete domain owners (#41350)

* refactor(workspace-layout): organize concrete domain owners

* test(layout): retire obsolete legacy-leaf publication setup
This commit is contained in:
Neil
2026-11-11 20:54:34 -08:00
committed by GitHub
parent bbe22681c2
commit e902c9a65b
508 changed files with 1291 additions and 11687 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
uses: ./.github/workflows/e2e.yml
with:
ref: ${{ inputs.ref || github.sha }}
test_files: '["tests/e2e/workspace-layout-oracle-headless.spec.ts","tests/e2e/workspace-layout-oracle.spec.ts","tests/e2e/cli-file-open-position.spec.ts","tests/e2e/relaunch-first-terminal.spec.ts","tests/e2e/relaunch-sleeping-agents-resume-on-view.spec.ts","tests/e2e/cli-first-worktree-single-terminal.spec.ts","tests/e2e/unopened-workspace-agent-single-resume.spec.ts","tests/e2e/terminal-pane-close-layout-consistency.spec.ts","tests/e2e/golden-file-open-edit-save.spec.ts","tests/e2e/golden-source-control-open-diff.spec.ts","tests/e2e/paired-cli-terminal-graph-sync-tab-retention.spec.ts","tests/e2e/floating-workspace-pane-height.spec.ts","tests/e2e/editor-model-host-isolation.spec.ts","tests/e2e/editor-tab-selection-restore.spec.ts","tests/e2e/finished-agent-ghost-resume.spec.ts","tests/e2e/live-background-terminal-mount-authority.spec.ts","tests/e2e/workspace-back-forward-navigation.spec.ts","tests/e2e/worktree-lineage-toggle-shortcut.spec.ts","tests/e2e/local-cli-terminal-retention.spec.ts","tests/e2e/default-tabs-mark-crash-relaunch.spec.ts","tests/e2e/terminal-cli-split-closed-before-mount.spec.ts","tests/e2e/layout-stream-loss-reopen.spec.ts","tests/e2e/terminal-tab-close-restart-persistence.spec.ts","tests/e2e/artificial-opencode-terminal-load.spec.ts","tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts","tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts","tests/e2e/headless-paired-remote-terminal-stall-recovery.spec.ts","tests/e2e/headless-paired-remote-terminal-viewer-size.spec.ts","tests/e2e/headless-serve-browser-tab-restart.spec.ts","tests/e2e/headless-serve-cli-terminal-retention-parity.spec.ts","tests/e2e/headless-serve-desktop-activation.spec.ts","tests/e2e/headless-serve-chat-tab-placement.spec.ts","tests/e2e/headless-serve-mobile-native-chat-identity.spec.ts","tests/e2e/layout-compat-old-phone-on-new-server.spec.ts","tests/e2e/layout-compat-serve-restart-keeps-at-at-terminals.spec.ts","tests/e2e/paired-mobile-serve-restart-terminal-tabs.spec.ts","tests/e2e/paired-mobile-desktop-tab-composition.spec.ts","tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts","tests/e2e/paired-desktop-server-layout-frames.spec.ts","tests/e2e/paired-desktop-server-held-pane-reopen.spec.ts","tests/e2e/paired-desktop-server-layout-intents.spec.ts","tests/e2e/paired-desktop-server-layout-writes.spec.ts","tests/e2e/ssh-orcad-"]'
test_files: '["tests/e2e/workspace-layout-oracle-headless.spec.ts","tests/e2e/workspace-layout-oracle.spec.ts","tests/e2e/cli-file-open-position.spec.ts","tests/e2e/relaunch-first-terminal.spec.ts","tests/e2e/relaunch-sleeping-agents-resume-on-view.spec.ts","tests/e2e/cli-first-worktree-single-terminal.spec.ts","tests/e2e/unopened-workspace-agent-single-resume.spec.ts","tests/e2e/terminal-pane-close-layout-consistency.spec.ts","tests/e2e/golden-file-open-edit-save.spec.ts","tests/e2e/golden-source-control-open-diff.spec.ts","tests/e2e/paired-cli-terminal-graph-sync-tab-retention.spec.ts","tests/e2e/floating-workspace-pane-height.spec.ts","tests/e2e/editor-model-host-isolation.spec.ts","tests/e2e/editor-tab-selection-restore.spec.ts","tests/e2e/finished-agent-ghost-resume.spec.ts","tests/e2e/live-background-terminal-mount-authority.spec.ts","tests/e2e/workspace-back-forward-navigation.spec.ts","tests/e2e/worktree-lineage-toggle-shortcut.spec.ts","tests/e2e/local-cli-terminal-retention.spec.ts","tests/e2e/default-tabs-mark-crash-relaunch.spec.ts","tests/e2e/terminal-cli-split-closed-before-mount.spec.ts","tests/e2e/layout-stream-loss-reopen.spec.ts","tests/e2e/terminal-tab-close-restart-persistence.spec.ts","tests/e2e/artificial-opencode-terminal-load.spec.ts","tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts","tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts","tests/e2e/headless-paired-remote-terminal-stall-recovery.spec.ts","tests/e2e/headless-paired-remote-terminal-viewer-size.spec.ts","tests/e2e/headless-serve-browser-tab-restart.spec.ts","tests/e2e/headless-serve-cli-terminal-retention-parity.spec.ts","tests/e2e/headless-serve-desktop-activation.spec.ts","tests/e2e/headless-serve-chat-tab-placement.spec.ts","tests/e2e/headless-serve-mobile-native-chat-identity.spec.ts","tests/e2e/layout-compat-old-phone-on-new-server.spec.ts","tests/e2e/layout-compat-serve-restart-keeps-at-at-terminals.spec.ts","tests/e2e/paired-mobile-serve-restart-terminal-tabs.spec.ts","tests/e2e/paired-mobile-desktop-tab-composition.spec.ts","tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts","tests/e2e/ssh-orcad-"]'
run_changed_e2e: true
needs_build: true
artifact_suffix: -layout-owner-switch-on
-5
View File
@@ -733,7 +733,6 @@
1 test-timeout-needs-reason:tests/e2e/daemon-generation-legacy-close-safety.spec.ts
2 test-timeout-needs-reason:tests/e2e/daemon-generation-reconnect-safety.spec.ts
2 test-timeout-needs-reason:tests/e2e/default-tabs-mark-crash-relaunch.spec.ts
1 test-timeout-needs-reason:tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts
2 test-timeout-needs-reason:tests/e2e/desktop-window-mobile-markdown.spec.ts
1 test-timeout-needs-reason:tests/e2e/floating-tab-rename.spec.ts
1 test-timeout-needs-reason:tests/e2e/golden-quit-relaunch-session.spec.ts
@@ -789,10 +788,6 @@
1 test-timeout-needs-reason:tests/e2e/paired-client-hosted-profile-cookies.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-client-project-removal-stops-host-terminals.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-cmd-j-host-qualified-tabs.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-desktop-server-held-pane-reopen.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-desktop-server-layout-frames.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-desktop-server-layout-intents.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-desktop-server-layout-writes.spec.ts
2 test-timeout-needs-reason:tests/e2e/paired-host-usage-credentials.spec.ts
7 test-timeout-needs-reason:tests/e2e/paired-instant-browser-tab.spec.ts
1 test-timeout-needs-reason:tests/e2e/paired-mobile-desktop-mirror-files-git-review.spec.ts
+6 -9
View File
@@ -5951,35 +5951,32 @@
"providers": ["local", "local-daemon", "ssh", "wsl", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local-daemon"],
"coverageNotes": "Audit1925 (sources1922/1923): Partial ownership suite loses fixed first-leaf/rootless/dangling-selection/idempotence/identity examples. Both existing active assertion aliases stay backed by exact retained active-leaf metadata pruning and idempotence across PTY/focus permutation callbacks; metadata collision and repeated-leaf challenges survive. No deleted fixed example is assigned replacement coverage. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Component unit tests cover persisted replay, rootless layouts, repeated leaf ids, metadata repair, and 12,000 nested duplicate leaves; retained store tests cover live/future pane-authority transfer and a prior detach destination. A two-launch macOS Electron journey covers a real surviving local-daemon PTY, persisted duplicate ownership, synchronized streaming output, and tab hide/reveal. Live Linux, Windows, SSH, WSL, and remote-runtime restore journeys remain gaps. The audit removed a mocked duplicate-replay wrapper suite. Its callback-count alarms are no longer covered here; retained ownership tests and historical Electron evidence do not establish equivalent wrapper coverage. The broad mirror fixture no longer supplies the duplicate-remote-PTY assertion referenced here. The terminal-mirroring fixture also lost its separate duplicate-PTY example and now retains host-finalized pending-binding composition only. Duplicate-PTY and topology fixture alarms are lost; remaining gate evidence is not replacement coverage. The test-value audit removed store duplicate/layout-only, hydration, legacy-leaf, and cross-tab layout-state examples. Their direct normalization, hydration authority, legacy compatibility, and cross-tab preservation alarms are lost. The retained store cases exercise live/future authority transfer and a prior detach destination; component and historical Electron evidence do not establish equivalent replacement coverage.",
"coverageNotes": "Current adopted test policy at native 6f2e: retire the sampled PTY/focus permutation callback and its assertion reference. This loses its ownership-uniqueness, rooted-focus, immutability and idempotence alarms over those combinations. Eleven direct component callbacks and the depth/store/E2E file selectors remain; no equivalent replacement or fresh execution is claimed. Previous metadata account (preserved verbatim): Audit1925 (sources1922/1923): Partial ownership suite loses fixed first-leaf/rootless/dangling-selection/idempotence/identity examples. Both existing active assertion aliases stay backed by exact retained active-leaf metadata pruning and idempotence across PTY/focus permutation callbacks; metadata collision and repeated-leaf challenges survive. No deleted fixed example is assigned replacement coverage. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Component unit tests cover persisted replay, rootless layouts, repeated leaf ids, metadata repair, and 12,000 nested duplicate leaves; retained store tests cover live/future pane-authority transfer and a prior detach destination. A two-launch macOS Electron journey covers a real surviving local-daemon PTY, persisted duplicate ownership, synchronized streaming output, and tab hide/reveal. Live Linux, Windows, SSH, WSL, and remote-runtime restore journeys remain gaps. The audit removed a mocked duplicate-replay wrapper suite. Its callback-count alarms are no longer covered here; retained ownership tests and historical Electron evidence do not establish equivalent wrapper coverage. The broad mirror fixture no longer supplies the duplicate-remote-PTY assertion referenced here. The terminal-mirroring fixture also lost its separate duplicate-PTY example and now retains host-finalized pending-binding composition only. Duplicate-PTY and topology fixture alarms are lost; remaining gate evidence is not replacement coverage. The test-value audit removed store duplicate/layout-only, hydration, legacy-leaf, and cross-tab layout-state examples. Their direct normalization, hydration authority, legacy compatibility, and cross-tab preservation alarms are lost. The retained store cases exercise live/future authority transfer and a prior detach destination; component and historical Electron evidence do not establish equivalent replacement coverage.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/11757",
"https://github.com/stablyai/orca/pull/11726"
],
"invariant": "Within one terminal tab, each PTY has at most one layout leaf, pane manager surface, and xterm renderer owner. Restore and remote mirroring must normalize duplicate ownership before replay while retaining active-leaf focus, scrollback, pane authority, agent metadata, and distinct sibling PTYs.",
"oracle": "Audit1925 (sources1922/1923): Partial ownership suite loses fixed first-leaf/rootless/dangling-selection/idempotence/identity examples. Both existing active assertion aliases stay backed by exact retained active-leaf metadata pruning and idempotence across PTY/focus permutation callbacks; metadata collision and repeated-leaf challenges survive. No deleted fixed example is assigned replacement coverage. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Launch Orca with a real daemon PTY running a synchronized full-screen stream, close the desktop client without killing the daemon process, seed the persisted tab layout with two leaves bound to that PTY, and relaunch. Hide the restored tab behind a sibling and reveal it, then require the stream to remain visible with exactly one manager pane, one xterm DOM node, one root leaf, one PTY binding, and one unique PTY. Component contracts cover persisted replay, rootless and repeated-leaf layouts, metadata repair, and deeply nested input. Retained store contracts cover live/future authority transfer onto a retained leaf and preservation of a prior detach destination.",
"oracle": "Current adopted test policy at native 6f2e: retire the sampled PTY/focus permutation callback and its assertion reference. This loses its ownership-uniqueness, rooted-focus, immutability and idempotence alarms over those combinations. Eleven direct component callbacks and the depth/store/E2E file selectors remain; no equivalent replacement or fresh execution is claimed. Previous metadata account (preserved verbatim): Audit1925 (sources1922/1923): Partial ownership suite loses fixed first-leaf/rootless/dangling-selection/idempotence/identity examples. Both existing active assertion aliases stay backed by exact retained active-leaf metadata pruning and idempotence across PTY/focus permutation callbacks; metadata collision and repeated-leaf challenges survive. No deleted fixed example is assigned replacement coverage. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Launch Orca with a real daemon PTY running a synchronized full-screen stream, close the desktop client without killing the daemon process, seed the persisted tab layout with two leaves bound to that PTY, and relaunch. Hide the restored tab behind a sibling and reveal it, then require the stream to remain visible with exactly one manager pane, one xterm DOM node, one root leaf, one PTY binding, and one unique PTY. Component contracts cover persisted replay, rootless and repeated-leaf layouts, metadata repair, and deeply nested input. Retained store contracts cover live/future authority transfer onto a retained leaf and preservation of a prior detach destination.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/shared/workspace-layout/terminal-layout-pty-ownership-depth.test.ts src/renderer/src/components/terminal-pane/layout/terminal-layout-pty-ownership.test.ts src/renderer/src/store/terminals/terminal-layout-pty-ownership.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/workspace-layout/terminal-topology/terminal-layout-pty-ownership-depth.test.ts src/renderer/src/components/terminal-pane/layout/terminal-layout-pty-ownership.test.ts src/renderer/src/store/terminals/terminal-layout-pty-ownership.test.ts --reporter=dot",
"pnpm exec electron-vite build --mode e2e",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/terminal-duplicate-pty-renderer-reveal.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
],
"testFiles": [
"src/shared/workspace-layout/terminal-layout-pty-ownership-depth.test.ts",
"src/shared/workspace-layout/terminal-topology/terminal-layout-pty-ownership-depth.test.ts",
"src/renderer/src/components/terminal-pane/layout/terminal-layout-pty-ownership.test.ts",
"src/renderer/src/store/terminals/terminal-layout-pty-ownership.test.ts",
"tests/e2e/terminal-duplicate-pty-renderer-reveal.spec.ts"
],
"assertionRefs": [
{
"file": "src/shared/workspace-layout/terminal-layout-pty-ownership-depth.test.ts",
"file": "src/shared/workspace-layout/terminal-topology/terminal-layout-pty-ownership-depth.test.ts",
"assertions": ["prunes deeply nested duplicate ownership without recursive stack growth"]
},
{
"file": "src/renderer/src/components/terminal-pane/layout/terminal-layout-pty-ownership.test.ts",
"assertions": [
"keeps the active leaf and prunes the stale surface plus its metadata",
"is idempotent and preserves one owner across PTY and focus permutations"
]
"assertions": ["keeps the active leaf and prunes the stale surface plus its metadata"]
},
{
"file": "src/renderer/src/store/terminals/terminal-layout-pty-ownership.test.ts",
@@ -2647,7 +2647,6 @@
"src/main/persistence/terminal-topology/persistence-tab-terminal-pane-binding-load.test.ts": 1795,
"src/main/persistence/terminal-topology/terminal-close-write-failure-store.test.ts": 2870,
"src/main/persistence/terminal-topology/terminal-close-write-failure-switch-off.test.ts": 2027,
"src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts": 1808,
"src/main/persistence/terminal-topology/terminal-layout-apply.test.ts": 2005,
"src/main/persistence/terminal-topology/terminal-layout-create-resend.test.ts": 1773,
"src/main/persistence/terminal-topology/terminal-layout-create.test.ts": 2107,
@@ -2686,7 +2685,6 @@
"src/main/persistence/tracking-repos/repo-update-host-guard.test.ts": 478,
"src/main/persistence/tracking-repos/worktree-identity-migration-field-coverage.test.ts": 505,
"src/main/persistence/tracking-repos/worktree-metadata-normalization.test.ts": 372,
"src/main/persistence/window-save-reconnect-facts.test.ts": 372,
"src/main/persistence/window-save-sleep-records.test.ts": 60,
"src/main/pi/managed-extensions/agent-status/agent-status-completion-delivery.test.ts": 423,
"src/main/pi/managed-extensions/agent-status/agent-status-extension-nested-run-settlement.test.ts": 335,
@@ -3021,7 +3019,6 @@
"src/main/runtime/session-graph/graph-sync-deletion-fence.test.ts": 4552,
"src/main/runtime/session-tabs/graph-sync-mobile-snapshot-gating.test.ts": 4333,
"src/main/runtime/terminal/surface/headless-create-terminal-handle-main.test.ts": 3873,
"src/main/runtime/session-tabs/headless-move-leaf-to-new-tab.test.ts": 3596,
"src/main/runtime/terminal/output/headless-terminal-dispose-write-ordering.test.ts": 4309,
"src/main/runtime/terminal/recovery/held-pane-reopen-layout-owner.test.ts": 5093,
"src/main/runtime/terminal/wait/hermes-slash-readiness-transcript.test.ts": 4931,
@@ -3088,7 +3085,6 @@
"src/main/runtime/terminal/surface/orca-runtime-cold-restore-terminal-membership.test.ts": 3716,
"src/main/runtime/terminal/wait/orca-runtime-exact-worker-provider-session.test.ts": 3617,
"src/main/runtime/terminal/lifecycle/orca-runtime-exited-split-pane-close.test.ts": 4235,
"src/main/runtime/terminal/surface/orca-runtime-failed-start-pane-layout-owner.test.ts": 4204,
"src/main/runtime/files/orca-runtime-files-preview-budget.test.ts": 672,
"src/main/runtime/files/search/orca-runtime-files-search.test.ts": 675,
"src/main/runtime/terminal/artifacts/orca-runtime-files-terminal-artifact-grants.test.ts": 580,
@@ -3596,7 +3592,6 @@
"src/main/runtime/session-lifecycle/projection/sleep-record-pane-claim.test.ts": 48,
"src/main/runtime/session-tabs/session-tab-close-force.test.ts": 7089,
"src/main/runtime/session-tabs/runtime-inventory-publication-authority.test.ts": 5775,
"src/main/runtime/session-tabs/headless-mobile-session-absorbed-rows.test.ts": 4223,
"src/main/runtime/session-tabs/main-layout-session-tab-move.test.ts": 4233,
"src/main/runtime/session-tabs/session-tab-prototype-dispatch.test.ts": 4888,
"src/main/runtime/session-tabs/session-tabs-headless-main-change.test.ts": 4320,
@@ -3693,8 +3688,6 @@
"src/main/runtime/terminal/input/agent-prompt-permission-hold.test.ts": 48,
"src/main/runtime/terminal/input/terminal-send-plugin-hook.test.ts": 169,
"src/main/runtime/terminal/launch/mobile-terminal-chat-fork.test.ts": 3900,
"src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts": 4503,
"src/main/runtime/terminal/launch/terminal-split-retry.test.ts": 4038,
"src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts": 5572,
"src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts": 4871,
"src/main/runtime/terminal/lifecycle/ssh-exit-code-pane-outcome.test.ts": 4334,
@@ -4102,7 +4095,6 @@
"src/main/window/runtime-window-lifecycle-terminal-reveal.test.ts": 100,
"src/main/window/runtime-window-pane-title-notification.test.ts": 97,
"src/main/window/session-tab-close-request-relay.test.ts": 74,
"src/main/window/terminal-pane-reveal-relay.test.ts": 91,
"src/main/window/terminal-tab-close-request-relay.test.ts": 64,
"src/main/window/updater-package-recovery-ipc.test.ts": 169,
"src/main/pty/process-retirement/windows-live-tree-kill.win32.test.ts": 76,
@@ -4273,7 +4265,6 @@
"src/preload/updater-package-recovery.test.ts": 1102,
"src/preload/usage-provider-api.test.ts": 46,
"src/renderer/src/app-shell/shutdown-checkpoint-persist.test.ts": 65,
"src/renderer/src/app-shell/startup-window-parity.test.ts": 2306,
"src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx": 355,
"src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx": 293,
"src/renderer/src/app-shell/visible-review-reporting.test.ts": 1872,
@@ -5221,7 +5212,6 @@
"src/renderer/src/components/tab-bar/host-agent-launch-preferences-source.test.tsx": 3154,
"src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts": 438,
"src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts": 206,
"src/renderer/src/components/tab-bar/tab-context-menu.test.tsx": 5620,
"src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts": 510,
"src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts": 2614,
"src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts": 2348,
@@ -5239,7 +5229,6 @@
"src/renderer/src/components/tab-group/tab-drag-retained-guest-passthrough.test.tsx": 2104,
"src/renderer/src/components/tab-group/tab-group-body-geometry.test.ts": 194,
"src/renderer/src/components/tab-group/tab-group-layout-owner-commands.test.ts": 1764,
"src/renderer/src/components/tab-group/tab-moves-drawn-from-frames.test.ts": 2105,
"src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.test.tsx": 2023,
"src/renderer/src/components/tab-group/workspace-tab-close-commands.frame-flush.test.ts": 1604,
"src/renderer/src/components/task-page/github/task-page-github-landing-refresh-record.test.tsx": 971,
@@ -5293,7 +5282,6 @@
"src/renderer/src/components/terminal-pane/provider-notices/claude-diff-panel/claude-diff-panel-notice-banner.test.tsx": 1986,
"src/renderer/src/components/terminal-pane/notifications/claude-side-question-completion.test.ts": 2305,
"src/renderer/src/components/terminal-pane/provider-notices/codex-backfill/codex-backfill-error-detector.test.ts": 26,
"src/renderer/src/components/terminal-pane/account-restart/detached/codex-detached-pane-restart.test.ts": 2270,
"src/renderer/src/components/terminal-pane/provider-notices/codex-server-isolation/codex-shared-server-banner.test.tsx": 2766,
"src/renderer/src/components/terminal-pane/pty-connection/output/deferred-reattach-live-data-queue.test.ts": 184,
"src/renderer/src/components/terminal-pane/layout/split/deferred-split-pane-handoff.test.ts": 78,
@@ -5340,7 +5328,6 @@
"src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-restore-and-resume.test.ts": 19401,
"src/renderer/src/components/terminal-pane/pty-connection/output-restore/pty-connection-restored-baseline-shortfall.test.ts": 1902,
"src/renderer/src/components/terminal-pane/pty-connection/output-restore/pty-connection-visible-pane-output-pause-latch.test.ts": 1639,
"src/renderer/src/components/terminal-pane/pty-connection/reattach/deferred-session-reattach-newer-daemon-hold.test.ts": 2570,
"src/renderer/src/components/terminal-pane/pty-connection/reattach/deferred-session-reattach-startup-snapshot.test.ts": 1830,
"src/renderer/src/components/terminal-pane/pty-connection/reattach/deferred-session-reattach-wsl-distro-stopped-hold.test.ts": 2517,
"src/shared/terminal/protocol/escape/terminal-synchronized-output-socket-splits.test.ts": 92,
@@ -5443,7 +5430,7 @@
"src/renderer/src/components/terminal-pane/input/ime/terminal-ios-hangul-preedit-device-trace.test.ts": 415,
"src/renderer/src/components/terminal-pane/input/ime/terminal-ios-hangul-preedit.test.ts": 818,
"src/renderer/src/components/terminal-pane/input/keyboard/terminal-keyboard-native-chat-cover.test.ts": 2084,
"src/shared/workspace-layout/terminal-layout-pty-ownership-depth.test.ts": 82,
"src/shared/workspace-layout/terminal-topology/terminal-layout-pty-ownership-depth.test.ts": 82,
"src/renderer/src/components/terminal-pane/layout/terminal-layout-pty-ownership.test.ts": 99,
"src/renderer/src/components/terminal-pane/links/terminal-link-click-fallback.test.ts": 985,
"src/renderer/src/components/terminal-pane/links/terminal-link-open-beside-source.test.ts": 947,
@@ -5801,7 +5788,6 @@
"src/renderer/src/lib/workspace/activation/worktree-activation-launch-awaiting-host.test.ts": 2161,
"src/renderer/src/lib/workspace/activation/worktree-activation-structured-chat-surface.test.ts": 2043,
"src/renderer/src/lib/workspace/activation/worktree-activation-web-runtime.test.ts": 2768,
"src/renderer/src/lib/workspace/activation/worktree-agent-activation-seam.test.ts": 3440,
"src/renderer/src/lib/workspace/creation/worktree-creation-initial-surface.test.ts": 5258,
"src/renderer/src/lib/workspace/creation/worktree-creation-structured-session-prompts.test.tsx": 4554,
"src/renderer/src/lib/workspace/ownership/worktree-live-terminal-surface-owners.test.ts": 266,
@@ -5817,20 +5803,17 @@
"src/renderer/src/runtime/rpc/calls/abortable-runtime-environment-call.test.ts": 189,
"src/renderer/src/runtime/provider-accounts/observations/claude-agent-view-observations.test.ts": 105,
"src/renderer/src/runtime/web-session-tabs-sync/closing/close-mirrored-editor-tab.test.ts": 397,
"src/renderer/src/runtime/workspace-layout/frames/divider-ratio-refusal.test.ts": 2212,
"src/renderer/src/runtime/sync-runtime-graph/editor-buffer-dirty-readers.test.ts": 1892,
"src/renderer/src/runtime/files/mutations/file-explorer-delete-owner-provenance.test.ts": 1878,
"src/renderer/src/runtime/native-chat/model-catalogs/host-model-catalog-snapshots.test.ts": 2348,
"src/renderer/src/runtime/web-session-tabs-sync/hydration/host-session-mirror-hydration-frame-ordering.test.tsx": 2469,
"src/renderer/src/runtime/web-session-tabs-sync/hydration/host-session-mirror-settle-receipt-frames.test.tsx": 2130,
"src/renderer/src/runtime/hosted-terminals/hydration/host-terminal-scrollback-hydration.test.ts": 467,
"src/renderer/src/runtime/workspace-layout/frames/layout-changes-during-stream-outage.test.ts": 1948,
"src/renderer/src/runtime/workspace-layout/commands/layout-owner-command.test.ts": 549,
"src/renderer/src/runtime/native-chat/naming/legacy-chat-rename-migration.test.tsx": 3834,
"src/renderer/src/runtime/web-session-tabs-sync/agent-status/mirrored-agent-status-clock-skew.test.ts": 1839,
"src/renderer/src/runtime/web-session-tabs-sync/closing/paired-terminal-tab-close-environment-removal.test.ts": 1897,
"src/renderer/src/runtime/web-session-tabs-sync/closing/paired-terminal-tab-close-reload.test.ts": 1943,
"src/renderer/src/runtime/workspace-layout/frames/pane-titles-drawn-from-frames.test.ts": 1988,
"src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts": 55,
"src/renderer/src/runtime/hosted-terminals/snapshots/remote-runtime-snapshot-outcome.test.ts": 233,
"src/renderer/src/runtime/hosted-terminals/streams/remote-runtime-terminal-frame-drop-resync.test.ts": 266,
@@ -5850,15 +5833,12 @@
"src/renderer/src/runtime/hosts/connections/runtime-host-contact-regained.test.ts": 413,
"src/renderer/src/runtime/jira/runtime-jira-image-client.test.ts": 214,
"src/renderer/src/runtime/jira/runtime-jira-payload-stream.test.ts": 192,
"src/renderer/src/runtime/workspace-layout/frames/runtime-layout-client.test.ts": 165,
"src/renderer/src/runtime/files/reads/runtime-paged-file-read.test.ts": 1705,
"src/renderer/src/runtime/files/search/runtime-quick-open-version-compatibility.test.ts": 451,
"src/renderer/src/runtime/rpc/calls/runtime-rpc-client.test.ts": 342,
"src/renderer/src/runtime/files/grants/runtime-terminal-artifact-write.test.ts": 567,
"src/renderer/src/runtime/hosted-terminals/inspection/runtime-terminal-inspection.test.ts": 2033,
"src/renderer/src/runtime/hosted-terminals/streams/runtime-terminal-stream.test.ts": 449,
"src/renderer/src/runtime/workspace-layout/server/server-held-panes.test.tsx": 2254,
"src/renderer/src/runtime/workspace-layout/server/server-layout-apply.test.ts": 2222,
"src/renderer/src/runtime/workspace-layout/server/server-layout-editor-handover.test.ts": 5075,
"src/renderer/src/runtime/workspace-layout/server/server-layout-frames-handover.test.ts": 1991,
"src/renderer/src/runtime/workspace-layout/server/server-layout-frames.test.ts": 2053,
@@ -5876,7 +5856,6 @@
"src/renderer/src/runtime/sync-runtime-graph/terminal-group-grid-sizes.test.ts": 194,
"src/renderer/src/runtime/browser/creation/web-runtime-browser-creation-placement.test.ts": 1927,
"src/renderer/src/runtime/browser/creation/web-runtime-browser-link-open.test.ts": 2217,
"src/renderer/src/runtime/web-session-tabs-sync/closing/web-session-close-replay.test.ts": 1119,
"src/renderer/src/runtime/web-session-tabs-sync/identity/web-session-restored-placeholder-identity.test.ts": 985,
"src/renderer/src/runtime/web-session-tabs-sync/agent-status/web-session-tabs-agent-completion-notifications.test.ts": 1953,
"src/renderer/src/runtime/web-session-tabs-sync/tracking/web-session-tabs-removed-frame-retires-live-publisher.test.ts": 854,
@@ -5905,9 +5884,6 @@
"src/renderer/src/runtime/hosted-terminals/recovery/panes/web-session-terminal-pending-handle-recovery.test.ts": 723,
"src/renderer/src/runtime/hosted-terminals/recovery/evidence/web-session-terminal-retirement-proof-ledger.test.ts": 685,
"src/renderer/src/runtime/rpc/subscriptions/window-visibility-subscription-parking-stream-end.test.ts": 225,
"src/renderer/src/runtime/workspace-layout/frames/workspace-layout-frame-binding.test.ts": 2155,
"src/renderer/src/runtime/workspace-layout/frames/workspace-layout-frame-held-tabs.test.ts": 2519,
"src/renderer/src/runtime/workspace-layout/frames/workspace-layout-frame-sync.test.ts": 2282,
"src/renderer/src/ssh/node-dist-mirror-recovery.test.ts": 1770,
"src/renderer/src/ssh/ssh-connect-in-flight.test.ts": 62,
"src/renderer/src/startup/startup-degraded-recovery.test.ts": 574,
@@ -6981,49 +6957,40 @@
"src/shared/workspace-layout/old-client/old-client-pane-layout-translation.test.ts": 363,
"src/shared/workspace-layout/old-client/old-client-session-tabs-projection-owned-editor.test.ts": 178,
"src/shared/workspace-layout/old-client/old-client-session-tabs-projection.test.ts": 207,
"src/shared/workspace-layout/tab-group-moves.test.ts": 52,
"src/shared/workspace-layout/tab-order-moves.test.ts": 44,
"src/shared/workspace-layout/workspace-layout-agent-session-tab.test.ts": 168,
"src/shared/workspace-layout/workspace-layout-browser-commands.test.ts": 177,
"src/shared/workspace-layout/workspace-layout-cold-local-window.test.ts": 319,
"src/shared/workspace-layout/workspace-layout-command-persistence.test.ts": 329,
"src/shared/workspace-layout/workspace-layout-commands.test.ts": 215,
"src/shared/workspace-layout/workspace-layout-default-tabs.test.ts": 159,
"src/shared/workspace-layout/workspace-layout-dropped-leaves.test.ts": 174,
"src/shared/workspace-layout/workspace-layout-editor-duplicates.test.ts": 312,
"src/shared/workspace-layout/workspace-layout-editor-file-owner.test.ts": 349,
"src/shared/workspace-layout/workspace-layout-editor-open-file-id.test.ts": 162,
"src/shared/workspace-layout/workspace-layout-editor-open.test.ts": 166,
"src/shared/workspace-layout/workspace-layout-editor-rekey.test.ts": 169,
"src/shared/workspace-layout/workspace-layout-fact-locations.test.ts": 449,
"src/shared/workspace-layout/workspace-layout-initial-surface.test.ts": 163,
"src/shared/workspace-layout/workspace-layout-legacy-persistence.test.ts": 332,
"src/shared/workspace-layout/workspace-layout-load-browser-repair.test.ts": 313,
"src/shared/workspace-layout/workspace-layout-load-check-details.test.ts": 286,
"src/shared/workspace-layout/workspace-layout-load-duplicate-pty.test.ts": 340,
"src/shared/workspace-layout/workspace-layout-load-normalization.test.ts": 470,
"src/shared/workspace-layout/workspace-layout-load-synthetic-pane-title.test.ts": 324,
"src/shared/workspace-layout/workspace-layout-load-view-less-tabs.test.ts": 294,
"src/shared/workspace-layout/workspace-layout-minted-ids.test.ts": 40,
"src/shared/workspace-layout/workspace-layout-model-rules.test.ts": 321,
"src/shared/workspace-layout/workspace-layout-move-tab-anchor.test.ts": 160,
"src/shared/workspace-layout/workspace-layout-move-tab-pin.test.ts": 180,
"src/shared/workspace-layout/workspace-layout-open-in-new-group.test.ts": 195,
"src/shared/workspace-layout/workspace-layout-owned-editor-id.test.ts": 334,
"src/shared/workspace-layout/workspace-layout-process-rules.test.ts": 201,
"src/shared/workspace-layout/workspace-layout-published-editor-view.test.ts": 175,
"src/shared/workspace-layout/workspace-layout-published.test.ts": 343,
"src/shared/workspace-layout/workspace-layout-ratio-commands.test.ts": 156,
"src/shared/workspace-layout/workspace-layout-record-patch.test.ts": 1264,
"src/shared/workspace-layout/workspace-layout-records.test.ts": 325,
"src/shared/workspace-layout/workspace-layout-resume-record.test.ts": 370,
"src/shared/workspace-layout/workspace-layout-round-trip.test.ts": 329,
"src/shared/workspace-layout/workspace-layout-simulator-tab.test.ts": 159,
"src/shared/workspace-layout/workspace-layout-terminal-adoption.test.ts": 171,
"src/shared/workspace-layout/workspace-layout-transitions.test.ts": 203,
"src/shared/workspace-layout/workspace-layout-views-owned-file.test.ts": 171,
"src/shared/workspace-layout/workspace-layout-views.test.ts": 183,
"src/shared/workspace-layout/workspace-layout-workspace-move.test.ts": 181,
"src/shared/workspace-layout/tab-groups/tab-group-moves.test.ts": 52,
"src/shared/workspace-layout/tab-groups/tab-order-moves.test.ts": 44,
"src/shared/workspace-layout/commands/workspace-layout-agent-session-tab.test.ts": 168,
"src/shared/workspace-layout/commands/workspace-layout-browser-commands.test.ts": 177,
"src/shared/workspace-layout/persistence/workspace-layout-command-persistence.test.ts": 329,
"src/shared/workspace-layout/commands/workspace-layout-commands.test.ts": 215,
"src/shared/workspace-layout/commands/workspace-layout-default-tabs.test.ts": 159,
"src/shared/workspace-layout/commands/workspace-layout-dropped-leaves.test.ts": 174,
"src/shared/workspace-layout/editor-tabs/workspace-layout-editor-duplicates.test.ts": 312,
"src/shared/workspace-layout/editor-tabs/workspace-layout-editor-file-owner.test.ts": 349,
"src/shared/workspace-layout/editor-tabs/workspace-layout-editor-open.test.ts": 166,
"src/shared/workspace-layout/editor-tabs/workspace-layout-editor-rekey.test.ts": 169,
"src/shared/workspace-layout/commands/workspace-layout-initial-surface.test.ts": 163,
"src/shared/workspace-layout/persistence/workspace-layout-legacy-persistence.test.ts": 332,
"src/shared/workspace-layout/persistence/workspace-layout-load-browser-repair.test.ts": 313,
"src/shared/workspace-layout/persistence/workspace-layout-load-check-details.test.ts": 286,
"src/shared/workspace-layout/persistence/workspace-layout-load-duplicate-pty.test.ts": 340,
"src/shared/workspace-layout/persistence/workspace-layout-load-normalization.test.ts": 470,
"src/shared/workspace-layout/persistence/workspace-layout-load-synthetic-pane-title.test.ts": 324,
"src/shared/workspace-layout/persistence/workspace-layout-load-view-less-tabs.test.ts": 294,
"src/shared/workspace-layout/invariants/workspace-layout-model-rules.test.ts": 321,
"src/shared/workspace-layout/commands/workspace-layout-move-tab-anchor.test.ts": 160,
"src/shared/workspace-layout/commands/workspace-layout-move-tab-pin.test.ts": 180,
"src/shared/workspace-layout/commands/workspace-layout-open-in-new-group.test.ts": 195,
"src/shared/workspace-layout/editor-tabs/workspace-layout-owned-editor-id.test.ts": 334,
"src/shared/workspace-layout/commands/workspace-layout-process-rules.test.ts": 201,
"src/shared/workspace-layout/commands/workspace-layout-ratio-commands.test.ts": 156,
"src/shared/workspace-layout/published-layout/workspace-layout-records.test.ts": 325,
"src/shared/workspace-layout/commands/workspace-layout-resume-record.test.ts": 370,
"src/shared/workspace-layout/commands/workspace-layout-simulator-tab.test.ts": 159,
"src/shared/workspace-layout/commands/workspace-layout-terminal-adoption.test.ts": 171,
"src/shared/workspace-layout/commands/workspace-layout-transitions.test.ts": 203,
"src/shared/workspace-layout/client-views/workspace-layout-views.test.ts": 183,
"src/shared/workspace-layout/editor-tabs/workspace-layout-workspace-move.test.ts": 181,
"src/shared/workspace-attachments/workspace-linked-item-source-context.test.ts": 65,
"src/shared/workspace-name.test.ts": 49,
"src/shared/workspace-attachments/workspace-reference-identity.test.ts": 82,
@@ -44,7 +44,7 @@ const layoutFunctions = layoutAst.statements.filter(
assert.equal(layoutFunctions.length, layoutNames.length)
const layoutSubset = `import { isTerminalLeafId } from ${JSON.stringify(path.resolve('src/shared/workspace-layout/stable-pane-id.ts'))};
import { readSplitRatio } from '@/lib/pane-manager/pane-layout/pane-split-ratio';
import { SPLIT_RATIO_TOLERANCE } from ${JSON.stringify(path.resolve('src/shared/workspace-layout/terminal-pane-tree.ts'))};
import { SPLIT_RATIO_TOLERANCE } from ${JSON.stringify(path.resolve('src/shared/workspace-layout/terminal-topology/terminal-pane-tree.ts'))};
${layoutFunctions.map((node) => node.getText(layoutAst)).join('\n')}`
async function load(source) {
@@ -65,6 +65,11 @@ async function load(source) {
resolveDir: path.dirname(entry)
}))
// Historical inputs keep their bodies while imports select this checkout's defining owners.
builder.onResolve({ filter: /(?:^|[/\\])terminal-pane-layout-tree$/ }, () => ({
path: path.resolve(
'src/shared/workspace-layout/terminal-topology/terminal-pane-layout-tree.ts'
)
}))
builder.onResolve({ filter: /^(?:\.\.\/)+shared\// }, (args) => {
const target = path.resolve('src/shared', args.path.split('/shared/')[1])
return { path: existsSync(target) ? target : `${target}.ts` }
@@ -23,7 +23,7 @@ try {
contents: `
export { readPersistedProfileState } from './tests/e2e/helpers/persisted-profile-state'
export { partitionsFromProfileRoot } from './tests/e2e/helpers/workspace-layout-oracle-views'
export { checkWorkspaceLayoutRules } from './src/shared/workspace-layout/workspace-layout-rules'
export { checkWorkspaceLayoutRules } from './src/shared/workspace-layout/invariants/workspace-layout-rules'
`,
resolveDir: root,
loader: 'ts'
@@ -476,16 +476,6 @@ export const PR_E2E_SOURCE_ROUTES = [
file
)
},
{
// Layout resolution can invent split directions; publishing the guess overwrites the original tree irreversibly.
id: 'terminal-session.split-orientation-resolution',
specs: ['tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/runtime\/(?:remote-terminal-layout-resolution\.ts|sync-runtime-graph\/(?:graph-publication|mobile-session-terminal-tabs|mobile-session-surfaces)\.ts|web-session-tabs-sync\/terminal-surfaces\.ts)$/.test(
file
)
},
{
id: 'terminal-session.remote-pane-layout-retry',
specs: ['tests/e2e/paired-remote-pane-layout-retry.spec.ts'],
@@ -10,7 +10,7 @@ const LAYOUT_ORACLE_PREFIXES = [
// Layout model, persistence and compatibility folders.
'src/shared/workspace-layout/',
'src/shared/remote-layout-snapshot.ts',
'src/shared/workspace-layout/terminal-leaf-move.ts',
'src/shared/workspace-layout/terminal-topology/terminal-leaf-move.ts',
'src/main/persistence/terminal-topology/',
'src/main/persistence/restoring-sessions/',
'src/main/persistence/loading-store/terminal-bindings/pty-binding-',
+1 -1
View File
@@ -1,6 +1,6 @@
import { toRuntimeExecutionHostId } from '../shared/execution-host'
import type { RuntimeTerminalShow } from '../shared/runtime-types'
import type { RuntimeTerminalReveal } from '../shared/workspace-layout/terminal-pane-reveal'
import type { RuntimeTerminalReveal } from '../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal'
import { RuntimeClient } from './runtime-client'
import { resolveEnvironment } from './runtime/environments'
@@ -2,7 +2,7 @@
import type { AgentLaunchPaneVerdict } from '../../shared/agent-launch/contracts/agent-launch-pane-verdict'
import { parsePaneKey } from '../../shared/workspace-layout/stable-pane-id'
import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surface-close-target'
import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target'
const PROCEED: AgentLaunchPaneVerdict = { kind: 'proceed' }
+1 -1
View File
@@ -4,7 +4,7 @@ import {
type FleetAgentStatusEvidence,
type FleetEvidenceBinding
} from '../../shared/orchestration/fleet/orchestration-fleet-agent-status-evidence'
import { isValidTerminalTabId } from '../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
export type AgentStatusRuntimeEnrichment = Pick<
@@ -1,6 +1,6 @@
import { isClosedPaneFenced } from '../../../persistence/terminal-topology/closed-pane-binding-fence'
import type { Store } from '../../../persistence/loading-store/store'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-pane-tree'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
/** Refuse closed/reclaimed panes before spawning; a later binding refusal cannot undo a startup command. */
export function refuseClosedPaneSpawn(
+1 -1
View File
@@ -7,7 +7,7 @@ import {
isTerminalLeafMoveRequest,
type TerminalLeafMoveRequest,
type TerminalLeafMoveResult
} from '../../../../shared/workspace-layout/terminal-leaf-move'
} from '../../../../shared/workspace-layout/terminal-topology/terminal-leaf-move'
/** Durable move first; agent-status and orchestration keys follow only a committed move. */
export async function commitLeafMoveAndRekey(
+1 -1
View File
@@ -1,7 +1,7 @@
import { randomUUID } from 'node:crypto'
import { AgentLaunch } from '../../../../shared/rpc-contract/agent-launch-params'
import { toAgentLaunchPreferences } from '../../../../shared/agent-launch/choices/agent-launch-preferences'
import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
import { AgentLaunchTabClosedError } from '../../../../shared/agent-launch/contracts/agent-launch-tab-closed'
import { trackRunningAgentLaunchPane } from '../../../agent-launch/agent-launch-pane-attachment'
+1 -1
View File
@@ -8,7 +8,7 @@ import { startupCwdFallbackOutcome } from '../spawn-startup-target'
import { commitPtySpawn } from '../spawn-commit-assembly'
import type { PtySpawnPaneBinding } from '../spawn-commit-binding'
import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/terminal-bindings/pty-binding-span'
import { parseTerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-pane-placement'
import { parseTerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-placement'
import { commitPtyWithOpenCodePromptIntent } from '../../../opencode/startup-prompts/opencode-startup-prompt-owner'
import { resumeSpawnRefusal, savedResumeResult } from '../host-env/saved-resume-admission'
+1 -1
View File
@@ -4,7 +4,7 @@ import {
parseLegacyNumericPaneKey
} from '../../../../shared/workspace-layout/stable-pane-id'
import { isOpaqueRemintedPaneKey } from '../../../../shared/pane-key-alias'
import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
import { LocalPtyProvider } from '../../../providers/local-pty/local-pty-provider'
import { resolvePathEnvKey } from '../../../pty/environment/windows-path-segment-merge'
import { routesFreshSpawnsToLocalProvider } from '../host-env/fresh-spawn-routing'
+1 -1
View File
@@ -1,6 +1,6 @@
import type { PtySpawnOutcome } from '../spawn-outcome'
import { isClosedPaneFenced } from '../../../persistence/terminal-topology/closed-pane-binding-fence'
import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import {
toSshExecutionHostId,
getConnectionExecutionHostId
@@ -1,6 +1,6 @@
import type { RuntimePtySpawnState } from './spawn-state'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/workspace-layout-published'
import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published'
type RuntimeResumeOutcomeState = Pick<
RuntimePtySpawnState,
+1 -1
View File
@@ -1,7 +1,7 @@
import type { IPtyProvider, PtySpawnResult } from '../../../providers/pty-provider-contract'
import { LocalPtyProvider } from '../../../providers/local-pty/local-pty-provider'
import { makePaneKey, isTerminalLeafId } from '../../../../shared/workspace-layout/stable-pane-id'
import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
import {
routesFreshSpawnsToLocalProvider,
beginPtySpawnForWorktree
+1 -1
View File
@@ -1,7 +1,7 @@
import { withClaudeProfileTerminalEnv } from '../../../claude-accounts/profiles/routing/claude-profile-installed-router'
import type { PtySpawnResult } from '../../../providers/pty-provider-contract'
import { LocalPtyProvider } from '../../../providers/local-pty/local-pty-provider'
import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
import { isTerminalLeafId } from '../../../../shared/workspace-layout/stable-pane-id'
import { getProvider } from '../provider/registry'
import {
+2 -2
View File
@@ -17,7 +17,7 @@ import type { StablePaneOwner } from '../pane/stable-owner'
import type { PaneSpawnReservation } from '../pane/spawn-reservation'
import type { AdoptStablePaneResult } from '../ipc/spawn-types'
import type { PtyBindingSourceExpectation } from '../../../persistence/loading-store/store'
import type { TerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-pane-placement'
import type { TerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-placement'
import type { ResumeSpawnAdmission } from '../host-env/saved-resume-admission'
import type { PtyRuntimeControllerDeps } from './controller-deps'
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
@@ -31,7 +31,7 @@ import type {
AgentSessionSurfaceBinding
} from '../../../../shared/agent-session/agent-session-host-authority'
import { localProvider } from '../provider/registry'
import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/workspace-layout-published'
import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published'
import type { StartupCwdFallback } from '../../../../shared/startup-cwd-fallback'
import type { TerminalWorkspaceLaunchScope } from '../../../runtime/orchestration/terminal-authority/structured-sessions/runtime-legacy-worker-terminal-recovery-types'
+1 -1
View File
@@ -1,6 +1,6 @@
import { readTerminalStartFailure } from '../../../shared/terminal/pty/lifecycle/terminal-start-failure'
import type { PtySpawnSizeSeed } from './spawn-options-assembly'
import { isValidTerminalTabId } from '../../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
import type { AgentSessionClaimedSpawnResult } from '../../../shared/agent-session/agent-session-host-authority'
import type { GlobalSettings } from '../../../shared/global-settings-types'
import type { TuiAgent } from '../../../shared/agent-launch/catalog/tui-agent'
+1 -1
View File
@@ -1,5 +1,5 @@
import { isTerminalLeafId, makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import { isValidTerminalTabId } from '../../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
import { makePaneSpawnReservationKey } from './pane/spawn-reservation'
type PtySpawnPaneCoordinates = {
+1 -1
View File
@@ -5,7 +5,7 @@ import {
} from '../../../shared/jcode-runtime-dir'
import { applyScrubSafeAgentEnvAliases } from '../../../shared/agent-hook-listener/transport/agent-hook-scrub-safe-env'
import { isTerminalLeafId, makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import { isValidTerminalTabId } from '../../../shared/workspace-layout/terminal-tab-id'
import { isValidTerminalTabId } from '../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id'
export type PtySpawnPane = {
paneKey: string
@@ -8,7 +8,7 @@ import {
emptySession,
GIT_KEY,
leaf
} from '../../shared/workspace-layout/workspace-layout-session.test-fixture'
} from '../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture'
import type { SleepingAgentSessionRecord } from '../../shared/agent-session/lifecycle/agent-session-resume'
import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types'
@@ -8,7 +8,7 @@ import {
emptySession,
GIT_KEY,
leaf
} from '../../shared/workspace-layout/workspace-layout-session.test-fixture'
} from '../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture'
import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types'
const { handlers } = vi.hoisted(() => ({
+1 -1
View File
@@ -1,7 +1,7 @@
import { ipcMain } from 'electron'
import type { Store } from '../persistence/loading-store/store'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { parseTerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surface-close-target'
import { parseTerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target'
import { isFrozenOrcadSourceSessionPartition } from '../persistence/migrating-orcad-catalog/source/catalog/orcad-source-visibility'
import { isWslMoveWindowWriteHeld } from '../persistence/moving-wsl-projects/quiescence/wsl-move-window-write-hold'
import { visibleSessionHostIds } from '../persistence/migrating-orcad-catalog/retained-source/orcad-retained-source-session'
@@ -1,4 +1,4 @@
import { hasClosedTerminalTabRecord } from '../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import {
closeTerminalTabInWorkspaceSession,
workspaceSessionListsTerminalTab
@@ -1,7 +1,7 @@
import { isTerminalLeafId } from '../../../../shared/workspace-layout/stable-pane-id'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import '../../restoring-sessions/terminal-layout-normalization'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-pane-tree'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { tabRowPtyIdAfterLeafBinding } from './terminal-tab-pty-ownership'
/** not_durable alone means memory matches and only the debounced save remains. */
@@ -26,11 +26,11 @@ import {
type PtyBindingOrigin,
type PtyBindingSpan
} from './pty-binding-span'
import type { TerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-pane-placement'
import type { TerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-placement'
import type {
TerminalLeafMoveRequest,
TerminalLeafMoveResult
} from '../../../../shared/workspace-layout/terminal-leaf-move'
} from '../../../../shared/workspace-layout/terminal-topology/terminal-leaf-move'
import {
admitPane,
moveLeaf,
@@ -1,9 +1,9 @@
import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import { isTerminalLeafId } from '../../../../shared/workspace-layout/stable-pane-id'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import type { TerminalSessionPartition } from '../../../../shared/workspace-layout/terminal-owner-invariants'
import type { TerminalSessionPartition } from '../../../../shared/workspace-layout/terminal-topology/terminal-owner-invariants'
import '../../restoring-sessions/terminal-layout-normalization'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-pane-tree'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import type { PtyBindingSourceExpectation } from '../store'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import { isClosedPaneFenced } from '../../terminal-topology/closed-pane-binding-fence'
@@ -2,7 +2,7 @@ import { isTerminalLeafId } from '../../../../shared/workspace-layout/stable-pan
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id'
import { cloneLayoutNode } from '../../restoring-sessions/terminal-layout-normalization'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-pane-tree'
import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { createMinimalPersistedTerminalTab } from '../../restoring-sessions/session-owner-fields'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import { placedSplitRoot } from '../../terminal-topology/terminal-pane-placement-graft'
@@ -7,9 +7,9 @@ import {
import {
findTerminalBindingConflict,
type TerminalOwnerConflictReason
} from '../../../../shared/workspace-layout/terminal-owner-invariants'
} from '../../../../shared/workspace-layout/terminal-topology/terminal-owner-invariants'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { TerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-pane-placement'
import type { TerminalPanePlacement } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-placement'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
export type PtyBindingPath = 'durable_match' | 'durability_only' | 'mutation'
@@ -6,7 +6,7 @@ import {
type ExecutionHostId
} from '../../../../shared/execution-host'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import type { LayoutEditorView } from '../../../../shared/workspace-layout/layout-editor-view'
import type { LayoutEditorView } from '../../../../shared/workspace-layout/editor-tabs/layout-editor-view'
import type { WorkspaceLayoutModel } from '../../../../shared/workspace-layout/workspace-layout-model'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import type { StoreRuntimeState } from '../store-runtime-state'
@@ -1,4 +1,4 @@
import { orderTerminalRowsByTabGroups } from '../../../../shared/workspace-layout/terminal-row-order'
import { orderTerminalRowsByTabGroups } from '../../../../shared/workspace-layout/terminal-topology/terminal-row-order'
import type { PersistedState } from '../../../../shared/persisted-state-types'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { sanitizeWorkspaceSessionTerminalRetirements } from '../../../runtime/terminal/surface/mobile-session-terminal-persistence-retirement'
@@ -25,7 +25,7 @@ import {
import { dropClosedTerminalTabs } from '../../closed-terminal-tab-write-fence'
import { keepWslMoveFrozenSessionsForStore } from '../../moving-wsl-projects/terminal-state/wsl-move-session-freeze'
import { type WriteSchedulingOperations, scheduleSave } from '../profile-writing/write-scheduling'
import { orderTerminalRowsByTabGroups } from '../../../../shared/workspace-layout/terminal-row-order'
import { orderTerminalRowsByTabGroups } from '../../../../shared/workspace-layout/terminal-topology/terminal-row-order'
export class SessionSnapshotOperations {
readonly [sessionSnapshotOperationsContext]: SessionSnapshotOperationsContext
@@ -30,7 +30,7 @@ import { preserveMissingWorkspaceSessionTerminalBindings } from './workspace-ses
import type { SessionSnapshotOperations } from './session-snapshot-operations'
import { getSessionSnapshotOperationsContext } from './session-snapshot-operations-context'
import { scheduleSave } from '../profile-writing/write-scheduling'
import { orderTerminalRowsByTabGroups } from '../../../../shared/workspace-layout/terminal-row-order'
import { orderTerminalRowsByTabGroups } from '../../../../shared/workspace-layout/terminal-topology/terminal-row-order'
export function setLocalWorkspaceSession(
owner: SessionSnapshotOperations,
@@ -4,7 +4,7 @@ import type { PersistedState } from '../../../../shared/persisted-state-types'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { SESSION_FOCUS_FIELDS } from '../../../../shared/workspace-session/workspace-session-host-field-ownership'
import { mergeWorkspaceSessions } from '../../../orca-profiles/project-transfer/profile-project-session-state'
import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree'
import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
const MERGED_SESSION_RECORD_FIELDS = [
'tabsByWorktree',
@@ -1,4 +1,4 @@
import { hasClosedTerminalTabRecord } from '../../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
import type { PersistedState } from '../../../../../shared/persisted-state-types'
import { normalizeWorkspaceSessionKeyToWorkspaceId } from '../../../../../shared/workspace-scope'
@@ -36,7 +36,7 @@ import {
type SessionFragment
} from './orcad-source-session-copy-resolution'
import { withoutOrcadMigrationDrafts } from '../worktrees/orcad-source-draft-rename'
import { hasClosedTerminalTabRecord } from '../../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
import { normalizeWorkspaceSessionKeyToWorkspaceId } from '../../../../../shared/workspace-scope'
import { orcadMigrationSessionPartitions } from './orcad-source-session-partitions'
@@ -5,7 +5,7 @@
* cannot rewrite) and closed-terminal-tab tombstones. Only for workspaces seeded in this run, so
* anything the user since changed on the host stays as it is.
*/
import type { ClosedTerminalTabTombstone } from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import type { ClosedTerminalTabTombstone } from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { ownedEditorFileId } from '../../../../shared/workspace-session/workspace-session-pane-ownership'
import { isHostBoundOpenFile } from '../../migrating-orcad-catalog/source/worktrees/orcad-source-draft-rename'
@@ -23,7 +23,7 @@ import {
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import type { WslMoveRequest } from '../../../../shared/wsl-move/wsl-move-driver-types'
import { parsePtySessionId } from '../../../../shared/terminal/pty/lifecycle/pty-session-id-format'
import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree'
import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree'
import type { ColdRestoreInfo } from '../../../daemon/history/restoration/terminal-history-cold-restore-info'
import type { TerminalCheckpointFile } from '../../../daemon/protocol/daemon-checkpoint-file'
import type { OrcadMigrationBlobSource } from '../../../managed-server/migration/transfers/orcad-migration-blob-coordinator'
@@ -1,7 +1,7 @@
import type { PersistedState } from '../../../shared/persisted-state-types'
import type { TerminalLayoutSnapshot } from '../../../shared/workspace-layout/terminal-tab-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { resolveRootlessTerminalLayoutLeafId } from '../../../shared/workspace-layout/terminal-pane-tree'
import { resolveRootlessTerminalLayoutLeafId } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { deriveHeadlessLegacyTerminalLeafId } from '../../runtime/terminal/surface/mobile-session-layout-projection'
function nameEmptyLayoutRoot(
@@ -6,7 +6,10 @@ import type {
} from '../../../shared/workspace-layout/terminal-tab-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { isTerminalLeafId, makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import { collectLayoutLeafIdsInOrder, firstLayoutLeafId } from './terminal-layout-normalization'
import {
collectLayoutLeafIdsInOrder,
firstLayoutLeafId
} from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
export function findWorktreeIdForTab(
session: WorkspaceSessionState,
@@ -14,7 +14,7 @@ import {
GIT_KEY,
leaf,
SSH_KEY
} from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
} from '../../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture'
import {
bindPersistedTabTerminalPanes,
bindTabTerminalPanesInSession
@@ -52,21 +52,6 @@ describe("load repair: a tab's own terminal and its unbound pane", () => {
expect(bindings(repair(stored, true))).toEqual({ [leaf(1)]: 'ssh:box@@pty-9' })
})
it("never takes a terminal another tab's pane binds", () => {
const stored = addWorkspace(emptySession(), GIT_KEY, [
{
id: 'g1',
tabs: [
{ id: 'tab-a', leaves: [[leaf(1)]] },
{ id: 'tab-b', leaves: [[leaf(2), 'pty-a']] }
]
}
])
stored.tabsByWorktree[GIT_KEY]![0]!.ptyId = 'pty-a'
stored.activeWorktreeIdsOnShutdown = [GIT_KEY]
expect(repair(stored)).toBe(stored)
})
it('binds one of two tabs naming the same terminal, never both', () => {
const stored = addWorkspace(emptySession(), GIT_KEY, [
{
@@ -3,15 +3,15 @@ import type { PersistedState } from '../../../shared/persisted-state-types'
import type { TerminalLayoutSnapshot } from '../../../shared/workspace-layout/terminal-tab-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-pane-tree'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import {
readCanonicalPtyOwners,
resolveCanonicalPtyClaim
} from '../../../shared/workspace-layout/legacy-terminal-row-claims'
} from '../../../shared/workspace-layout/terminal-topology/legacy-terminal-row-claims'
import {
liveAtShutdown,
sweepUnattachedTerminalRows
} from '../../../shared/workspace-layout/unattached-terminal-row-sweep'
} from '../../../shared/workspace-layout/terminal-topology/unattached-terminal-row-sweep'
/** Removed leaves cannot claim terminal ownership. */
function mountedPtyIds(layout: TerminalLayoutSnapshot): string[] {
@@ -1,9 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { TerminalLayoutSnapshot } from '../../../shared/workspace-layout/terminal-tab-types'
import {
collectLayoutLeafIdsInOrder,
normalizeTerminalLayoutSnapshotForPersistence
} from './terminal-layout-normalization'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { normalizeTerminalLayoutSnapshotForPersistence } from './terminal-layout-normalization'
const STABLE_A = '11111111-1111-4111-8111-111111111111'
const STABLE_B = '22222222-2222-4222-8222-222222222222'
@@ -7,13 +7,7 @@ import { isTerminalLeafId } from '../../../shared/workspace-layout/stable-pane-i
import {
collectLayoutLeafIdsInOrder,
firstLayoutLeafId
} from '../../../shared/workspace-layout/terminal-pane-tree'
export {
collectLayoutLeafIdsInOrder,
firstLayoutLeafId,
layoutContainsLeafId
} from '../../../shared/workspace-layout/terminal-pane-tree'
} from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
export type LayoutLeafNormalization = {
snapshot: TerminalLayoutSnapshot
@@ -4,17 +4,15 @@ import { describe, expect, it } from 'vitest'
import { getDefaultPersistedState } from '../../../shared/constants'
import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { sleepingRecord } from '../../../shared/workspace-layout/workspace-layout-profile.test-fixture'
import '../../../shared/workspace-layout/persistence/workspace-layout-profile.test-fixture'
import {
addWorkspace,
emptySession,
GIT_KEY,
leaf
} from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
} from '../../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture'
import { sweepPersistedUnattachedTerminalRows } from './tab-terminal-pane-binding-repair'
const CLI_KEY = 'repo-1::/Users/dev/orca-cli'
function unattachedRow(): WorkspaceSessionState {
const session = addWorkspace(emptySession(), GIT_KEY, [
{
@@ -30,97 +28,10 @@ function unattachedRow(): WorkspaceSessionState {
return session
}
function swept(session: WorkspaceSessionState): WorkspaceSessionState {
const state = getDefaultPersistedState('/home/dev')
state.workspaceSession = session
return sweepPersistedUnattachedTerminalRows(state).workspaceSession
}
const rowIds = (session: WorkspaceSessionState, key = GIT_KEY): string[] =>
(session.tabsByWorktree[key] ?? []).map((row) => row.id)
describe('load sweep: a terminal row outside the tab bar', () => {
it('goes with its pane layout when nothing would reattach it', () => {
const session = swept(unattachedRow())
expect(rowIds(session)).toEqual(['tab-a'])
expect(session.terminalLayoutsByTabId['tab-b']).toBeUndefined()
})
it('goes when only a pane binds a terminal, as the window does', () => {
const session = unattachedRow()
session.terminalLayoutsByTabId['tab-b']!.ptyIdsByLeafId = { [leaf(2)]: 'pty-b' }
expect(rowIds(swept(session))).toEqual(['tab-a'])
})
it('goes when its workspace was not live at shutdown', () => {
const session = unattachedRow()
session.tabsByWorktree[GIT_KEY]![1]!.ptyId = 'pty-b'
session.activeWorktreeIdsOnShutdown = []
expect(rowIds(swept(session))).toEqual(['tab-a'])
})
it('leaves its sleeping agent record in place, as the window does', () => {
const session = unattachedRow()
const paneKey = `tab-b:${leaf(2)}`
session.sleepingAgentSessionsByPaneKey = {
[paneKey]: sleepingRecord(GIT_KEY, 'tab-b', leaf(2))
}
const after = swept(session)
expect(rowIds(after)).toEqual(['tab-a'])
expect(Object.keys(after.sleepingAgentSessionsByPaneKey ?? {})).toEqual([paneKey])
})
it('moves selections off it: the workspace to its first kept row, the global one to none', () => {
const session = unattachedRow()
session.activeTabId = 'tab-b'
session.activeTabIdByWorktree = { [GIT_KEY]: 'tab-b' }
const after = swept(session)
expect(after.activeTabIdByWorktree).toEqual({ [GIT_KEY]: 'tab-a' })
expect(after.activeTabId).toBeNull()
})
it('stays when its own terminal would reattach', () => {
const session = unattachedRow()
session.tabsByWorktree[GIT_KEY]![1]!.ptyId = 'pty-b'
expect(swept(session)).toBe(session)
})
it('stays when its terminal is on an SSH relay', () => {
const session = unattachedRow()
session.remoteSessionIdsByTabId = { 'tab-b': 'relay-b' }
expect(swept(session)).toBe(session)
})
it('stays when a split row keeps its own pane’s terminal after the tab bar takes the shared one', () => {
const session = unattachedRow()
session.terminalLayoutsByTabId['tab-b'] = {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: leaf(2) },
second: { type: 'leaf', leafId: leaf(3) }
},
activeLeafId: leaf(2),
expandedLeafId: null,
ptyIdsByLeafId: { [leaf(2)]: 'pty-a', [leaf(3)]: 'pty-c' }
}
expect(swept(session)).toBe(session)
})
it('stays when the CLI started it in a workspace the window never opened', () => {
const session = unattachedRow()
session.tabsByWorktree[GIT_KEY]![1]!.ptyId = 'pty-b'
session.tabsByWorktree[CLI_KEY] = [
{
...session.tabsByWorktree[GIT_KEY]![1]!,
id: 'tab-cli',
worktreeId: CLI_KEY,
ptyId: 'pty-cli'
}
]
expect(swept(session)).toBe(session)
})
it('sweeps SSH partitions too and passes a paired server’s through', () => {
const state = getDefaultPersistedState('/home/dev')
const sshHost = toSshExecutionHostId('box')
@@ -131,48 +42,4 @@ describe('load sweep: a terminal row outside the tab bar', () => {
expect(rowIds(after[sshHost]!)).toEqual(['tab-a'])
expect(after[serverHost]).toBe(serverSession)
})
it('stays when it is an SSH workspace’s and only its own terminal id names one, as the window may reattach it', () => {
const state = getDefaultPersistedState('/home/dev')
state.repos = [
{
id: 'repo-1',
path: '/Users/dev/orca',
displayName: 'orca',
badgeColor: '#000',
addedAt: 0,
connectionId: 'box'
}
]
const session = unattachedRow()
session.tabsByWorktree[GIT_KEY]![1]!.ptyId = 'pty-b'
state.workspaceSession = session
expect(rowIds(sweepPersistedUnattachedTerminalRows(state).workspaceSession)).toEqual([
'tab-a',
'tab-b'
])
})
it('stays when another partition’s stale shutdown list names its workspace, as the merged list does', () => {
const state = getDefaultPersistedState('/home/dev')
const sshHost = toSshExecutionHostId('box')
const local = unattachedRow()
local.tabsByWorktree[GIT_KEY]![1]!.ptyId = 'pty-b'
local.activeWorktreeIdsOnShutdown = []
const remote = emptySession()
remote.activeWorktreeIdsOnShutdown = [GIT_KEY]
state.workspaceSession = local
state.workspaceSessionsByHostId = { [sshHost]: remote }
expect(sweepPersistedUnattachedTerminalRows(state).workspaceSession).toBe(local)
})
it('treats a session from before the tab bar as the window does: keeps a live row, sweeps the rest', () => {
const session = unattachedRow()
delete session.unifiedTabs
delete session.tabGroups
delete session.tabGroupLayouts
const after = swept(session)
expect(rowIds(after)).toEqual(['tab-a'])
expect(after.terminalLayoutsByTabId['tab-a']).toBeDefined()
})
})
@@ -3,7 +3,7 @@
import type { ExecutionHostId } from '../../../shared/execution-host'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-pane-tree'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import type { LayoutCommand } from '../../../shared/workspace-layout/workspace-layout-command-types'
import type { DurableProfileStateMutation } from '../loading-store/store-runtime-state'
@@ -1,6 +1,6 @@
// Main moves editor tabs across partitions and writes only changed records, retaining in-memory views.
import type { LayoutClientVisibility } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import type { LayoutClientVisibility } from '../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import { randomUUID } from 'node:crypto'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
@@ -8,16 +8,16 @@ import type {
CommandOf,
LayoutApplyOutcome
} from '../../../shared/workspace-layout/workspace-layout-command-types'
import { updateLegacyPersistence } from '../../../shared/workspace-layout/workspace-layout-legacy-persistence'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load'
import type { LoadedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load-types'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
import { updateLegacyPersistence } from '../../../shared/workspace-layout/persistence/workspace-layout-legacy-persistence'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-load'
import type { LoadedWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-load-types'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import type {
WorkspaceLayout,
WorkspaceLayoutModel
} from '../../../shared/workspace-layout/workspace-layout-model'
import { patchStoredSession } from '../../../shared/workspace-layout/workspace-layout-record-patch'
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save'
import { patchStoredSession } from '../../../shared/workspace-layout/persistence/workspace-layout-record-patch'
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-save'
import { applyLayoutCommand } from '../../../shared/workspace-layout/workspace-layout-commands'
import {
editorViewsOfModel,
@@ -3,7 +3,7 @@ import { isTerminalLeafId, makePaneKey } from '../../../shared/workspace-layout/
import {
normalizeTerminalLayoutPtyOwnership,
resolveTerminalLayoutPtyOwnershipTransfers
} from '../../../shared/workspace-layout/terminal-layout-pty-ownership'
} from '../../../shared/workspace-layout/terminal-topology/terminal-layout-pty-ownership'
import type { TerminalLayoutSnapshot } from '../../../shared/workspace-layout/terminal-tab-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import type { DurableProfileStateMutation } from '../loading-store/store-runtime-state'
@@ -8,7 +8,7 @@ import {
emptySession,
GIT_KEY,
SSH_KEY
} from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
} from '../../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture'
import { commitLayoutCommandsAcrossPartitions } from './layout-command-commit'
import type { TerminalTopologyCommitContext } from './terminal-topology-commit'
@@ -1,141 +0,0 @@
import { withoutGitGraph } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import type { Store } from '../loading-store/store'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { applyWindowLayoutCommand } from '../../runtime/terminal/layout/runtime-layout-apply'
import { WindowSession } from './terminal-topology-window-session-fixture'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID,
openTopologyStore,
reopenTopologyStore
} from './terminal-topology-profile-fixture'
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../ssh/config/ssh-config-parser', () => ({
loadUserSshConfig: () => ({ hosts: [] }),
sshConfigHostsToTargets: () => []
}))
const TAB = 'tab-1'
const LEAF = '11111111-1111-4111-8111-111111111111'
const directories: string[] = []
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
async function storeWithTerminal(worktreeId: string): Promise<{ store: Store; directory: string }> {
const directory = mkdtempSync(join(tmpdir(), 'orca-editor-apply-'))
directories.push(directory)
const store = await openTopologyStore(directory, emptyTerminalSessionProfile())
const window = new WindowSession(getDefaultWorkspaceSession())
window.addTab(worktreeId, TAB, LEAF)
store.setWorkspaceSession(window.snapshot())
await store.flushPendingOrThrowAsync()
return { store, directory }
}
function editorOpen(worktreeId: string, tabId: string, path: string) {
return {
type: 'openEditorTab',
workspace: worktreeId,
tabId,
fileId: path,
contentType: 'editor',
groupId: `group-${worktreeId}`,
preview: true,
file: { filePath: path, relativePath: path.slice(1), language: 'typescript' }
} as const
}
describe('a window moveTabToWorkspace between a local and an SSH workspace', () => {
const SSH_HOST = 'ssh:target-1'
const SSH_WORKTREE = 'repo-ssh::/home/dev/api'
const routes: Record<string, ExecutionHostId> = {
[FIXTURE_GIT_WORKTREE_ID]: 'local',
[SSH_WORKTREE]: SSH_HOST
}
const hostIdsFor = (worktreeId: string) => (routes[worktreeId] ? [routes[worktreeId]] : [])
it.each([
['local to SSH', FIXTURE_GIT_WORKTREE_ID, SSH_WORKTREE],
['SSH to local', SSH_WORKTREE, FIXTURE_GIT_WORKTREE_ID]
])('moves a restored editor tab %s in one durable write', async (_name, from, to) => {
const { store, directory } = await storeWithTerminal(FIXTURE_GIT_WORKTREE_ID)
const path = '/fixture/other/readme.md'
const open = { ...editorOpen(from, 'e1', path), preview: undefined }
await store.applyLayoutCommand({ ...open, file: { ...open.file, relativePath: path } }, [
routes[from]!
])
const newId = `editor:${to}:${path}`
const held = store.getWorkspaceSession(routes[to]).tabGroups?.[to]?.[0]?.id
await store.applyLayoutCommand(
{
type: 'openEditorTab',
workspace: to,
tabId: 'graph',
fileId: 'graph',
contentType: 'git-graph',
view: {
view: { mode: 'git-graph' },
file: { filePath: 'graph', relativePath: 'graph', language: 'plaintext' }
},
groupId: 'hidden',
...(held ? { split: { besideGroupId: held, direction: 'right' as const } } : {})
},
[routes[to]!]
)
const destination = held ?? 'hidden'
await expect(
applyWindowLayoutCommand(
store,
hostIdsFor,
{
type: 'moveTabToWorkspace',
workspace: from,
toWorkspace: to,
tabs: [{ tabId: 'e1', newTabId: 'e1', newEntityId: newId }],
groupId: `group-${to}`,
file: { filePath: path, relativePath: 'readme.md', language: 'markdown' }
},
undefined,
{ visibility: withoutGitGraph }
)
).resolves.toEqual({ ok: true, result: { tabId: 'e1', groupId: destination } })
// Inspect before reload: this fixture lacks the SSH repo needed to retain its workspace.
const source = store.getWorkspaceSession(routes[from])
const target = store.getWorkspaceSession(routes[to])
expect(target.tabGroups?.[to]?.find((group) => group.id === destination)?.tabOrder).toEqual(
held ? [TAB, 'e1'] : ['graph', 'e1']
)
expect(target.unifiedTabs?.[to]?.find((tab) => tab.id === 'graph')?.contentType).toBe(
'git-graph'
)
expect(source.unifiedTabs?.[from]?.some((tab) => tab.id === 'e1') ?? false).toBe(false)
expect(source.openFilesByWorktree?.[from] ?? []).toEqual([])
expect(target.unifiedTabs?.[to]?.find((tab) => tab.id === 'e1')).toMatchObject({
entityId: newId
})
expect(target.openFilesByWorktree?.[to]).toEqual([
expect.objectContaining({ filePath: path, relativePath: 'readme.md', worktreeId: to })
])
const reopened = await reopenTopologyStore(store, directory)
expect(reopened.getWorkspaceSession('local').tabsByWorktree[FIXTURE_GIT_WORKTREE_ID]).toEqual([
expect.objectContaining({ id: TAB })
])
expect(
reopened
.getWorkspaceSession('local')
.unifiedTabs?.[FIXTURE_GIT_WORKTREE_ID]?.some((tab) => tab.id === 'e1')
).toBe(to === FIXTURE_GIT_WORKTREE_ID)
})
})
@@ -2,7 +2,7 @@
import type { ExecutionHostId } from '../../../shared/execution-host'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-pane-tree'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { bumpPtyBindingGeneration } from '../loading-store/terminal-bindings/pty-binding-generations'
import type {
@@ -1,4 +1,4 @@
import type { LayoutClientVisibility } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import type { LayoutClientVisibility } from '../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import type {
CommandOf,
@@ -3,13 +3,13 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-pane-tree'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { TEST_LEAF_1, TEST_LEAF_2 } from '../persistence-session-fixtures'
import { _resetClosedPaneFencesForTests } from './closed-pane-binding-fence'
import { closeLeafOrTab } from './terminal-topology-commit'
import { captureTerminalCloseRequest } from '../../runtime/terminal/surface/terminal-surface-close'
import type { TerminalSurfaceCloseTarget } from '../../../shared/workspace-layout/terminal-surface-close-target'
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host'
import './terminal-topology-commit'
import '../../runtime/terminal/surface/terminal-surface-close'
import '../../../shared/execution-host'
vi.mock('electron', () => ({
app: { getPath: () => testState.dir },
@@ -64,27 +64,6 @@ function lateWindowBinding(store: TestStore): Promise<boolean> {
})
}
async function closeThroughMain(
store: TestStore,
target: TerminalSurfaceCloseTarget
): Promise<void> {
const hostIds: readonly ExecutionHostId[] = [LOCAL_EXECUTION_HOST_ID]
const refusal = await store.runDurableMutation(
closeLeafOrTab({
worktreeId: WORKTREE,
target,
options: { allowMissing: true },
...captureTerminalCloseRequest(WORKTREE, target, hostIds, store, false),
ownerMatches: () => true,
hostIds: () => hostIds,
getSession: (hostId) => store.getWorkspaceSession(hostId),
setSession: (session, hostId) => store.setWorkspaceSession(session, hostId),
onClosed: () => {}
})
)
expect(refusal).toBeUndefined()
}
function leafIds(store: TestStore): string[] {
const root = store.getWorkspaceSession().terminalLayoutsByTabId[TAB]?.root
return root ? collectLayoutLeafIdsInOrder(root) : []
@@ -134,16 +113,6 @@ describe('a pane main closed or took back is not brought back by its late termin
expect(leafIds(store)).toEqual([TEST_LEAF_1])
})
it('refuses the binding of a pane main closed while its terminal was starting', async () => {
const store = createStore()
await tabWithStartingSplit(store)
await closeThroughMain(store, { kind: 'pane', tabId: TAB, leafId: TEST_LEAF_2 })
expect(await lateWindowBinding(store)).toBe(false)
expect(leafIds(store)).toEqual([TEST_LEAF_1])
})
it('refuses the binding of a pane whose tab the window closed while it was starting', async () => {
const store = createStore()
await tabWithStartingSplit(store)
@@ -1,99 +0,0 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
import { TEST_LEAF_1, TEST_LEAF_2 } from '../persistence-session-fixtures'
import type { Store } from '../loading-store/store'
import { closeLeafOrTab } from './terminal-topology-commit'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID,
openTopologyStore
} from './terminal-topology-profile-fixture'
import { WindowSession } from './terminal-topology-window-session-fixture'
import { checkWorkspaceLayoutRules } from '../../../shared/workspace-layout/workspace-layout-rules'
// Retire this rollback coverage with switch-off code.
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: false }))
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../ssh/config/ssh-config-parser', () => ({
loadUserSshConfig: () => ({ hosts: [] }),
sshConfigHostsToTargets: () => []
}))
const WT = FIXTURE_GIT_WORKTREE_ID
const directories: string[] = []
async function openWithSplit(): Promise<{ store: Store; window: WindowSession }> {
const directory = mkdtempSync(join(tmpdir(), 'orca-late-bind-'))
directories.push(directory)
const store = await openTopologyStore(directory, emptyTerminalSessionProfile())
const window = new WindowSession(structuredClone(store.getWorkspaceSession()))
window.addTab(WT, 'tab-a', TEST_LEAF_1)
window.setLayout(
'tab-a',
{
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: TEST_LEAF_1 },
second: { type: 'leaf', leafId: TEST_LEAF_2 }
},
{}
)
store.setWorkspaceSession(window.snapshot())
for (const [leafId, ptyId] of [
[TEST_LEAF_1, 'pty-1'],
[TEST_LEAF_2, 'pty-2']
] as const) {
await expect(
store.persistPtyBinding({ worktreeId: WT, tabId: 'tab-a', leafId, ptyId })
).resolves.toBe(true)
window.bind('tab-a', leafId, ptyId)
}
store.setWorkspaceSession(window.snapshot())
return { store, window }
}
function violations(store: Store): string[] {
return checkWorkspaceLayoutRules([
{ hostId: LOCAL_EXECUTION_HOST_ID, session: store.getWorkspaceSession() }
]).map((violation) => `${violation.rule}: ${violation.detail}`)
}
describe('a terminal spawn that lands after its pane changed', () => {
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
it('does not bring back a tab the user closed', async () => {
const { store, window } = await openWithSplit()
window.removeTab('tab-a')
await store.runDurableMutation(
closeLeafOrTab({
worktreeId: WT,
target: { kind: 'tab', tabId: 'tab-a' },
options: { allowMissing: true, force: true, closedByLayoutOwner: true, reason: 'user' },
requestedSession: store.getWorkspaceSession(),
ownerMatches: () => true,
hostIds: () => [LOCAL_EXECUTION_HOST_ID],
getSession: (hostId) => store.getWorkspaceSession(hostId),
setSession: (session, hostId) => store.setWorkspaceSession(session, hostId),
onClosed: () => {}
})
)
store.setWorkspaceSession(window.snapshot())
await store.persistPtyBinding({
worktreeId: WT,
tabId: 'tab-a',
leafId: TEST_LEAF_1,
ptyId: 'pty-late'
})
expect(violations(store)).toEqual([])
expect(store.getWorkspaceSession().tabsByWorktree[WT] ?? []).toEqual([])
await store.freezeWritesAsync()
})
})
@@ -1,4 +1,4 @@
import { withoutGitGraph } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import { withoutGitGraph } from '../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import type { LayoutEffects } from '../../../shared/workspace-layout/workspace-layout-command-types'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -6,7 +6,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import type { Store } from '../loading-store/store'
import { leafIds, WindowSession } from './terminal-topology-window-session-fixture'
import { WindowSession } from './terminal-topology-window-session-fixture'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID,
@@ -100,41 +100,6 @@ describe('Store.applyLayoutCommand (layout.apply)', () => {
})
expect(revision(store)).toBeGreaterThan(revisionBefore)
})
it('keeps records the split did not touch exactly as stored', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-layout-apply-'))
directories.push(directory)
const store = await openTopologyStore(directory, emptyTerminalSessionProfile())
const window = new WindowSession(getDefaultWorkspaceSession())
window.addTab(WORKTREE, TAB, LEAF)
window.bind(TAB, LEAF, 'pty-1')
// Verify record patches preserve residue that full load/save normalization would drop.
window.session.terminalPtyIncarnationsByPaneKey = { [`${TAB}:${LEAF}`]: 'incarnation-1' }
window.session.tabsByWorktree[WORKTREE]![0]!.sortOrder = 7
store.setWorkspaceSession(window.snapshot())
const stored = store.getWorkspaceSession()
await store.applyLayoutCommand(split)
const written = store.getWorkspaceSession()
expect(written.terminalPtyIncarnationsByPaneKey).toEqual(
stored.terminalPtyIncarnationsByPaneKey
)
expect(written.tabsByWorktree).toEqual(stored.tabsByWorktree)
expect(written.unifiedTabs).toEqual(stored.unifiedTabs)
})
it('writes an SSH workspace’s split into that host’s partition only', async () => {
const { store } = await storeWithOneTab('ssh:target-1')
const local = store.getWorkspaceSession()
const reply = await store.applyLayoutCommand(split, ['ssh:target-1'])
expect(reply.ok).toBe(true)
const root = store.getWorkspaceSession('ssh:target-1').terminalLayoutsByTabId[TAB]?.root
expect(leafIds(root)).toHaveLength(2)
expect(store.getWorkspaceSession()).toEqual(local)
})
})
it('admits competing initial requests against visible tabs inside the durable mutation', async () => {
@@ -4,7 +4,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import type { CommandOf } from '../../../shared/workspace-layout/workspace-layout-command-types'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-pane-layout-tree'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree'
import type { Store } from '../loading-store/store'
import { _resetClosedPaneFencesForTests } from './closed-pane-binding-fence'
import { WindowSession } from './terminal-topology-window-session-fixture'
@@ -1,4 +1,4 @@
import { withoutGitGraph } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import { withoutGitGraph } from '../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -1,7 +1,7 @@
import { tmpdir } from 'node:os'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { TerminalSurfaceCloseTarget } from '../../../shared/workspace-layout/terminal-surface-close-target'
import { makeRepo } from '../persistence-test-harness'
import { afterEach, expect, it, vi } from 'vitest'
import '../persistence-test-harness'
import {
closeMoveTestStores,
MOVED,
@@ -14,7 +14,7 @@ import {
tabsHoldingLeaf,
WT
} from './terminal-leaf-move-fixture'
import { closeLeafOrTab } from './terminal-topology-commit'
import './terminal-topology-commit'
vi.mock('electron', () => ({
app: {
@@ -38,68 +38,7 @@ afterEach(closeMoveTestStores)
const request = { ...moveRequest, ptyId: 'pty-agent' }
async function rendererClose(
store: ReturnType<typeof openStore>,
target: TerminalSurfaceCloseTarget,
reason: 'user' | 'cleanup'
): Promise<void> {
await store.runDurableMutation(
closeLeafOrTab({
worktreeId: WT,
target,
options: { allowMissing: true, force: true, closedByLayoutOwner: true, reason },
requestedSession: store.getWorkspaceSession(),
ownerMatches: () => true,
hostIds: () => ['local'],
getSession: (hostId) => store.getWorkspaceSession(hostId),
setSession: (session, hostId) => store.setWorkspaceSession(session, hostId),
onClosed: () => {}
})
)
}
async function openMovedStore() {
const dataFile = newDataFile()
const store = openStore(dataFile)
store.addRepo(makeRepo({ id: 'repo-1', path: '/tmp/move-worktree' }))
await seedSplitSource(store)
await expect(store.moveTerminalLeafToNewTab(request)).resolves.toMatchObject({
status: 'moved'
})
return { dataFile, store }
}
function restartedTabs(store: ReturnType<typeof openStore>, dataFile: string) {
store.flush()
const session = openStore(dataFile).getWorkspaceSession()
return { tabIds: (session.tabsByWorktree[WT] ?? []).map((tab) => tab.id), session }
}
// The renderer completes a post-move close against main's new tab.
describe('a close that lands while main commits the move', () => {
it('leaves no dead pane after restart when the user closed the moved pane', async () => {
const { dataFile, store } = await openMovedStore()
await rendererClose(store, { kind: 'pane', tabId: SOURCE, leafId: MOVED }, 'user')
expect(tabsHoldingLeaf(store.getWorkspaceSession(), MOVED)).toEqual([TARGET])
await rendererClose(store, { kind: 'tab', tabId: TARGET }, 'cleanup')
const { tabIds, session } = restartedTabs(store, dataFile)
expect(tabsHoldingLeaf(session, MOVED)).toEqual([])
expect(tabIds).toEqual([SOURCE])
})
it('leaves no ghost tab after restart when the user closed the source tab', async () => {
const { dataFile, store } = await openMovedStore()
await rendererClose(store, { kind: 'tab', tabId: SOURCE }, 'user')
await rendererClose(store, { kind: 'tab', tabId: TARGET }, 'cleanup')
const { tabIds, session } = restartedTabs(store, dataFile)
expect(tabIds).toEqual([])
expect(tabsHoldingLeaf(session, MOVED)).toEqual([])
})
})
// In-flight drags must refuse: a spawn without captured PTY identity can recreate SOURCE:leaf after move.
it('grafts a late spawn result for the moved leaf back into its source tab', async () => {
@@ -6,7 +6,7 @@ import type { SleepingAgentSessionRecord } from '../../../shared/agent-session/l
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { closeTestStores, createSqliteTestStore } from '../persistence-test-harness'
import { Store } from '../loading-store/store'
import { collectLayoutLeafIdsInOrder } from '../restoring-sessions/terminal-layout-normalization'
import { collectLayoutLeafIdsInOrder } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
// Import only after the test mocks electron.
@@ -1,20 +1,20 @@
import type { PersistedState } from '../../../shared/persisted-state-types'
import type { SshRemotePtyLease } from '../../../shared/ssh-types'
import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-pane-layout-tree'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree'
import {
terminalLeafMovePaneKeys,
type TerminalLeafMoveRequest,
type TerminalLeafMoveResult
} from '../../../shared/workspace-layout/terminal-leaf-move'
} from '../../../shared/workspace-layout/terminal-topology/terminal-leaf-move'
import type {
TerminalLayoutSnapshot,
TerminalTab
} from '../../../shared/workspace-layout/terminal-tab-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { retireLeavesFromTerminalLayout } from '../../../shared/workspace-layout/terminal-layout-leaf-retirement'
import { retireLeavesFromTerminalLayout } from '../../../shared/workspace-layout/terminal-topology/terminal-layout-leaf-retirement'
import { createMinimalPersistedTerminalTab } from '../restoring-sessions/session-owner-fields'
import { layoutContainsLeafId } from '../restoring-sessions/terminal-layout-normalization'
import { layoutContainsLeafId } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { writeRestorable } from './restorable-write'
import type { TerminalTopologyCommitContext } from './terminal-topology-commit'
import {
@@ -3,7 +3,7 @@ import { getDefaultWorkspaceSession } from '../../../shared/constants'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { TEST_LEAF_1, TEST_LEAF_2 } from '../persistence-session-fixtures'
import { findTerminalBindingConflict } from '../../../shared/workspace-layout/terminal-owner-invariants'
import { findTerminalBindingConflict } from '../../../shared/workspace-layout/terminal-topology/terminal-owner-invariants'
const WT = 'repo-1::/tmp/wt'
@@ -6,7 +6,7 @@ import type { WorkspaceSessionState } from '../../../shared/workspace-session/wo
import {
leafIdsOf,
locatePane
} from '../../../shared/workspace-layout/workspace-layout-command-steps'
} from '../../../shared/workspace-layout/commands/workspace-layout-command-steps'
import type {
CommandOf,
LayoutApplyOutcome,
@@ -16,20 +16,20 @@ import type {
LayoutRefusalCode
} from '../../../shared/workspace-layout/workspace-layout-command-types'
import { applyLayoutCommand } from '../../../shared/workspace-layout/workspace-layout-commands'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load'
import type { LoadedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load-types'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-load'
import type { LoadedWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-load-types'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import {
paneKeyOf,
type WorkspaceLayoutModel
} from '../../../shared/workspace-layout/workspace-layout-model'
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save'
import { patchStoredSession } from '../../../shared/workspace-layout/workspace-layout-record-patch'
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-save'
import { patchStoredSession } from '../../../shared/workspace-layout/persistence/workspace-layout-record-patch'
import {
followRemoteSessionAfterPaneClose,
followRemoteSessionAfterPaneDetach
} from './pane-remote-session-follow'
import { pruneClosedTerminalTabTombstones } from '../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { pruneClosedTerminalTabTombstones } from '../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import {
editorViewsOfModel,
@@ -1,6 +1,6 @@
import type { TerminalPanePlacement } from '../../../shared/workspace-layout/terminal-pane-placement'
import type { TerminalPanePlacement } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-placement'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { layoutContainsLeafId } from '../restoring-sessions/terminal-layout-normalization'
import { layoutContainsLeafId } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
/** Low-cardinality span attribute; absent tracks new leaves relying on legacy mint/graft placement. */
export type TerminalPanePlacementAgreement =
@@ -1,4 +1,4 @@
import type { TerminalPanePlacement } from '../../../shared/workspace-layout/terminal-pane-placement'
import type { TerminalPanePlacement } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-placement'
import type { TerminalPaneLayoutNode } from '../../../shared/workspace-layout/terminal-tab-types'
import {
collectLayoutLeafIdsInOrder,
@@ -6,7 +6,7 @@ import {
layoutContainsLeafId,
removeLayoutLeaf,
samePanesIgnoringRatios
} from '../../../shared/workspace-layout/terminal-pane-tree'
} from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { cloneLayoutNode } from '../restoring-sessions/terminal-layout-normalization'
/** Accept a proposed tree only if it adds this leaf alone; otherwise split beside the parent or return null. */
@@ -1,10 +1,10 @@
import type { ExecutionHostId } from '../../../shared/execution-host'
import { hasClosedTerminalTabRecord } from '../../../shared/workspace-layout/closed-terminal-tab-tombstones'
import { hasClosedTerminalTabRecord } from '../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones'
import type { PersistedState } from '../../../shared/persisted-state-types'
import type {
TerminalLeafMoveRequest,
TerminalLeafMoveResult
} from '../../../shared/workspace-layout/terminal-leaf-move'
} from '../../../shared/workspace-layout/terminal-topology/terminal-leaf-move'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import type {
CommandOf,
@@ -6,7 +6,7 @@ import {
import type { PersistedState } from '../../../shared/persisted-state-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
import { layoutContainsLeafId } from '../restoring-sessions/terminal-layout-normalization'
import { layoutContainsLeafId } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
export function advanceTerminalTopologyRevision(
session: WorkspaceSessionState,
@@ -6,15 +6,15 @@ import type { ExecutionHostId } from '../../../shared/execution-host'
import {
collectLayoutLeafIdsInOrder,
layoutContainsLeafId
} from '../../../shared/workspace-layout/terminal-pane-tree'
import { findTerminal } from '../../../shared/workspace-layout/workspace-layout-command-steps'
} from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import { findTerminal } from '../../../shared/workspace-layout/commands/workspace-layout-command-steps'
import type {
CommandOf,
LayoutRefusalCode,
WindowPaneDetach
} from '../../../shared/workspace-layout/workspace-layout-command-types'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-load'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import { leafHeldByOtherTab } from './terminal-leaf-move'
import {
isTerminalOwnerPartition,
@@ -7,9 +7,9 @@ import {
parseExecutionHostId,
type ExecutionHostId
} from '../../shared/execution-host'
import { loadWorkspaceLayoutWithoutReport } from '../../shared/workspace-layout/workspace-layout-load'
import { stableJson } from '../../shared/workspace-layout/workspace-layout-load-report'
import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids'
import { loadWorkspaceLayoutWithoutReport } from '../../shared/workspace-layout/persistence/workspace-layout-load'
import { stableJson } from '../../shared/workspace-layout/persistence/workspace-layout-load-report'
import { nameBasedLoadContext } from '../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import type {
WorkspaceLayout,
WorkspaceLayoutModel
@@ -1,188 +0,0 @@
import { describe, expect, it } from 'vitest'
import { toSshExecutionHostId } from '../../shared/execution-host'
import type { SshConnectionStatus } from '../../shared/ssh-types'
import {
addWorkspace,
emptySession,
GIT_KEY,
leaf,
SSH_KEY
} from '../../shared/workspace-layout/workspace-layout-session.test-fixture'
import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types'
import { deriveShutdownReconnectFacts } from '../runtime/terminal/recovery/shutdown-reconnect-facts'
import { windowViewPatchForMain, windowViewSaveForMain } from './window-save-view'
const TARGET = 'target'
const SSH_HOST = toSshExecutionHostId(TARGET)
const RELAY = `ssh:${TARGET}@@pty-42`
function sshPartition(): WorkspaceSessionState {
return addWorkspace(
emptySession(),
SSH_KEY,
[{ id: 'group-s', tabs: [{ id: 'tab-s', leaves: [[leaf(3), RELAY]] }] }],
SSH_HOST
)
}
function localPartition(): WorkspaceSessionState {
return addWorkspace(emptySession(), GIT_KEY, [
{
id: 'group-g',
tabs: [
{ id: 'tab-live', leaves: [[leaf(1), 'pty-live']] },
{ id: 'tab-gone', leaves: [[leaf(2), 'pty-gone']] }
]
}
])
}
const factsFor =
(
live: string[],
ssh: [string, SshConnectionStatus][],
more: { exited?: string[]; others?: WorkspaceSessionState[] } = {}
) =>
(session: WorkspaceSessionState) =>
deriveShutdownReconnectFacts(session, {
isLiveLocalPty: (ptyId) => live.includes(ptyId),
isKnownExited: (ptyId) => more.exited?.includes(ptyId) ?? false,
sshStatusByTargetId: new Map(ssh),
otherPartitions: () => more.others ?? []
})
function forgetfulSave(stored: WorkspaceSessionState): WorkspaceSessionState {
return {
...stored,
activeWorktreeIdsOnShutdown: [],
remoteSessionIdsByTabId: undefined,
activeConnectionIdsAtShutdown: undefined
}
}
describe('what the next startup reconnects comes from main, not the window’s save', () => {
it('keeps an unvisited SSH tab’s relay session and its target the window dropped', () => {
const stored = sshPartition()
const saved = windowViewSaveForMain(
stored,
forgetfulSave(stored),
SSH_HOST,
factsFor([], [[TARGET, 'reconnecting']])
)
expect(saved.remoteSessionIdsByTabId).toEqual({ 'tab-s': RELAY })
expect(saved.activeWorktreeIdsOnShutdown).toEqual([SSH_KEY])
expect(saved.activeConnectionIdsAtShutdown).toEqual([TARGET])
})
it('names only workspaces with a running local terminal, whatever the window sent', () => {
const stored = localPartition()
const save = { ...stored, activeWorktreeIdsOnShutdown: ['repo-1::/elsewhere'] }
const saved = windowViewSaveForMain(stored, save, null, factsFor(['pty-live'], []))
expect(saved.activeWorktreeIdsOnShutdown).toEqual([GIT_KEY])
expect(saved).not.toHaveProperty('remoteSessionIdsByTabId')
expect(saved).not.toHaveProperty('activeConnectionIdsAtShutdown')
expect(
windowViewSaveForMain(stored, save, null, factsFor([], [])).activeWorktreeIdsOnShutdown
).toEqual([])
})
it('a slept pane keeps its relay id as a wake hint only: the next startup does not reconnect it', () => {
const stored = sshPartition()
stored.sleepingAgentSessionsByPaneKey = {
[`tab-s:${leaf(3)}`]: {
paneKey: `tab-s:${leaf(3)}`,
worktreeId: SSH_KEY,
agent: 'codex',
providerSession: { key: 'session_id', id: 'sess-1' },
prompt: '',
state: 'working',
capturedAt: 1,
updatedAt: 1,
origin: 'worktree-sleep'
}
}
const saved = windowViewSaveForMain(stored, stored, SSH_HOST, factsFor([], []))
expect(saved).not.toHaveProperty('remoteSessionIdsByTabId')
expect(saved.activeWorktreeIdsOnShutdown).toEqual([])
})
it.each(['live', 'quit'] as const)(
'a running agent’s %s resume record is not a sleep: its relay session reconnects',
(origin) => {
const stored = sshPartition()
stored.sleepingAgentSessionsByPaneKey = {
[`tab-s:${leaf(3)}`]: {
paneKey: `tab-s:${leaf(3)}`,
worktreeId: SSH_KEY,
agent: 'codex',
providerSession: { key: 'session_id', id: 'sess-1' },
prompt: '',
state: 'working',
capturedAt: 1,
updatedAt: 1,
origin
}
}
const saved = windowViewSaveForMain(stored, stored, SSH_HOST, factsFor([], []))
expect(saved.remoteSessionIdsByTabId).toEqual({ 'tab-s': RELAY })
expect(saved.activeWorktreeIdsOnShutdown).toEqual([SSH_KEY])
}
)
it('a target the user left offline is not redialed; a connected one is', () => {
const stored = sshPartition()
expect(
windowViewSaveForMain(stored, stored, SSH_HOST, factsFor([], [[TARGET, 'disconnected']]))
).not.toHaveProperty('activeConnectionIdsAtShutdown')
expect(
windowViewSaveForMain(stored, stored, SSH_HOST, factsFor([], [['other', 'connected']]))
.activeConnectionIdsAtShutdown
).toEqual(['other'])
})
it('the local save names a target in a passing state from a relay session in its SSH partition', () => {
const local = localPartition()
// Reconnecting relay routes remain recoverable at quit.
const facts = factsFor([], [[TARGET, 'reconnecting']], { others: [sshPartition()] })
expect(windowViewSaveForMain(local, local, null, facts).activeConnectionIdsAtShutdown).toEqual([
TARGET
])
})
it('a relay session proven exited (a slept plain shell) is not reconnected', () => {
const stored = sshPartition()
const saved = windowViewSaveForMain(
stored,
stored,
SSH_HOST,
factsFor([], [[TARGET, 'reconnecting']], { exited: [RELAY] })
)
expect(saved).not.toHaveProperty('remoteSessionIdsByTabId')
expect(saved.activeWorktreeIdsOnShutdown).toEqual([])
expect(saved).not.toHaveProperty('activeConnectionIdsAtShutdown')
})
it('a patch has main’s facts only for the fields it carries', () => {
const stored = sshPartition()
const facts = factsFor([], [[TARGET, 'connected']])
expect(
windowViewPatchForMain(stored, { activeConnectionIdsAtShutdown: [] }, SSH_HOST, facts)
).toEqual({ activeConnectionIdsAtShutdown: [TARGET] })
expect(windowViewPatchForMain(stored, { activeRepoId: 'r' }, SSH_HOST, facts)).toEqual({
activeRepoId: 'r'
})
})
})
+6 -6
View File
@@ -10,13 +10,13 @@ import type {
WorkspaceSessionPatch,
WorkspaceSessionState
} from '../../shared/workspace-session/workspace-session-state-types'
import { SESSION_FIELD_OWNERS } from '../../shared/workspace-layout/workspace-layout-beside'
import { loadWorkspaceLayoutWithoutReport } from '../../shared/workspace-layout/workspace-layout-load'
import type { LoadedWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load-types'
import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids'
import { SESSION_FIELD_OWNERS } from '../../shared/workspace-layout/persistence/workspace-layout-beside'
import { loadWorkspaceLayoutWithoutReport } from '../../shared/workspace-layout/persistence/workspace-layout-load'
import type { LoadedWorkspaceLayout } from '../../shared/workspace-layout/persistence/workspace-layout-load-types'
import { nameBasedLoadContext } from '../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import type { WorkspaceLayoutModel } from '../../shared/workspace-layout/workspace-layout-model'
import { patchStoredSession } from '../../shared/workspace-layout/workspace-layout-record-patch'
import { saveWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-save'
import { patchStoredSession } from '../../shared/workspace-layout/persistence/workspace-layout-record-patch'
import { saveWorkspaceLayout } from '../../shared/workspace-layout/persistence/workspace-layout-save'
import type { ShutdownReconnectFacts } from '../runtime/terminal/recovery/shutdown-reconnect-facts'
const FIELD_OWNERS: Readonly<Record<string, string>> = SESSION_FIELD_OWNERS
@@ -1,6 +1,6 @@
import { expect, type Mock } from 'vitest'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-pane-layout-tree'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree'
import type {
LayoutApplyOutcome,
LayoutCommand
@@ -3,11 +3,11 @@
import type { AgentType } from '../../../shared/agent-status/contracts/agent-status-types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { applyLayoutCommand } from '../../../shared/workspace-layout/workspace-layout-commands'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
import { loadWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-load'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import type { CommandOf } from '../../../shared/workspace-layout/workspace-layout-command-types'
import type { WorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-model'
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save'
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/persistence/workspace-layout-save'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { randomUUID } from 'node:crypto'
import { getStructuredAgentSessionHost } from '../../native-chat/agent-session-wire/host/structured-agent-session-registry'
@@ -3,10 +3,10 @@
import type { BrowserPage } from '../../../../shared/browser-workspace-types'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import { applyLayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-commands'
import { BLANK_BROWSER_TAB_STATE } from '../../../../shared/workspace-layout/workspace-layout-beside'
import { loadWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../../shared/workspace-layout/workspace-layout-minted-ids'
import { saveWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-save'
import { BLANK_BROWSER_TAB_STATE } from '../../../../shared/workspace-layout/persistence/workspace-layout-beside'
import { loadWorkspaceLayout } from '../../../../shared/workspace-layout/persistence/workspace-layout-load'
import { nameBasedLoadContext } from '../../../../shared/workspace-layout/persistence/workspace-layout-minted-ids'
import { saveWorkspaceLayout } from '../../../../shared/workspace-layout/persistence/workspace-layout-save'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { randomUUID } from 'node:crypto'
@@ -13,12 +13,12 @@ import type {
AgentLaunchTabPublishRequest
} from '../../../../shared/agent-launch/contracts/agent-launch-tab-publication'
import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry'
import type { TerminalRevealIdentity } from '../../../../shared/workspace-layout/terminal-reveal-identity'
import type { TerminalPaneRevealTarget } from '../../../../shared/workspace-layout/terminal-pane-reveal'
import type { TerminalRevealIdentity } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-reveal-identity'
import type { TerminalPaneRevealTarget } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal'
import type {
TerminalLeafMoveResult,
TerminalPaneMoveToNewTabRequest
} from '../../../../shared/workspace-layout/terminal-leaf-move'
} from '../../../../shared/workspace-layout/terminal-topology/terminal-leaf-move'
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
import type { WorkspaceSleepNotice } from '../../../../shared/workspace-sleep'
import type { RuntimeNavigationTarget } from '../../../../shared/runtime-navigation'
@@ -1,7 +1,7 @@
import type { RuntimeMobileSessionClientTab } from '../../../../shared/runtime-mobile-session-tab-contracts'
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-session-contracts'
import { pickNextActiveTabAfterClose } from '../../../../shared/workspace-session/workspace-session-terminal-tab-close'
import { findSiblingGroupId } from '../../../../shared/workspace-layout/tab-group-layout-tree'
import { findSiblingGroupId } from '../../../../shared/workspace-layout/tab-groups/tab-group-layout-tree'
export type ClientSessionTabSelection = {
activeTabId: string | null
@@ -4,7 +4,7 @@ import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../../shared/sidebar-inputs-swit
import {
LAYOUT_INTENTS_RUNTIME_CAPABILITY,
LAYOUT_REOPEN_HELD_PANES_CAPABILITY
} from '../../../shared/workspace-layout/layout-intents-capability'
} from '../../../shared/workspace-layout/published-layout/layout-intents-capability'
import { WORKSPACE_UNREAD_HOST_RAISES_CAPABILITY } from '../../../shared/workspace-unread-raise-capability'
export function darkSwitchRuntimeCapabilities(): RuntimeCapability[] {
@@ -39,7 +39,7 @@ await import('../orca-runtime-tests/agent-status-and-waits-part-03.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-02.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-04.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-05.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-06.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-07.spec')
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-08.spec')
@@ -99,7 +99,7 @@ await import('../orca-runtime-tests/wsl-stopped-distro-worktree-scan.spec')
await import('../orca-runtime-tests/worktree-setup-and-startup-part-03.spec')
await import('../orca-runtime-tests/worktree-setup-and-startup-part-04.spec')
await import('../orca-runtime-tests/worktree-setup-single-creator.spec')
await import('../orca-runtime-tests/gitlab-and-pr-bases-part-02.spec')
await import('../orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec')
await import('../orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec')
@@ -9,7 +9,7 @@ import {
storeOf
} from '../terminal/layout/profile-session-store.test-fixture'
import { reusedTerminalIdUnbinding } from '../../persistence/loading-store/terminal-bindings/reused-terminal-id-unbind'
import { layoutContainsLeafId } from '../../../shared/workspace-layout/terminal-pane-tree'
import { layoutContainsLeafId } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-tree'
import type { OrcaRuntimeService } from '../orca-runtime'
export type BoundPane = {
@@ -9,7 +9,7 @@ import { withDurableRuntimeStore, withRuntimePaneAdmission } from './runtime-dur
import { OrcaRuntimeService } from '../orca-runtime'
import { committingSpawn } from './bind-first-pane.test-fixture'
import type { RuntimeNotifier } from '../client/events/runtime-notifier-contract'
import type { PaneResumeOutcome } from '../../../shared/workspace-layout/workspace-layout-published'
import type { PaneResumeOutcome } from '../../../shared/workspace-layout/published-layout/workspace-layout-published'
export const REPO = {
id: 'repo-1',
@@ -1,330 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { registerLivePty } from './bind-first-pane.test-fixture'
import './runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../orca-runtime'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import 'node:path'
import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import 'node:fs/promises'
import 'node:os'
import {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID,
TEST_WORKTREE_PATH,
makeHeadlessTerminalLayout,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal,
store
} from './orca-runtime-test-fixtures.spec'
import { spawnedPtyEnv } from '../terminal/launch/spawned-pty-env-fixtures'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-pane-layout-tree'
describe('OrcaRuntimeService', () => {
it.runIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
'returns the exact pre-minted leaf for concurrent renderer-backed splits, each started by main',
async () => {
const tabId = 'tab-concurrent-splits'
const sourceLeafId = HEADLESS_LEAF_ID
const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
id: tabId,
ptyId: 'pty-source',
worktreeId: TEST_WORKTREE_ID,
title: 'shell',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
[tabId]: makeHeadlessTerminalLayout({ [sourceLeafId]: 'pty-source' })
}
})
)
const splitTerminal = vi.fn()
const spawn = vi.fn(async (args: { leafId?: string }) => ({ id: `pty-${args.leafId}` }))
const runtime = new OrcaRuntimeService(runtimeStore)
runtime.setNotifier({ splitTerminal } as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.syncWindowGraph(1, {
tabs: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
title: 'shell',
activeLeafId: sourceLeafId,
layout: null
}
],
leaves: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
leafId: sourceLeafId,
paneRuntimeId: 1,
ptyId: 'pty-source',
paneTitle: null
}
]
})
registerLivePty(runtime, 'pty-source', TEST_WORKTREE_ID, null, {
tabId,
leafId: sourceLeafId
})
const sourceHandle = runtime.getTerminalHandleForPaneKey(makePaneKey(tabId, sourceLeafId))
expect(sourceHandle).not.toBeNull()
const [horizontal, vertical] = await Promise.all([
runtime.splitTerminal(sourceHandle!, { direction: 'horizontal' }),
runtime.splitTerminal(sourceHandle!, { direction: 'vertical' })
])
expect(splitTerminal).not.toHaveBeenCalled()
expect(horizontal.leafId).not.toBe(vertical.leafId)
const leafIds = collectLeafIds(getSession().terminalLayoutsByTabId[tabId]?.root)
expect(leafIds).toEqual(
expect.arrayContaining([sourceLeafId, horizontal.leafId, vertical.leafId])
)
for (const split of [horizontal, vertical]) {
expect(split.tabId).toBe(tabId)
const leafId = split.leafId ?? ''
const call = spawn.mock.calls.find(([args]) => args.leafId === leafId)
expect(spawnedPtyEnv(call?.[0]).ORCA_PANE_KEY).toBe(makePaneKey(tabId, leafId))
}
expect(spawn).toHaveBeenCalledTimes(2)
}
)
it.skipIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
'returns the exact pre-minted leaf for concurrent renderer-backed splits',
async () => {
const tabId = 'tab-concurrent-splits'
const sourceLeafId = '11111111-1111-4111-8111-111111111111'
const splitTerminal = vi.fn()
const runtime = new OrcaRuntimeService(store)
runtime.setNotifier({ splitTerminal } as never)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
title: 'shell',
activeLeafId: sourceLeafId,
layout: null
}
],
leaves: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
leafId: sourceLeafId,
paneRuntimeId: 1,
ptyId: 'pty-source',
paneTitle: null
}
]
})
const sourceHandle = runtime.getTerminalHandleForPaneKey(makePaneKey(tabId, sourceLeafId))
expect(sourceHandle).not.toBeNull()
const horizontal = runtime.splitTerminal(sourceHandle!, { direction: 'horizontal' })
const vertical = runtime.splitTerminal(sourceHandle!, { direction: 'vertical' })
await vi.waitFor(() => expect(splitTerminal).toHaveBeenCalledTimes(2))
const horizontalLeafId = splitTerminal.mock.calls.find(
(call) => call[2]?.direction === 'horizontal'
)?.[2]?.newLeafId
const verticalLeafId = splitTerminal.mock.calls.find(
(call) => call[2]?.direction === 'vertical'
)?.[2]?.newLeafId
expect(horizontalLeafId).toEqual(expect.any(String))
expect(verticalLeafId).toEqual(expect.any(String))
expect(horizontalLeafId).not.toBe(verticalLeafId)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
title: 'shell',
activeLeafId: verticalLeafId,
layout: null
}
],
// Reverse publication order so a first-new-leaf heuristic would swap the receipts.
leaves: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
leafId: verticalLeafId!,
paneRuntimeId: 3,
ptyId: 'pty-vertical',
paneTitle: null
},
{
tabId,
worktreeId: TEST_WORKTREE_ID,
leafId: horizontalLeafId!,
paneRuntimeId: 2,
ptyId: 'pty-horizontal',
paneTitle: null
},
{
tabId,
worktreeId: TEST_WORKTREE_ID,
leafId: sourceLeafId,
paneRuntimeId: 1,
ptyId: 'pty-source',
paneTitle: null
}
]
})
await expect(horizontal).resolves.toMatchObject({ leafId: horizontalLeafId })
await expect(vertical).resolves.toMatchObject({ leafId: verticalLeafId })
}
)
it('keeps a persisted split when mounted renderer adoption rejects', async () => {
const tabId = 'persisted-mounted-tab'
const ptyId = 'persisted-mounted-pty'
const splitPtyId = 'persisted-mounted-split-pty'
const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
id: tabId,
ptyId,
worktreeId: TEST_WORKTREE_ID,
title: 'Persisted terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
[tabId]: makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId })
}
})
)
const revealTerminalSession = vi.fn().mockRejectedValue(new Error('renderer rejected'))
const kill = vi.fn(() => true)
let resolveSpawn!: (result: { id: string }) => void
const spawn = vi.fn(
(_args: unknown) =>
new Promise<{ id: string }>((resolve) => {
resolveSpawn = resolve
})
)
const runtime = new OrcaRuntimeService(runtimeStore as never)
runtime.setPtyController({
spawn,
write: () => true,
kill,
getForegroundProcess: async () => null
})
runtime.setNotifier({ revealTerminalSession } as never)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
runtime.registerPty(ptyId, TEST_WORKTREE_ID, null, {
tabId,
leafId: HEADLESS_LEAF_ID,
incarnationId: 'live-source-incarnation'
})
const internals = runtime as unknown as {
issuePtyHandle: (pty: unknown) => string
ptysById: Map<string, unknown>
}
const handle = internals.issuePtyHandle(internals.ptysById.get(ptyId))
const split = runtime.splitTerminal(handle, { direction: 'horizontal' })
await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce())
const splitSpawn = spawn.mock.calls[0]?.[0] as
| { expectedSourceBinding?: { incarnationId?: string } }
| undefined
// An unsaved incarnation cannot satisfy the store's persisted-binding fence.
expect(splitSpawn?.expectedSourceBinding).not.toHaveProperty('incarnationId')
runtime.syncWindowGraph(1, {
tabs: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
title: 'Persisted terminal',
activeLeafId: HEADLESS_LEAF_ID,
layout: { type: 'leaf', leafId: HEADLESS_LEAF_ID }
}
],
leaves: [
{
tabId,
worktreeId: TEST_WORKTREE_ID,
leafId: HEADLESS_LEAF_ID,
paneRuntimeId: 1,
ptyId
}
]
})
resolveSpawn({ id: splitPtyId })
await expect(split).resolves.toMatchObject({
tabId,
handle: expect.stringMatching(/^term_/)
})
expect(revealTerminalSession).toHaveBeenCalledOnce()
expect(kill).not.toHaveBeenCalled()
expect(getSession().terminalLayoutsByTabId[tabId]?.root).toMatchObject({
type: 'split',
direction: 'horizontal'
})
})
it('rejects a projected split retired during spawn before publishing the new pane', async () => {
let resolveSplitSpawn!: (result: { id: string }) => void
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'projected-source-pty' })
.mockImplementationOnce(
() =>
new Promise<{ id: string }>((resolve) => {
resolveSplitSpawn = resolve
})
)
const kill = vi.fn(() => true)
const { runtimeStore, setSession } = makeRuntimeStoreWithWorkspaceSession(
getDefaultWorkspaceSession()
)
const runtime = new OrcaRuntimeService(runtimeStore)
runtime.setPtyController({
spawn,
write: () => true,
kill,
getForegroundProcess: async () => null
})
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`)
const split = runtime.splitTerminal(handle, { direction: 'vertical' })
await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2))
runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID)
setSession(getDefaultWorkspaceSession())
resolveSplitSpawn({ id: 'retired-projected-split-pty' })
await expect(split).rejects.toThrow('terminal_split_source_not_found')
expect(kill).toHaveBeenCalledWith('retired-projected-split-pty')
expect(runtime['mobileSessionTabsByWorktree'].has(TEST_WORKTREE_ID)).toBe(false)
})
})
@@ -1,11 +1,11 @@
// Normalize records frames into per-workspace whole-layout events so both layout ownership modes share assertions.
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
import { applyWorkspaceLayoutRecordChange } from '../../../shared/workspace-layout/workspace-layout-records'
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/published-layout/workspace-layout-published'
import { applyWorkspaceLayoutRecordChange } from '../../../shared/workspace-layout/published-layout/workspace-layout-records'
import type {
WorkspaceLayoutEvent,
WorkspaceLayoutStreamEvent
} from '../../../shared/workspace-layout/workspace-layout-stream-frames'
} from '../../../shared/workspace-layout/published-layout/workspace-layout-stream-frames'
export type LayoutEventCollector = {
/** One per workspace changed, in frame order. */
@@ -1,170 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { RuntimeClientEvent } from '../../../shared/runtime-client-events'
import type { WorktreeStartupLaunch } from '../../../shared/worktree/launch-types'
import './runtime-test-module-registration.spec'
import {
computeWorktreePathMock,
ensurePathWithinWorkspaceMock
} from '../orca-runtime-test-mocks/setup.spec'
import { createSetupRunnerScript } from '../../workspace-hooks/worktree-runner-script'
import { getEffectiveHooks } from '../../workspace-hooks/hooks'
import { listWorktrees } from '../../git/worktrees/catalog/worktree-scan-cache'
import { setRuntimeDesktopSurface } from '../construction/runtime-desktop-surface'
import { shouldRunSetupForCreate } from '../../workspace-hooks/effective-hook-config'
import { makeRuntimeStoreWithWorkspaceSession, store } from './orca-runtime-test-fixtures.spec'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { collectLeafIds } from '../../../shared/workspace-layout/terminal-pane-layout-tree'
import { ProvisioningRuntime } from './orca-runtime-layout-owner-start-fixture'
const WORKTREE_PATH = '/tmp/workspaces/runtime-setup-all'
const RUNNER = '/tmp/repo/.git/orca/setup-runner.sh'
async function createActivated(options: {
startup?: WorktreeStartupLaunch
failAgentSpawn?: boolean
}): Promise<{
activateWorktree: ReturnType<typeof vi.fn>
events: RuntimeClientEvent[]
spawnedCommands: (string | undefined)[]
runtime: ProvisioningRuntime
session: () => WorkspaceSessionState
}> {
const mainLayout = makeRuntimeStoreWithWorkspaceSession(getDefaultWorkspaceSession())
const runtime = new ProvisioningRuntime(
ORCA_LAYOUT_OWNER_IS_MAIN ? mainLayout.runtimeStore : store
)
const activateWorktree = vi.fn()
let spawned = 0
const spawn = vi.fn(async (args: { command?: string }) => {
if (options.failAgentSpawn && args.command === options.startup?.command) {
throw new Error('agent_session_exited_during_start')
}
return { id: `pty-${++spawned}` }
})
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree,
createTerminal: vi.fn(),
revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-revealed' }),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
// Keep a live window to exercise foreground provisioning.
const liveWindow = { isDestroyed: () => false, webContents: { isDestroyed: () => false } }
setRuntimeDesktopSurface({
showNotification: () => false,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the create path reads only the window's liveness.
findWindowById: () => liveWindow as never,
onIpc: () => {},
removeIpcListener: () => {}
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
const events: RuntimeClientEvent[] = []
const unsubscribe = runtime.onClientEvent((event) => events.push(event))
computeWorktreePathMock.mockReturnValue(WORKTREE_PATH)
ensurePathWithinWorkspaceMock.mockReturnValue(WORKTREE_PATH)
vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } })
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
vi.mocked(createSetupRunnerScript).mockResolvedValue({
runnerScriptPath: RUNNER,
envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: WORKTREE_PATH }
})
vi.mocked(listWorktrees).mockResolvedValue([
{
path: WORKTREE_PATH,
head: 'def',
branch: 'runtime-setup-all',
isBare: false,
isMainWorktree: false
}
])
try {
await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-setup-all',
setupDecision: 'run',
activate: true,
navigation: 'all',
...(options.startup ? { startup: options.startup } : {})
})
} finally {
unsubscribe()
setRuntimeDesktopSurface(null)
}
return {
activateWorktree,
events,
spawnedCommands: spawn.mock.calls.map(([args]) => args.command),
runtime,
session: mainLayout.getSession
}
}
describe('OrcaRuntimeService setup creator', () => {
it('creates the setup terminal once when an activated create navigates the window and clients', async () => {
const { activateWorktree, events, spawnedCommands } = await createActivated({})
const hostHandoffs = activateWorktree.mock.calls.filter((call) => call[2] !== undefined)
const clientHandoffs = events.filter(
(event) => event.type === 'activateWorktree' && event.setup !== undefined
)
expect({
hostHandoffs: hostHandoffs.length,
clientHandoffs: clientHandoffs.length,
setupSpawns: spawnedCommands.filter((command) => command?.includes(RUNNER)).length
}).toEqual({ hostHandoffs: 0, clientHandoffs: 0, setupSpawns: 1 })
})
it('hands the host window the whole create when the agent fails to start', async () => {
const startup = { command: 'claude' }
const { activateWorktree, events, spawnedCommands, runtime, session } = await createActivated({
startup,
failAgentSpawn: true
})
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
expect(spawnedCommands).toEqual(['claude', expect.stringContaining(RUNNER)])
expect(activateWorktree.mock.calls).toEqual([
['repo-1', expect.any(String), undefined, undefined, undefined]
])
const [agentRow] = Object.values(session().tabsByWorktree).flat()
const [agentLeafId] = collectLeafIds(session().terminalLayoutsByTabId[agentRow!.id]?.root)
expect(runtime.startStatusOf(`${agentRow!.id}:${agentLeafId}`)).toEqual({
status: 'failed',
reason: 'agent_session_exited_during_start'
})
expect(
events.filter((event) => event.type === 'activateWorktree' && event.setup !== undefined)
).toEqual([])
return
}
expect(spawnedCommands).toEqual(['claude'])
expect(activateWorktree).toHaveBeenCalledOnce()
expect(activateWorktree).toHaveBeenCalledWith(
'repo-1',
expect.any(String),
expect.objectContaining({ runnerScriptPath: RUNNER }),
expect.objectContaining({ command: 'claude' }),
undefined
)
expect(
events.filter((event) => event.type === 'activateWorktree' && event.setup !== undefined)
).toEqual([])
})
})
+1 -1
View File
@@ -19,7 +19,7 @@ import { ORCHESTRATION_SESSION_CALLER_ERROR_CODES } from '../../../shared/orches
import { ORCHESTRATION_FEDERATION_ERROR_CODES } from '../../../shared/orchestration/orchestration-federation-error-codes'
import { computerUseErrorRecoveryData } from '../../../shared/computer-use-error-recovery'
import { COMPUTER_ERROR_CODES } from '../../../shared/computer-use-runtime-types'
import { TerminalSplitOutcomeError } from '../../../shared/workspace-layout/terminal-split-outcome'
import { TerminalSplitOutcomeError } from '../../../shared/workspace-layout/terminal-topology/terminal-split-outcome'
import { WslHostWindowsDriveRefusalError } from '../../../shared/wsl-host-windows-drive-refusal'
import { wslDistroHoldRpcFailure } from '../../../shared/wsl-distro-stopped-hold'
import { LINEAR_ERROR_CODES } from '../../../shared/linear/agent-access'
@@ -1,14 +1,11 @@
import { withoutGitGraph } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import { withoutGitGraph } from '../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import type { LayoutCommand } from '../../../shared/workspace-layout/workspace-layout-command-types'
import type { WorkspaceLayoutModel } from '../../../shared/workspace-layout/workspace-layout-model'
import { applyLayoutCommand } from '../../../shared/workspace-layout/workspace-layout-commands'
import { expect, it } from 'vitest'
import { createGitGraphFrameProjection, projectGitGraphLayout } from './git-graph-layout-projection'
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
import {
diffPublishedLayouts,
indexPublishedLayout
} from '../../../shared/workspace-layout/workspace-layout-records'
import './git-graph-layout-projection'
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/published-layout/workspace-layout-published'
import '../../../shared/workspace-layout/published-layout/workspace-layout-records'
function layout(): PublishedWorkspaceLayout {
return {
@@ -38,53 +35,6 @@ function layout(): PublishedWorkspaceLayout {
}
}
it('hides graph tabs in snapshots and later record frames while preserving sequence', () => {
const original = layout()
const project = createGitGraphFrameProjection()
expect(
project({ type: 'snapshot', seq: 1, workspaces: [{ key: 'w', layout: original }] })
).toMatchObject({
workspaces: [
{
layout: {
groups: [
{ id: 'g1', tabIds: ['a', 'b'] },
{ id: 'g2', tabIds: ['c'] }
]
}
}
]
})
const after = {
...original,
groups: [
{ id: 'g1', tabIds: ['graph'] },
{ id: 'g2', tabIds: ['a', 'b', 'c'] }
]
}
const change = diffPublishedLayouts(
'w',
indexPublishedLayout(original),
indexPublishedLayout(after),
after
)!
const frame = project({ type: 'records', epoch: 'e', seq: 2, workspaces: [change] })
expect(frame).toMatchObject({
type: 'records',
epoch: 'e',
seq: 2,
workspaces: [
{
workspace: {
groups: [{ id: 'g2', tabIds: ['a', 'b', 'c'] }],
groupLayout: { type: 'leaf', groupId: 'g2' }
}
}
]
})
expect(projectGitGraphLayout(after).tabs.map((tab) => tab.id)).toEqual(['a', 'b', 'c'])
})
function canonical(original = layout()): WorkspaceLayoutModel {
return {
hostId: 'local',
@@ -403,76 +353,3 @@ it('places a detached pane at a visible gap and leaves the hidden graph untouche
{ id: 'g2', tabOrder: ['c', 'minted'] }
])
})
it('projects graph add/remove around binding-only deltas without losing visible updates', () => {
const original = layout()
original.tabs = original.tabs.map((tab) =>
tab.id === 'c'
? {
...tab,
kind: 'terminal',
terminal: {
defaultTitle: 'Terminal 1',
root: { type: 'leaf', leafId: 'one' },
panes: [{ leafId: 'one', ptyId: 'initial' }]
}
}
: tab
)
const graphFree = {
...original,
tabs: original.tabs.filter((tab) => tab.id !== 'graph'),
groups: [
{ id: 'g1', tabIds: ['a', 'b'] },
{ id: 'g2', tabIds: ['c'] }
]
}
const project = createGitGraphFrameProjection()
project({ type: 'workspace', key: 'w', seq: 1, layout: graphFree })
const binding = {
key: 'w',
change: 'records' as const,
bindings: { set: [{ leafId: 'one', ptyId: 'pty' }] }
}
const before = project({ type: 'records', epoch: 'e', seq: 2, workspaces: [binding] })
expect(before.type === 'records' && before.workspaces[0]).toBe(binding)
const added = project({
type: 'records',
epoch: 'e',
seq: 3,
workspaces: [
diffPublishedLayouts(
'w',
indexPublishedLayout(graphFree),
indexPublishedLayout(original),
original
)!
]
})
expect(added).toMatchObject({ workspaces: [] })
const whileHidden = project({
type: 'records',
epoch: 'e',
seq: 4,
workspaces: [{ ...binding, bindings: { set: [{ leafId: 'one', ptyId: 'next' }] } }]
})
expect(whileHidden).toMatchObject({
workspaces: [{ key: 'w', bindings: { set: [{ leafId: 'one', ptyId: 'next' }] } }]
})
const removed = project({
type: 'records',
epoch: 'e',
seq: 5,
workspaces: [
diffPublishedLayouts(
'w',
indexPublishedLayout(original),
indexPublishedLayout(graphFree),
graphFree
)!
]
})
expect(removed).toMatchObject({ workspaces: [] })
const after = project({ type: 'records', epoch: 'e', seq: 6, workspaces: [binding] })
expect(after.type === 'records' && after.workspaces[0]).toBe(binding)
})
@@ -1,6 +1,6 @@
import { GIT_GRAPH_TAB_CAPABILITY } from '../../../shared/source-control/runtime/git-graph-capabilities'
import { visibleGroupTree } from '../../../shared/workspace-layout/workspace-layout-client-visibility'
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
import { visibleGroupTree } from '../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/published-layout/workspace-layout-published'
import {
applyPublishedLayoutRecordChanges,
splitPublishedLayout,
@@ -8,8 +8,8 @@ import {
type PublishedLayoutRecords,
diffPublishedLayouts,
indexPublishedLayout
} from '../../../shared/workspace-layout/workspace-layout-records'
import type { WorkspaceLayoutStreamFrame } from '../../../shared/workspace-layout/workspace-layout-stream-frames'
} from '../../../shared/workspace-layout/published-layout/workspace-layout-records'
import type { WorkspaceLayoutStreamFrame } from '../../../shared/workspace-layout/published-layout/workspace-layout-stream-frames'
export function readsGitGraph(capabilities?: readonly string[]): boolean {
return capabilities?.includes(GIT_GRAPH_TAB_CAPABILITY) === true
}
@@ -2,7 +2,7 @@ import '../../../../../shared/protocol-version'
import { SESSION_TABS_RETIREMENT_PROOF_DELTA_RUNTIME_CAPABILITY } from '../../../../../shared/session-tabs-runtime-capabilities'
import type { RuntimeCapability } from '../../../../../shared/protocol-version'
import type { RuntimeMobileSessionRetiredTerminalSurface } from '../../../../../shared/runtime-session-contracts'
import { retirementProofKey as proofKey } from '../../../../../shared/workspace-layout/terminal-retirement-proof-ledger'
import { retirementProofKey as proofKey } from '../../../../../shared/workspace-layout/terminal-surfaces/terminal-retirement-proof-ledger'
type ProofCarrier = {
worktree: string
@@ -1,8 +1,5 @@
import { RUNTIME_CAPABILITIES } from '../../../../../shared/protocol-version'
import {
LAYOUT_INTENTS_RUNTIME_CAPABILITY,
LAYOUT_REOPEN_HELD_PANES_CAPABILITY
} from '../../../../../shared/workspace-layout/layout-intents-capability'
import '../../../../../shared/protocol-version'
import '../../../../../shared/workspace-layout/published-layout/layout-intents-capability'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { WorkspaceSessionState } from '../../../../../shared/workspace-session/workspace-session-state-types'
@@ -11,11 +8,7 @@ import type { ExecutionHostId } from '../../../../../shared/execution-host'
import type { LayoutApplyOutcome } from '../../../../../shared/workspace-layout/workspace-layout-command-types'
import type { Tab } from '../../../../../shared/tab-types'
import type { RuntimeStore } from '../../../construction/runtime-store-contract'
import '../../../../../shared/workspace-layout/workspace-layout-client-visibility'
import '../../../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import '../../server/runtime-rpc-mobile-method-allowlist'
import { OrcaRuntimeService } from '../../../orca-runtime'
@@ -76,68 +69,6 @@ afterEach(() => {
})
describe('layout.apply', () => {
it('does not advertise layout mutations in a window-owned build', () => {
expect(RUNTIME_CAPABILITIES).not.toContain(LAYOUT_INTENTS_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).not.toContain(LAYOUT_REOPEN_HELD_PANES_CAPABILITY)
})
it('gives each title in its tab’s partition in one write, naming a paired server’s tab', async () => {
const applyTabRenames = vi.fn<NonNullable<RuntimeStore['applyTabRenames']>>(async (batches) => [
{ rename: batches[0]!.renames[0]!, code: 'tab_not_found' }
])
const hosts: Record<string, string> = { 'tab-1': 'local', 'tab-2': 'ssh:target-1' }
const store: PartialRuntimeStore = {
getRepo: () => undefined,
getWorkspaceSessionHostIds: () => ['local', 'ssh:target-1', 'runtime:env-1'],
getWorkspaceSession: (host: string) => ({
...sessionListing('other'),
tabsByWorktree: {},
unifiedTabs: {
[WORKTREE]: Object.entries({ ...hosts, 'tab-3': 'runtime:env-1' })
.filter(([, owner]) => owner === host)
.map(([id]): Tab => ({
id,
entityId: id,
groupId: 'group-1',
worktreeId: WORKTREE,
contentType: 'terminal',
label: id,
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}))
}
}),
applyTabRenames
}
const runtime = new OrcaRuntimeService(partialRuntimeStore(store))
const titled = (tabId: string) => ({ workspace: WORKTREE, tabId, title: `Title ${tabId}` })
const reply = await runtime.applyLayoutCommand({
type: 'renameTabs',
kind: 'generated',
renames: [titled('tab-1'), titled('tab-2'), titled('tab-3')]
})
const renameOf = (tabId: string) => ({ type: 'renameTab', kind: 'generated', ...titled(tabId) })
expect(applyTabRenames).toHaveBeenCalledTimes(1)
expect(applyTabRenames).toHaveBeenCalledWith([
{ hostId: 'local', renames: [renameOf('tab-1')] },
{ hostId: 'ssh:target-1', renames: [renameOf('tab-2')] }
])
expect(reply).toEqual({
ok: true,
result: {
untitled: [
{ workspace: WORKTREE, tabId: 'tab-3', code: 'host_not_owned' },
{ workspace: WORKTREE, tabId: 'tab-1', code: 'tab_not_found' }
]
},
frameSeq: 0
})
})
it('names the stream position of the frame its write produced, sent before the reply', async () => {
let session = sessionListing('tab-1')
const written = new Set<(hostId: ExecutionHostId | null) => void>()
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import type { OrcaRuntimeService } from '../../../orca-runtime'
import type { WorkspaceLayoutStreamEvent } from '../../../../../shared/workspace-layout/workspace-layout-stream-frames'
import type { WorkspaceLayoutStreamEvent } from '../../../../../shared/workspace-layout/published-layout/workspace-layout-stream-frames'
import { eraseRpcMethods, isStreamingMethod, type RpcContext } from '../../core'
import { ALL_RPC_METHODS } from '../rpc-method-registry'
@@ -1,4 +1,4 @@
import { withoutGitGraph } from '../../../../../shared/workspace-layout/workspace-layout-client-visibility'
import { withoutGitGraph } from '../../../../../shared/workspace-layout/client-views/workspace-layout-client-visibility'
import { readsGitGraph, createGitGraphFrameProjection } from '../../git-graph-layout-projection'
// Paired desktops use layout read/stream/apply only when the server offers layout.intents.v1; mobile is excluded.
@@ -9,7 +9,7 @@ import {
LayoutUnsubscribeParams
} from '../../../../../shared/rpc-contract/workspace-layout-params'
import { LayoutReopenHeldPanesParams } from '../../../../../shared/rpc-contract/workspace-layout-held-panes-params'
import type { WorkspaceLayoutStreamFrame } from '../../../../../shared/workspace-layout/workspace-layout-stream-frames'
import type { WorkspaceLayoutStreamFrame } from '../../../../../shared/workspace-layout/published-layout/workspace-layout-stream-frames'
import { defineMethod, defineStreamingMethod } from '../../core'
import { getAppEnvironment } from '../../../../../shared/app-environment'
import { listEnvironments } from '../../../../../shared/remote-runtime/host-store/runtime-environment-store'
@@ -22,7 +22,7 @@ import {
import type {
TerminalPaneCloseTarget,
TerminalSurfaceCloseTarget
} from '../../../../shared/workspace-layout/terminal-surface-close-target'
} from '../../../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target'
import { retireTerminalSurfacesFromSnapshot } from '../../terminal/surface/mobile-session-terminal-retirement'
import type { PtyControllerInventory } from '../../terminal/lifecycle/runtime-pty-controller-contract'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
@@ -4,7 +4,7 @@ import type {
} from '../../../../shared/runtime-mobile-session-tab-contracts'
import type { RuntimeMobileSessionTabGroup } from '../../../../shared/runtime-session-contracts'
import type { TabGroupLayoutNode } from '../../../../shared/tab-types'
import { collectTabGroupLayoutGroupIds } from '../../../../shared/workspace-layout/tab-group-moves'
import { collectTabGroupLayoutGroupIds } from '../../../../shared/workspace-layout/tab-groups/tab-group-moves'
import { isStructuredAgentSessionHostTabId } from '../../../../shared/agent-session/view/structured-agent-session-host-tab-id'
import {
collectHeadlessParentTabOrder,
@@ -1,6 +1,6 @@
import { normalizeMobileSessionTabSelection } from '../../session-tabs/mobile-session-tab-selection'
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-session-contracts'
import { dropRetirementProofsForLiveSurfaces } from '../../../../shared/workspace-layout/terminal-retirement-proof-ledger'
import { dropRetirementProofsForLiveSurfaces } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-retirement-proof-ledger'
import { orderMobileSessionTabsByGroups } from './mobile-session-tab-merge'
import type {
RuntimeMobileSessionProjectionHost,
@@ -3,7 +3,7 @@ import type {
TerminalPaneLayoutNode,
TerminalTab
} from '../../../shared/workspace-layout/terminal-tab-types'
import { sanitizeTerminalLayoutPaneTitles } from '../../../shared/workspace-layout/terminal-pane-title-sanitization'
import { sanitizeTerminalLayoutPaneTitles } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-title-sanitization'
function collectLeafIds(node: TerminalPaneLayoutNode | null): string[] {
if (!node) {
@@ -14,7 +14,7 @@ import { editorRowHost } from '../../../shared/workspace-session/session-tab-fil
import {
editorTabNamesFile,
sameEditorFile
} from '../../../shared/workspace-layout/workspace-layout-editor-tab-file'
} from '../../../shared/workspace-layout/editor-tabs/workspace-layout-editor-tab-file'
import type {
PersistedOpenFile,
WorkspaceSessionState
@@ -1,87 +0,0 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from '../../../shared/constants'
import {
HEADLESS_PHONE_LAYOUT_CASES,
HEADLESS_PHONE_WORKTREES,
type PhoneLayoutView
} from '../../../shared/workspace-layout/headless-phone-layout.test-fixture'
import { leaf } from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import { deriveHeadlessLegacyTerminalLeafId } from '../terminal/surface/mobile-session-layout-projection'
import { OrcaRuntimeService } from '../orca-runtime'
const repos = [
{ id: 'repo-1', path: '/repos', displayName: 'repo-1', badgeColor: 'blue', addedAt: 1 }
]
function hydratedRuntime(session: WorkspaceSessionState) {
const runtime = new OrcaRuntimeService({
getRepo: (id: string) => repos.find((repo) => repo.id === id),
getRepos: () => repos,
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
addRepo: () => {},
updateRepo: () => {
throw new Error('unused')
},
setWorktreeMeta: () => {
throw new Error('unused')
},
removeWorktreeMeta: () => {},
getGitHubCache: () => ({ pr: {}, issue: {} }),
getRetiredWorktreeNameRegistry: () => ({ exhaustedTiers: 0, names: [] }),
addRetiredWorktreeName: () => {},
mergeRetiredWorktreeNames: () => false,
getSettings: () => getDefaultSettings('/tmp/home'),
getWorkspaceSession: () => session,
setWorkspaceSession: () => {}
})
runtime.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(undefined, { force: true })
return runtime
}
function phoneView(session: WorkspaceSessionState): PhoneLayoutView {
const view: PhoneLayoutView = {}
for (const [worktreeId, snapshot] of hydratedRuntime(session).mobileSessionTabsByWorktree) {
view[worktreeId] = {
panes: snapshot.tabs.flatMap((tab) =>
tab.type === 'terminal' ? [`${tab.parentTabId} ${tab.ptyId ?? '-'} ${tab.title}`] : []
),
active: snapshot.activeTabId?.split('::')[0] ?? null,
groups: (snapshot.tabGroups ?? []).map((group) => ({
tabs: group.tabOrder,
active: group.activeTabId
}))
}
}
return view
}
describe('headless phone terminal tabs', () => {
it.each(HEADLESS_PHONE_LAYOUT_CASES)('$name', (scenario) => {
for (const worktreeId of scenario.phoneFollowsDesktop) {
expect(scenario.desktop[worktreeId]).not.toEqual(scenario.main[worktreeId])
}
const expected = Object.fromEntries(
Object.entries(scenario.main).map(([worktreeId, view]) => [
worktreeId,
scenario.phoneFollowsDesktop.includes(worktreeId) ? scenario.desktop[worktreeId] : view
])
)
expect(phoneView(scenario.build())).toEqual(expected)
})
it('keeps pane ids in worktrees with no absorbed row', () => {
const scenario = HEADLESS_PHONE_LAYOUT_CASES.find((entry) =>
entry.name.startsWith('duplicate in one worktree')
)!
const { B, C, D } = HEADLESS_PHONE_WORKTREES
const leafIds = (worktreeId: string) =>
hydratedRuntime(scenario.build())
.mobileSessionTabsByWorktree.get(worktreeId)
?.tabs.flatMap((tab) => (tab.type === 'terminal' ? [tab.leafId] : []))
expect(leafIds(B)).toEqual([leaf(4), leaf(5)])
expect(leafIds(C)).toEqual([leaf(6)])
expect(leafIds(D)).toEqual([deriveHeadlessLegacyTerminalLeafId('tab-old')])
})
})
@@ -1,240 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import '../orca-runtime-tests/runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../orca-runtime'
import {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal
} from '../orca-runtime-tests/orca-runtime-test-fixtures.spec'
import type { RuntimeStore } from '../construction/runtime-store-contract'
import { planTerminalLeafMove } from '../../persistence/terminal-topology/terminal-leaf-move'
import type { TerminalLeafMoveRequest } from '../../../shared/workspace-layout/terminal-leaf-move'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import type { Tab } from '../../../shared/tab-types'
import { ownedEditorFileId } from '../../../shared/workspace-session/workspace-session-pane-ownership'
const SIBLING_LEAF_ID = '33333333-3333-4333-8333-333333333333'
const OTHER_LEAF_ID = '44444444-4444-4444-8444-444444444444'
async function setup(
withSession: (session: WorkspaceSessionState) => WorkspaceSessionState = (session) => session,
/** Runs while the move's commit is pending, as another writer would. */
duringCommit: (session: WorkspaceSessionState) => WorkspaceSessionState = (session) => session
) {
const base = makeWorkspaceSessionWithHeadlessTerminal()
const hostTab = base.tabsByWorktree[TEST_WORKTREE_ID]![0]!
const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(
withSession({
...base,
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
hostTab,
{ ...hostTab, id: 'other-tab', ptyId: 'other-pty', title: 'Other', sortOrder: 1 }
]
},
terminalLayoutsByTabId: {
'other-tab': {
root: { type: 'leaf', leafId: OTHER_LEAF_ID },
activeLeafId: OTHER_LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [OTHER_LEAF_ID]: 'other-pty' }
},
'host-tab': {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: HEADLESS_LEAF_ID },
second: { type: 'leaf', leafId: SIBLING_LEAF_ID }
},
activeLeafId: HEADLESS_LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [HEADLESS_LEAF_ID]: 'persisted-pty', [SIBLING_LEAF_ID]: 'sibling-pty' }
}
}
})
)
// The host's real planner over the shared session, as Store.moveTerminalLeafToNewTab runs it.
const commit = vi.fn(async (request: TerminalLeafMoveRequest) => {
setSession(duringCommit(getSession()))
const plan = planTerminalLeafMove([{ hostId: 'local', session: getSession() }], request)
if (plan.sessions[0]) {
setSession(plan.sessions[0].session)
}
return plan.result
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture implements RuntimeStore for this runtime.
const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore)
await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
const published: unknown[] = []
runtime.onMobileSessionTabsChanged((snapshot) => published.push(snapshot))
const move = (
targetTabId = 'host-new-tab',
placement?: Parameters<typeof runtime.moveMobileSessionLeafToNewTab>[3]
) =>
runtime.moveMobileSessionLeafToNewTab(
`id:${TEST_WORKTREE_ID}`,
{ sourceTabId: 'host-tab', targetTabId, leafId: SIBLING_LEAF_ID },
commit,
placement
)
return { runtime, getSession, published, commit, move }
}
describe('moveMobileSessionLeafToNewTab on a headless host', () => {
it('moves the live terminal with its pane, so tab-scoped reads find it in the new tab', async () => {
const { runtime, move } = await setup()
runtime.registerPty('sibling-pty', TEST_WORKTREE_ID, null, {
tabId: 'host-tab',
leafId: SIBLING_LEAF_ID
})
await expect(move()).resolves.toEqual({ status: 'moved', ptyId: 'sibling-pty' })
const { terminals } = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
expect(terminals.find((terminal) => terminal.leafId === SIBLING_LEAF_ID)?.tabId).toBe(
'host-new-tab'
)
})
it('keeps a tab opened while the move commits, after the tabs the user had', async () => {
const { runtime, move } = await setup(withFileBetweenTerminals, withLateTerminal)
const worktree = `id:${TEST_WORKTREE_ID}`
const listed = async () => (await runtime.listMobileSessionTabs(worktree)).tabGroups?.[0]
const group = (await listed())!
const file = group.tabOrder.find((id) => id !== 'host-tab' && id !== 'other-tab')!
await runtime.moveMobileSessionTab(worktree, {
kind: 'reorder',
tabId: file,
targetGroupId: group.id,
tabOrder: ['host-tab', file, 'other-tab']
})
await move('host-new-tab', { targetGroupId: group.id, index: 2 })
expect((await listed())?.tabOrder).toEqual([
'host-tab',
file,
'host-new-tab',
'other-tab',
'late-tab'
])
})
it('keeps a reorder another client made while the move committed', async () => {
let reorder: () => Promise<unknown> = async () => {}
const { runtime, move } = await setup(undefined, (session) => {
void reorder()
return session
})
const worktree = `id:${TEST_WORKTREE_ID}`
const listed = async () => (await runtime.listMobileSessionTabs(worktree)).tabGroups?.[0]
const group = (await listed())!
expect(group.tabOrder).toEqual(['host-tab', 'other-tab'])
reorder = async () => {
reorder = async () => {}
await runtime.moveMobileSessionTab(worktree, {
kind: 'reorder',
tabId: 'other-tab',
targetGroupId: group.id,
tabOrder: ['other-tab', 'host-tab']
})
}
await move('host-new-tab', { targetGroupId: group.id, index: 0 })
expect((await listed())?.tabOrder).toEqual(['host-new-tab', 'other-tab', 'host-tab'])
})
it('refuses a move that would empty its source, which no headless cleanup retires', async () => {
const { runtime, getSession, published, move } = await setup(undefined, (session) => ({
...session,
terminalLayoutsByTabId: {
...session.terminalLayoutsByTabId,
'host-tab': {
root: { type: 'leaf', leafId: SIBLING_LEAF_ID },
activeLeafId: SIBLING_LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [SIBLING_LEAF_ID]: 'sibling-pty' }
}
}
}))
await expect(move()).resolves.toEqual({ status: 'refused', reason: 'last_pane' })
expect(getSession().terminalLayoutsByTabId['host-tab']?.root).toEqual({
type: 'leaf',
leafId: SIBLING_LEAF_ID
})
expect(published).toHaveLength(0)
const { tabs } = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(tabs.some((tab) => tab.type === 'terminal' && tab.parentTabId === 'host-new-tab')).toBe(
false
)
})
})
/** A session whose worktree also has an editor open beside its two terminal tabs. */
function withFileBetweenTerminals(session: WorkspaceSessionState): WorkspaceSessionState {
const filePath = '/repo/notes.ts'
const entry = (
id: string,
entityId: string,
contentType: Tab['contentType'],
sortOrder: number
) => ({
id,
entityId,
groupId: 'group-1',
worktreeId: TEST_WORKTREE_ID,
contentType,
label: id,
customLabel: null,
color: null,
sortOrder,
createdAt: 1
})
return {
...session,
openFilesByWorktree: {
[TEST_WORKTREE_ID]: [
{ filePath, relativePath: 'notes.ts', worktreeId: TEST_WORKTREE_ID, language: 'typescript' }
]
},
unifiedTabs: {
[TEST_WORKTREE_ID]: [
entry('host-tab', 'host-tab', 'terminal', 0),
entry('file-tab', ownedEditorFileId(filePath, TEST_WORKTREE_ID, undefined), 'editor', 1),
entry('other-tab', 'other-tab', 'terminal', 2)
]
},
tabGroups: {
[TEST_WORKTREE_ID]: [
{
id: 'group-1',
worktreeId: TEST_WORKTREE_ID,
activeTabId: 'host-tab',
tabOrder: ['host-tab', 'file-tab', 'other-tab']
}
]
}
}
}
const LATE_LEAF_ID = '55555555-5555-4555-8555-555555555555'
/** A terminal tab another writer opens while the move's commit is pending. */
function withLateTerminal(session: WorkspaceSessionState): WorkspaceSessionState {
const tabs = session.tabsByWorktree[TEST_WORKTREE_ID] ?? []
if (tabs.some((tab) => tab.id === 'late-tab')) {
return session
}
return {
...session,
tabsByWorktree: {
...session.tabsByWorktree,
[TEST_WORKTREE_ID]: [
...tabs,
{ ...tabs[0]!, id: 'late-tab', ptyId: 'late-pty', title: 'Late', sortOrder: 9 }
]
},
terminalLayoutsByTabId: {
...session.terminalLayoutsByTabId,
'late-tab': {
root: { type: 'leaf', leafId: LATE_LEAF_ID },
activeLeafId: LATE_LEAF_ID,
expandedLeafId: null,
ptyIdsByLeafId: { [LATE_LEAF_ID]: 'late-pty' }
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More