From e902c9a65b691fdffcc5b4200bbf4fda2a12dc73 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 11 Nov 2026 20:54:34 -0800 Subject: [PATCH] refactor(workspace-layout): organize concrete domain owners (#41350) * refactor(workspace-layout): organize concrete domain owners * test(layout): retire obsolete legacy-leaf publication setup --- .github/workflows/layout-owner-e2e.yml | 2 +- config/flaky-test-pattern-baseline.txt | 5 - config/reliability-gates.jsonc | 15 +- .../scripts/ci/sharding/ci-shard-timings.json | 103 ++--- ...terminal-shutdown-byte-limit-benchmark.mjs | 7 +- .../check-workspace-layout-rules.mjs | 2 +- .../source-routing/pr-e2e-source-routing.mjs | 10 - .../source-routing/pr-layout-oracle-route.mjs | 2 +- src/cli/paired-terminal-desktop-reveal.ts | 2 +- .../agent-launch-pane-attachment.ts | 2 +- src/main/ipc/agent-status-ipc-boundary.ts | 2 +- .../ipc/pty/ipc/closed-pane-spawn-refusal.ts | 2 +- src/main/ipc/pty/ipc/leaf-move.ts | 2 +- src/main/ipc/pty/ipc/spawn-agent-launch.ts | 2 +- src/main/ipc/pty/ipc/spawn-commit.ts | 2 +- src/main/ipc/pty/ipc/spawn-env.ts | 2 +- src/main/ipc/pty/pane/adopt-stable.ts | 2 +- .../ipc/pty/runtime/spawn-commit-response.ts | 2 +- src/main/ipc/pty/runtime/spawn-options.ts | 2 +- src/main/ipc/pty/runtime/spawn-preflight.ts | 2 +- src/main/ipc/pty/runtime/spawn-state.ts | 4 +- src/main/ipc/pty/spawn-commit-assembly.ts | 2 +- src/main/ipc/pty/spawn-pane-coordinates.ts | 2 +- src/main/ipc/pty/spawn-pane-env.ts | 2 +- ...-shutdown-checkpoint-sleep-records.test.ts | 2 +- src/main/ipc/session-window-view-save.test.ts | 2 +- src/main/ipc/session.ts | 2 +- .../closed-terminal-tab-write-fence.ts | 2 +- .../pty-binding-fast-lane.ts | 2 +- .../pty-binding-persistence.ts | 4 +- .../terminal-bindings/pty-binding-refusals.ts | 6 +- .../pty-binding-session-update.ts | 2 +- .../terminal-bindings/pty-binding-span.ts | 4 +- .../workspace-sessions/layout-editor-views.ts | 2 +- .../session-host-partitions.ts | 2 +- .../session-snapshot-operations.ts | 2 +- .../workspace-session-snapshot-publication.ts | 2 +- .../orcad-destination-workspace-session.ts | 2 +- .../orcad-source-session-partitions.ts | 2 +- .../orcad-source-workspace-session.ts | 2 +- .../wsl-move-seeded-view-state.ts | 2 +- .../wsl-move-slept-scrollback.ts | 2 +- .../empty-terminal-layout-root-repair.ts | 2 +- .../pane-identity-migration.ts | 5 +- .../tab-terminal-pane-binding-repair.test.ts | 17 +- .../tab-terminal-pane-binding-repair.ts | 6 +- .../terminal-layout-normalization.test.ts | 6 +- .../terminal-layout-normalization.ts | 8 +- .../unattached-terminal-row-sweep.test.ts | 137 +----- .../closed-pane-binding-fence.ts | 2 +- .../cross-partition-tab-move.ts | 14 +- .../duplicate-pty-pane-repair.ts | 2 +- .../editor-renames-across-partitions.test.ts | 2 +- .../editor-tab-layout-apply.test.ts | 141 ------ .../layout-command-binding-generations.ts | 2 +- .../layout-command-persistence.ts | 2 +- ...sistence-closed-pane-binding-fence.test.ts | 41 +- .../terminal-late-bind-layout-rules.test.ts | 99 ---- .../terminal-layout-apply.test.ts | 39 +- .../terminal-layout-create-resend.test.ts | 2 +- .../terminal-layout-pane-ops.test.ts | 2 +- ...erminal-leaf-move-concurrent-close.test.ts | 69 +-- .../terminal-leaf-move-fixture.ts | 2 +- .../terminal-topology/terminal-leaf-move.ts | 8 +- .../terminal-owner-invariants.test.ts | 2 +- .../terminal-pane-admission.ts | 14 +- .../terminal-pane-placement-agreement.ts | 4 +- .../terminal-pane-placement-graft.ts | 4 +- .../terminal-topology-commit.ts | 4 +- .../terminal-topology-membership.ts | 2 +- .../terminal-topology/window-pane-detach.ts | 8 +- .../persistence/window-save-divergence.ts | 6 +- .../window-save-reconnect-facts.test.ts | 188 -------- src/main/persistence/window-save-view.ts | 12 +- .../__fixtures__/main-layout-close-fixture.ts | 2 +- .../agent-session/agent-session-tab-layout.ts | 6 +- .../headless/serve-browser-tab-layout.ts | 8 +- .../events/runtime-notifier-contract.ts | 6 +- ...client-session-tab-selection-projection.ts | 2 +- .../dark-switch-runtime-capabilities.ts | 2 +- .../runtime/construction/orca-runtime.test.ts | 4 +- .../bind-first-pane.test-fixture.ts | 2 +- ...orca-runtime-layout-owner-start-fixture.ts | 2 +- ...nal-creation-and-readiness-part-05.spec.ts | 330 -------------- ...space-layout-stream-events.test-fixture.ts | 6 +- .../worktree-setup-single-creator.spec.ts | 170 ------- src/main/runtime/rpc/errors.ts | 2 +- .../rpc/git-graph-layout-projection.test.ts | 131 +----- .../rpc/git-graph-layout-projection.ts | 8 +- .../session-tabs-retirement-proof-delta.ts | 2 +- .../workspace-layout-apply.test.ts | 75 +-- .../workspace-layout/workspace-layout.test.ts | 2 +- .../workspace-layout/workspace-layout.ts | 4 +- ...ld-headless-mobile-session-browser-tabs.ts | 2 +- .../projection/mobile-session-tab-merge.ts | 2 +- ...time-mobile-session-result-finalization.ts | 2 +- .../desktop-pane-title-forwarding.ts | 2 +- .../session-tabs/headless-editor-tabs.ts | 2 +- ...dless-mobile-session-absorbed-rows.test.ts | 87 ---- .../headless-move-leaf-to-new-tab.test.ts | 240 ---------- .../headless-pane-move-to-new-tab.ts | 4 +- .../session-tabs/headless-tab-group-moves.ts | 2 +- .../session-tabs/headless-tab-properties.ts | 2 +- .../headless-terminal-tab-close.ts | 2 +- .../main-layout-session-tab-move.ts | 2 +- .../structured-session-tab-publication.ts | 2 +- ...minal-create-closed-tab-durability.test.ts | 2 +- .../runtime-terminal-orphan-adoption-plan.ts | 2 +- ...-terminal-orphan-adoption-rollback.test.ts | 20 +- ...untime-terminal-orphan-session-adoption.ts | 8 +- .../runtime/terminal/focus/focus-terminal.ts | 2 +- .../identity/moved-pane-owner-rekey.ts | 2 +- .../runtime-terminal-pane-identity.ts | 2 +- .../mobile-terminal-chat-fork-create.ts | 4 +- .../launch/mobile-terminal-owned-create.ts | 2 +- .../terminal/launch/runtime-pane-admission.ts | 4 +- .../terminal/launch/terminal-split-in-main.ts | 2 +- .../terminal-split-layout-owner.test.ts | 169 ------- .../launch/terminal-split-retry.test.ts | 127 ------ .../terminal/launch/terminal-split-retry.ts | 2 +- .../layout/agent-generated-tab-titles.test.ts | 95 +--- .../layout/editor-view-layout-apply.test.ts | 2 +- .../editor-view-partition-changes.test.ts | 2 +- .../layout/headless-terminal-split-layout.ts | 2 +- .../terminal/layout/runtime-layout-apply.ts | 2 +- .../runtime-terminal-spawn-placement.ts | 2 +- .../lifecycle/closed-pane-of-exited-pty.ts | 2 +- .../exited-split-pane-layout-close.ts | 2 +- ...ca-runtime-exited-split-pane-close.test.ts | 49 +- .../terminal/lifecycle/pty-registration.ts | 2 +- .../runtime-pty-controller-contract.ts | 4 +- .../terminal-orchestration-authority.ts | 2 +- .../terminal/query/pty-terminal-summary.ts | 2 +- .../records/runtime-leaf-record-projection.ts | 10 +- .../terminal/recovery/held-pane-reopen.ts | 2 +- .../recovery/shutdown-reconnect-facts.ts | 2 +- ...session-terminal-persistence-retirement.ts | 2 +- .../mobile-session-terminal-projection.ts | 2 +- ...obile-session-terminal-retirement-proof.ts | 2 +- .../mobile-session-terminal-retirement.ts | 4 +- ...ime-failed-start-pane-layout-owner.test.ts | 98 ---- ...ime-terminal-close-spill-partition.spec.ts | 96 +--- .../terminal/surface/pane-live-facts.test.ts | 2 +- .../terminal/surface/pane-start-statuses.ts | 2 +- .../terminal/surface/pty-pane-query.ts | 6 +- .../surface/published-pane-live-titles.ts | 2 +- .../surface/terminal-surface-close.ts | 4 +- .../surface/terminal-surface-index.ts | 4 +- .../workspace-layout-frame-model-check.ts | 4 +- .../workspace-layout-frame-observer.ts | 6 +- .../workspace-layout-operation-frames.test.ts | 44 +- .../surface/workspace-layout-projection.ts | 10 +- .../surface/workspace-layout-stream.test.ts | 127 +----- .../surface/workspace-layout-stream.ts | 8 +- ...act-persisted-terminal-surface-identity.ts | 2 +- ...older-workspace-mixed-host-routing.test.ts | 26 +- .../orcad-migration-bridged-panes.ts | 2 +- src/main/window/runtime-window-lifecycle.ts | 2 +- .../window/terminal-pane-move-commit-fence.ts | 2 +- .../terminal-pane-move-request-relay.test.ts | 33 +- .../terminal-pane-move-request-relay.ts | 2 +- .../window/terminal-pane-reveal-relay.test.ts | 92 ---- src/main/window/terminal-pane-reveal-relay.ts | 2 +- .../terminal-tab-close-request-relay.ts | 2 +- .../worktree-removal-close-records.test.ts | 2 +- src/preload/api/pty-api.ts | 4 +- src/preload/api/pty-bridge-session-control.ts | 4 +- ...ui-bridge-clipboard-and-window-controls.ts | 2 +- .../ui-bridge-terminal-and-session-tabs.ts | 4 +- src/preload/api/ui-command-event-api.ts | 10 +- src/preload/api/workspace-session-api.ts | 2 +- ...-window-parity-differences.test-fixture.ts | 74 --- .../app-shell/startup-window-parity.test.ts | 77 ---- .../startup-window-view.test-fixture.ts | 204 --------- .../native-chat-side-chat-placement.ts | 2 +- .../tab-bar/cli-chat-resume-close-terminal.ts | 2 +- .../tab-bar/tab-context-menu.test.tsx | 429 ------------------ .../tab-bar/use-allow-auto-close.ts | 2 +- .../tab-group-layout-owner-commands.ts | 6 +- .../tab-group/tab-group-panel-split-target.ts | 2 +- .../tab-moves-drawn-from-frames.test.ts | 198 -------- .../codex-detached-pane-restart.test.ts | 328 ------------- .../detached/codex-detached-pane-restart.ts | 2 +- .../controller/use-terminal-pane-tab-state.ts | 4 +- .../detach/terminal-layout-leaf-detach.ts | 2 +- .../terminal-pane-detach-layout-owner.ts | 2 +- .../terminal-pane-move-request-routing.ts | 2 +- .../layout/detach/terminal-pane-tab-detach.ts | 2 +- .../ownership/terminal-layout-active-leaf.ts | 2 +- .../terminal-pane-layout-owner-gestures.ts | 4 +- .../ownership/terminal-pane-placement-row.ts | 2 +- .../persistence/remote-pane-layout-push.ts | 4 +- .../use-terminal-pane-layout-persistence.ts | 2 +- .../terminal-live-layout-reconciliation.ts | 2 +- .../terminal-pane-frame-reconciliation.ts | 2 +- ...use-host-terminal-layout-reconciliation.ts | 2 +- .../split/terminal-pane-spawn-placement.ts | 2 +- .../terminal-layout-dom-serialization.ts | 2 +- .../layout/terminal-layout-leaf-traversal.ts | 2 +- .../layout/terminal-layout-normalization.ts | 4 +- .../terminal-layout-pty-ownership.test.ts | 47 +- .../layout/terminal-layout-replay.ts | 2 +- .../parked-terminal-tab-coverage.ts | 2 +- .../confirmed-shell-agent-title.ts | 2 +- .../agent-identity/shell-command-inference.ts | 2 +- .../pty-connection/pty-connection-deps.ts | 2 +- ...session-reattach-newer-daemon-hold.test.ts | 238 ---------- .../spawn/disposed-spawn-retention.ts | 2 +- .../spawn/main-pane-start-wait.ts | 2 +- .../startup/sleeping-record-access.ts | 2 +- ...onnection-test-main-pane-start-fixtures.ts | 2 +- .../pty-transport/pty-transport-types.ts | 2 +- .../remote-layout-scrollback-retention.ts | 2 +- .../terminal-shutdown-layout-capture.ts | 2 +- .../titles/use-host-pane-title-sync.ts | 2 +- .../use-synthetic-pane-title-pruning.ts | 2 +- .../use-terminal-startup-activation.ts | 2 +- .../wsl-move/wsl-move-scope-rows.ts | 2 +- .../src/hooks/agent-auto-ack-targets.ts | 2 +- .../agent-hook-completion-notifications.ts | 2 +- .../ipc-events-close-routing-test-harness.ts | 2 +- .../agent-status-pane-routing-index.ts | 2 +- .../ipc-events/terminal-command-state.ts | 2 +- .../terminal-pane-reveal-ipc-bridge.ts | 2 +- .../terminal-pane-title-ipc-bridge.ts | 2 +- src/renderer/src/hooks/pane-key-pty-id.ts | 2 +- .../identity/pane-agent-conversations.ts | 2 +- .../agent-launch-pane-verdict-application.ts | 2 +- .../ownership/agent-status-pane-ownership.ts | 2 +- .../src/lib/browser/documents/file-preview.ts | 2 +- .../navigation/visible-browser-link-target.ts | 2 +- .../src/lib/keyboard/tab-number-shortcuts.ts | 2 +- .../lib/links/placement/link-open-intent.ts | 2 +- .../pane-layout/pane-layout-geometry-apply.ts | 2 +- .../pane-layout/pane-split-ratio.ts | 2 +- .../lib/pane-manager/pane-manager-types.ts | 2 +- .../worktree-initial-terminal-seeding.ts | 2 +- .../activation/reveal-terminal-pane.ts | 2 +- .../worktree-activation-store-contract.ts | 2 +- .../worktree-agent-activation-seam.test.ts | 245 ---------- .../worktree-agent-live-surface-adoption.ts | 2 +- .../tabs/terminal-reveal-identity.ts | 2 +- .../tabs/unified-tab-anchor-insertion.ts | 2 +- .../actions/web-runtime-leaf-move.ts | 2 +- ...ession-terminal-retirement-proof-ledger.ts | 2 +- ...-terminal-orphan-recovery-surface-index.ts | 2 +- .../mobile-session-capture.ts | 2 +- .../mobile-session-surfaces.ts | 2 +- .../mobile-session-terminal-tabs.ts | 2 +- .../apply-terminal-records.ts | 2 +- .../paired-terminal-tab-close-intents.ts | 2 +- .../closing/web-session-close-replay.test.ts | 88 ---- .../closing/web-session-close-replay.ts | 2 +- .../web-session-tabs-sync/terminal-build.ts | 2 +- .../terminal-surfaces.ts | 2 +- .../commands/layout-apply-send.ts | 6 +- .../commands/layout-owner-new-group-split.ts | 2 +- .../frames/divider-ratio-refusal.test.ts | 138 ------ ...ayout-changes-during-stream-outage.test.ts | 197 -------- .../frames/layout-frame-stream.ts | 2 +- .../frames/layout-frame-terminal-panes.ts | 2 +- .../frames/layout-frame-workspaces.ts | 2 +- .../pane-titles-drawn-from-frames.test.ts | 97 ---- .../frames/runtime-layout-client.test.ts | 172 ------- .../frames/runtime-layout-client.ts | 2 +- .../workspace-layout-frame-binding.test.ts | 122 ----- .../frames/workspace-layout-frame-bindings.ts | 4 +- .../frames/workspace-layout-frame-builds.ts | 6 +- .../frames/workspace-layout-frame-changes.ts | 2 +- ...workspace-layout-frame-drawn-workspaces.ts | 2 +- .../workspace-layout-frame-held-tabs.test.ts | 189 -------- .../workspace-layout-frame-pane-starts.ts | 2 +- .../workspace-layout-frame-pane-trees.ts | 2 +- .../frames/workspace-layout-frame-pending.ts | 8 +- .../workspace-layout-frame-sync.test.ts | 132 ------ .../frames/workspace-layout-frame-sync.ts | 2 +- .../intents/layout-owner-pending-tab-facts.ts | 4 +- .../server/server-held-panes.test.tsx | 198 -------- .../server/server-held-panes.ts | 2 +- .../server/server-layout-apply.test.ts | 189 -------- .../server/server-layout-editor-handover.ts | 2 +- .../server/server-layout-frame-ids.ts | 6 +- .../server/server-layout-frames.ts | 2 +- ...ver-layout-editor-handover.test-fixture.ts | 4 +- .../server-layout-frames.test-fixture.ts | 6 +- ...ayout-apply-test-transport.test-fixture.ts | 6 +- .../main-loaded-layout-frames.test-fixture.ts | 10 +- ...e-layout-frame-apply-check.test-fixture.ts | 2 +- .../browser-managed-profile-actions.ts | 2 +- .../browser-tab-close-through-layout-owner.ts | 2 +- .../tabs/editor-open-link-placement.test.ts | 20 +- .../editor/tabs/editor-open-target-group.ts | 2 +- .../editor-tab-open-through-layout-owner.ts | 6 +- .../editor/tabs/open-file-layout-view.ts | 4 +- .../tabs/collapse-emptied-kept-groups.ts | 2 +- .../tabs/recently-closed-tab-position.ts | 2 +- .../src/store/slices/tabs/tab-group-state.ts | 2 +- .../slices/tabs/tab-order-layout-owner.ts | 5 +- .../store/slices/tabs/tabs-close-actions.ts | 4 +- .../store/slices/tabs/tabs-create-actions.ts | 2 +- .../store/slices/tabs/tabs-drop-actions.ts | 6 +- .../store/slices/tabs/tabs-group-actions.ts | 6 +- .../src/store/slices/tabs/tabs-hydration.ts | 8 +- .../store/slices/tabs/tabs-label-actions.ts | 2 +- .../store/slices/tabs/tabs-move-actions.ts | 4 +- .../store/slices/tabs/tabs-reconciliation.ts | 2 +- .../slices/tabs/tabs-split-ratio-actions.ts | 2 +- .../attention-removal-follow-up.ts | 2 +- ...aired-terminal-tab-close-intent-actions.ts | 2 +- ...-terminal-tab-close-intent.test-fixture.ts | 11 - .../store/terminals/terminal-layout-state.ts | 4 +- .../terminal-session-row-hydration.ts | 4 +- .../src/store/terminals/terminal-state.ts | 4 +- .../terminal-surface-close-intent.ts | 2 +- ...erminal-tab-create-through-layout-owner.ts | 2 +- .../store/terminals/terminal-tab-creation.ts | 6 +- .../terminals/window-tab-start-in-main.ts | 2 +- .../workspace-terminal-layout-plan.ts | 4 +- .../workspace-layout-frame-held-tab-groups.ts | 2 +- .../frames/workspace-layout-frame-slices.ts | 10 +- .../workspace-layout-frame-store-patch.ts | 4 +- .../workspace-layout-frame-store-view.ts | 4 +- .../workspace-layout-cache.test.ts | 2 +- .../workspace-layout-cache.ts | 8 +- .../agent-session-host-authority.ts | 2 +- .../view/agent-session-surface-tab-id.ts | 2 +- ...nch-params.pre-observation.test-fixture.ts | 2 +- .../rpc-contract/agent-launch-params.ts | 2 +- .../rpc-contract/agent-session-params.ts | 2 +- .../workspace-layout-terminal-tab-params.ts | 2 +- src/shared/session-tab-placement.ts | 5 +- .../workspace-layout-client-command.ts | 12 +- .../workspace-layout-client-visibility.ts | 4 +- .../workspace-layout-view-prune.ts | 10 +- .../workspace-layout-view-tab-closes.ts | 6 +- .../workspace-layout-views.test.ts | 4 +- .../workspace-layout-views.ts | 8 +- ...workspace-layout-agent-session-tab.test.ts | 6 +- .../workspace-layout-browser-commands.test.ts | 30 +- .../workspace-layout-browser-commands.ts | 4 +- .../workspace-layout-command-steps.ts | 17 +- .../workspace-layout-command.test-fixture.ts | 13 +- .../workspace-layout-commands.test.ts | 130 +----- .../workspace-layout-content-commands.ts | 12 +- .../workspace-layout-default-tabs.test.ts | 0 .../workspace-layout-default-tabs.ts | 4 +- .../workspace-layout-dropped-leaves.test.ts | 97 +--- .../workspace-layout-dropped-leaves.ts | 13 +- .../workspace-layout-group-commands.ts | 19 +- .../workspace-layout-initial-surface.test.ts | 6 +- .../workspace-layout-initial-surface.ts | 6 +- .../workspace-layout-move-tab-anchor.test.ts | 24 +- .../workspace-layout-move-tab-pin.test.ts | 2 +- ...workspace-layout-open-in-new-group.test.ts | 106 +---- .../workspace-layout-owner-transitions.ts | 8 +- .../workspace-layout-pane-commands.ts | 10 +- .../workspace-layout-pane-outcomes.ts | 0 .../workspace-layout-pane-status.ts | 0 .../workspace-layout-process-commands.ts | 8 +- .../workspace-layout-process-exit.ts | 8 +- .../workspace-layout-process-rules.test.ts | 66 +-- .../workspace-layout-ratio-commands.test.ts | 6 +- .../workspace-layout-removal.ts | 8 +- .../workspace-layout-resume-record.test.ts | 81 +--- .../workspace-layout-simulator-tab.test.ts | 4 +- .../workspace-layout-simulator-tab.ts | 4 +- .../workspace-layout-tab-commands.ts | 21 +- .../workspace-layout-tab-rename.ts | 4 +- ...workspace-layout-terminal-adoption.test.ts | 6 +- .../workspace-layout-terminal-adoption.ts | 4 +- .../workspace-layout-transitions.test.ts | 10 +- .../workspace-layout-transitions.ts | 8 +- .../{ => editor-tabs}/layout-editor-view.ts | 4 +- .../saved-editor-tab-restore.ts | 2 +- ...workspace-layout-editor-duplicates.test.ts | 18 +- ...workspace-layout-editor-file-owner.test.ts | 107 +---- .../workspace-layout-editor-open-file-id.ts | 4 +- .../workspace-layout-editor-open.test.ts | 53 +-- .../workspace-layout-editor-rekey.test.ts | 34 +- .../workspace-layout-editor-rekey.ts | 8 +- .../workspace-layout-editor-tab-file.ts | 6 +- .../workspace-layout-owned-editor-id.test.ts | 52 +-- .../workspace-layout-tab-host.ts | 6 +- .../workspace-layout-workspace-move.test.ts | 10 +- .../workspace-layout-workspace-move.ts | 12 +- .../headless-phone-layout.test-fixture.ts | 278 ------------ .../workspace-layout-id-stability.ts | 7 +- .../workspace-layout-model-rules.test.ts | 12 +- .../workspace-layout-model-rules.ts | 10 +- .../workspace-layout-rule-types.ts | 6 +- .../workspace-layout-rules.ts | 10 +- .../old-client-method-translation.test.ts | 175 +------ .../old-client-method-translation.ts | 9 +- ...old-client-pane-layout-translation.test.ts | 79 +--- .../old-client-pane-layout-translation.ts | 2 +- .../old-client-session-editor-rows.ts | 6 +- .../old-client/old-client-session-tab-rows.ts | 5 +- ...ssion-tabs-projection-owned-editor.test.ts | 4 +- ...old-client-session-tabs-projection.test.ts | 204 +-------- .../old-client-session-tabs-projection.ts | 4 +- .../{ => persistence}/stored-record-fields.ts | 0 .../workspace-layout-beside.ts | 12 +- ...rkspace-layout-command-persistence.test.ts | 64 +++ .../workspace-layout-disk-fields.ts | 14 +- .../workspace-layout-folded-rows.ts | 6 +- ...orkspace-layout-legacy-persistence.test.ts | 6 +- .../workspace-layout-legacy-persistence.ts | 6 +- .../workspace-layout-load-bindings.ts | 14 +- ...rkspace-layout-load-browser-repair.test.ts | 41 +- ...orkspace-layout-load-check-details.test.ts | 10 +- ...orkspace-layout-load-duplicate-pty.test.ts | 18 +- ...orkspace-layout-load-normalization.test.ts | 137 ++++++ .../workspace-layout-load-order.ts | 6 +- .../workspace-layout-load-records.ts | 4 +- .../workspace-layout-load-report.ts | 8 +- .../workspace-layout-load-retirement.ts | 6 +- ...e-layout-load-synthetic-pane-title.test.ts | 34 +- .../workspace-layout-load-tabs.ts | 8 +- .../workspace-layout-load-types.ts | 10 +- ...rkspace-layout-load-view-less-tabs.test.ts | 37 +- .../workspace-layout-load-workspace.ts | 27 +- .../workspace-layout-load.ts | 8 +- .../workspace-layout-minted-ids.ts | 0 .../workspace-layout-profile.test-fixture.ts | 12 +- .../workspace-layout-record-patch.ts | 6 +- .../workspace-layout-recorded-key-order.ts | 2 +- .../workspace-layout-retiring-group-layout.ts | 2 +- .../workspace-layout-round-trip-check.ts | 4 +- .../workspace-layout-save-tabs.ts | 12 +- .../workspace-layout-save.ts | 20 +- .../workspace-layout-session.test-fixture.ts | 12 +- .../layout-intents-capability.ts | 0 .../workspace-layout-from-published.ts | 8 +- .../workspace-layout-published.ts | 20 +- .../workspace-layout-records.test.ts | 77 +--- .../workspace-layout-records.ts | 4 +- .../workspace-layout-stream-frames.ts | 0 .../pane-column-split-drop-no-op.ts | 4 +- .../{ => tab-groups}/split-ratio-intents.ts | 6 +- .../{ => tab-groups}/tab-group-layout-tree.ts | 2 +- .../{ => tab-groups}/tab-group-moves.test.ts | 0 .../{ => tab-groups}/tab-group-moves.ts | 4 +- .../{ => tab-groups}/tab-group-owners.ts | 2 +- .../tab-groups/tab-order-moves.test.ts | 13 + .../{ => tab-groups}/tab-order.ts | 2 +- .../workspace-layout/tab-order-moves.test.ts | 33 -- .../closed-terminal-panes.ts | 0 .../closed-terminal-tab-tombstones.ts | 2 +- .../paired-terminal-tab-close-intent.ts | 0 .../terminal-pane-reveal.ts | 2 +- .../terminal-pane-title-reset.ts | 2 +- .../terminal-retirement-proof-ledger.ts | 2 +- .../terminal-reveal-identity.ts | 0 .../terminal-surface-close-target.ts | 0 .../terminal-surface-tombstone-schema.ts | 0 .../terminal-tab-close.ts | 6 +- .../terminal-tab-id-schema.ts | 0 .../terminal-tab-id.ts | 2 +- .../legacy-terminal-row-claims.ts | 6 +- .../terminal-layout-equality.ts | 2 +- .../terminal-layout-leaf-retirement.ts | 2 +- ...erminal-layout-pty-ownership-depth.test.ts | 2 +- .../terminal-layout-pty-ownership.ts | 2 +- .../terminal-layout-snapshot-schema.ts | 4 +- .../terminal-leaf-move.ts | 2 +- .../terminal-owner-invariants.ts | 10 +- .../terminal-pane-layout-tree.ts | 2 +- .../terminal-pane-placement.ts | 4 +- .../terminal-pane-title-sanitization.ts | 2 +- .../terminal-pane-tree.ts | 4 +- .../terminal-row-order.ts | 2 +- .../terminal-split-outcome.ts | 0 .../terminal-tab-ordinal.ts | 2 +- .../unattached-terminal-row-sweep.ts | 6 +- ...workspace-layout-cold-local-window.test.ts | 16 - ...orkspace-layout-cold-local.test-fixture.ts | 76 ---- ...rkspace-layout-command-persistence.test.ts | 180 -------- .../workspace-layout-command-types.ts | 12 +- .../workspace-layout-commands.ts | 30 +- ...rkspace-layout-editor-open-file-id.test.ts | 37 -- .../workspace-layout-fact-locations.test.ts | 330 -------------- ...pace-layout-legacy-profile.test-fixture.ts | 207 --------- ...orkspace-layout-load-normalization.test.ts | 425 ----------------- .../workspace-layout-minted-ids.test.ts | 13 - .../workspace-layout-model.ts | 6 +- ...space-layout-published-editor-view.test.ts | 110 ----- .../workspace-layout-published.test.ts | 75 --- .../workspace-layout-record-patch.test.ts | 193 -------- .../workspace-layout-round-trip.test.ts | 97 ---- .../workspace-layout-views-owned-file.test.ts | 56 --- .../workspace-session-close-intent-schema.ts | 2 +- .../workspace-session-schema.ts | 10 +- .../workspace-session-sleeping-agents.ts | 2 +- .../workspace-session-state-types.ts | 6 +- ...session-tabs-retirement-proof.unit.test.ts | 203 --------- ...shed-split-orientation-legacy-leaf.spec.ts | 194 -------- tests/e2e/helpers/layout-compat-baseline.ts | 4 +- tests/e2e/helpers/main-terminal-layout.ts | 2 +- .../helpers/paired-desktop-server-layout.ts | 4 +- .../workspace-layout-oracle-compare.ts | 2 +- .../helpers/workspace-layout-oracle-feed.ts | 6 +- .../helpers/workspace-layout-oracle-model.ts | 12 +- tests/e2e/helpers/workspace-layout-oracle.ts | 2 +- ...ed-desktop-server-held-pane-reopen.spec.ts | 203 --------- ...aired-desktop-server-layout-frames.spec.ts | 186 -------- ...ired-desktop-server-layout-intents.spec.ts | 154 ------- ...aired-desktop-server-layout-writes.spec.ts | 282 ------------ ...inal-layout-parity-declared-differences.ts | 16 +- 508 files changed, 1291 insertions(+), 11687 deletions(-) delete mode 100644 src/main/persistence/terminal-topology/editor-tab-layout-apply.test.ts delete mode 100644 src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts delete mode 100644 src/main/persistence/window-save-reconnect-facts.test.ts delete mode 100644 src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-05.spec.ts delete mode 100644 src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts delete mode 100644 src/main/runtime/session-tabs/headless-mobile-session-absorbed-rows.test.ts delete mode 100644 src/main/runtime/session-tabs/headless-move-leaf-to-new-tab.test.ts delete mode 100644 src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts delete mode 100644 src/main/runtime/terminal/launch/terminal-split-retry.test.ts delete mode 100644 src/main/runtime/terminal/surface/orca-runtime-failed-start-pane-layout-owner.test.ts delete mode 100644 src/main/window/terminal-pane-reveal-relay.test.ts delete mode 100644 src/renderer/src/app-shell/startup-window-parity-differences.test-fixture.ts delete mode 100644 src/renderer/src/app-shell/startup-window-parity.test.ts delete mode 100644 src/renderer/src/app-shell/startup-window-view.test-fixture.ts delete mode 100644 src/renderer/src/components/tab-bar/tab-context-menu.test.tsx delete mode 100644 src/renderer/src/components/tab-group/tab-moves-drawn-from-frames.test.ts delete mode 100644 src/renderer/src/components/terminal-pane/account-restart/detached/codex-detached-pane-restart.test.ts delete mode 100644 src/renderer/src/components/terminal-pane/pty-connection/reattach/deferred-session-reattach-newer-daemon-hold.test.ts delete mode 100644 src/renderer/src/lib/workspace/activation/worktree-agent-activation-seam.test.ts delete mode 100644 src/renderer/src/runtime/web-session-tabs-sync/closing/web-session-close-replay.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/divider-ratio-refusal.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/layout-changes-during-stream-outage.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/pane-titles-drawn-from-frames.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/runtime-layout-client.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/workspace-layout-frame-binding.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/workspace-layout-frame-held-tabs.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/frames/workspace-layout-frame-sync.test.ts delete mode 100644 src/renderer/src/runtime/workspace-layout/server/server-held-panes.test.tsx delete mode 100644 src/renderer/src/runtime/workspace-layout/server/server-layout-apply.test.ts delete mode 100644 src/renderer/src/store/terminals/paired-terminal-tab-close-intent.test-fixture.ts rename src/shared/workspace-layout/{ => client-views}/workspace-layout-client-command.ts (94%) rename src/shared/workspace-layout/{ => client-views}/workspace-layout-client-visibility.ts (94%) rename src/shared/workspace-layout/{ => client-views}/workspace-layout-view-prune.ts (95%) rename src/shared/workspace-layout/{ => client-views}/workspace-layout-view-tab-closes.ts (91%) rename src/shared/workspace-layout/{ => client-views}/workspace-layout-views.test.ts (97%) rename src/shared/workspace-layout/{ => client-views}/workspace-layout-views.ts (93%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-agent-session-tab.test.ts (94%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-browser-commands.test.ts (80%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-browser-commands.ts (94%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-command-steps.ts (91%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-command.test-fixture.ts (82%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-commands.test.ts (74%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-content-commands.ts (95%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-default-tabs.test.ts (100%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-default-tabs.ts (92%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-dropped-leaves.test.ts (54%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-dropped-leaves.ts (91%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-group-commands.ts (93%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-initial-surface.test.ts (90%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-initial-surface.ts (71%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-move-tab-anchor.test.ts (70%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-move-tab-pin.test.ts (97%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-open-in-new-group.test.ts (60%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-owner-transitions.ts (96%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-pane-commands.ts (97%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-pane-outcomes.ts (100%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-pane-status.ts (100%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-process-commands.ts (94%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-process-exit.ts (90%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-process-rules.test.ts (70%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-ratio-commands.test.ts (97%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-removal.ts (93%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-resume-record.test.ts (65%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-simulator-tab.test.ts (94%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-simulator-tab.ts (89%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-tab-commands.ts (94%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-tab-rename.ts (89%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-terminal-adoption.test.ts (94%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-terminal-adoption.ts (95%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-transitions.test.ts (96%) rename src/shared/workspace-layout/{ => commands}/workspace-layout-transitions.ts (92%) rename src/shared/workspace-layout/{ => editor-tabs}/layout-editor-view.ts (74%) rename src/shared/workspace-layout/{ => editor-tabs}/saved-editor-tab-restore.ts (94%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-duplicates.test.ts (86%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-file-owner.test.ts (56%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-open-file-id.ts (81%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-open.test.ts (62%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-rekey.test.ts (83%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-rekey.ts (93%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-editor-tab-file.ts (94%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-owned-editor-id.test.ts (57%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-tab-host.ts (83%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-workspace-move.test.ts (95%) rename src/shared/workspace-layout/{ => editor-tabs}/workspace-layout-workspace-move.ts (93%) delete mode 100644 src/shared/workspace-layout/headless-phone-layout.test-fixture.ts rename src/shared/workspace-layout/{ => invariants}/workspace-layout-id-stability.ts (95%) rename src/shared/workspace-layout/{ => invariants}/workspace-layout-model-rules.test.ts (75%) rename src/shared/workspace-layout/{ => invariants}/workspace-layout-model-rules.ts (93%) rename src/shared/workspace-layout/{ => invariants}/workspace-layout-rule-types.ts (78%) rename src/shared/workspace-layout/{ => invariants}/workspace-layout-rules.ts (97%) rename src/shared/workspace-layout/{ => persistence}/stored-record-fields.ts (100%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-beside.ts (92%) create mode 100644 src/shared/workspace-layout/persistence/workspace-layout-command-persistence.test.ts rename src/shared/workspace-layout/{ => persistence}/workspace-layout-disk-fields.ts (96%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-folded-rows.ts (93%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-legacy-persistence.test.ts (91%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-legacy-persistence.ts (92%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-bindings.ts (93%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-browser-repair.test.ts (67%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-check-details.test.ts (86%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-duplicate-pty.test.ts (82%) create mode 100644 src/shared/workspace-layout/persistence/workspace-layout-load-normalization.test.ts rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-order.ts (91%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-records.ts (95%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-report.ts (94%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-retirement.ts (97%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-synthetic-pane-title.test.ts (72%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-tabs.ts (92%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-types.ts (82%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-view-less-tabs.test.ts (64%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load-workspace.ts (90%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-load.ts (96%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-minted-ids.ts (100%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-profile.test-fixture.ts (95%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-record-patch.ts (96%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-recorded-key-order.ts (95%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-retiring-group-layout.ts (98%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-round-trip-check.ts (96%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-save-tabs.ts (93%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-save.ts (89%) rename src/shared/workspace-layout/{ => persistence}/workspace-layout-session.test-fixture.ts (93%) rename src/shared/workspace-layout/{ => published-layout}/layout-intents-capability.ts (100%) rename src/shared/workspace-layout/{ => published-layout}/workspace-layout-from-published.ts (92%) rename src/shared/workspace-layout/{ => published-layout}/workspace-layout-published.ts (89%) rename src/shared/workspace-layout/{ => published-layout}/workspace-layout-records.test.ts (55%) rename src/shared/workspace-layout/{ => published-layout}/workspace-layout-records.ts (98%) rename src/shared/workspace-layout/{ => published-layout}/workspace-layout-stream-frames.ts (100%) rename src/shared/workspace-layout/{ => tab-groups}/pane-column-split-drop-no-op.ts (94%) rename src/shared/workspace-layout/{ => tab-groups}/split-ratio-intents.ts (96%) rename src/shared/workspace-layout/{ => tab-groups}/tab-group-layout-tree.ts (99%) rename src/shared/workspace-layout/{ => tab-groups}/tab-group-moves.test.ts (100%) rename src/shared/workspace-layout/{ => tab-groups}/tab-group-moves.ts (97%) rename src/shared/workspace-layout/{ => tab-groups}/tab-group-owners.ts (95%) create mode 100644 src/shared/workspace-layout/tab-groups/tab-order-moves.test.ts rename src/shared/workspace-layout/{ => tab-groups}/tab-order.ts (99%) delete mode 100644 src/shared/workspace-layout/tab-order-moves.test.ts rename src/shared/workspace-layout/{ => terminal-surfaces}/closed-terminal-panes.ts (100%) rename src/shared/workspace-layout/{ => terminal-surfaces}/closed-terminal-tab-tombstones.ts (97%) rename src/shared/workspace-layout/{ => terminal-surfaces}/paired-terminal-tab-close-intent.ts (100%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-pane-reveal.ts (91%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-pane-title-reset.ts (89%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-retirement-proof-ledger.ts (98%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-reveal-identity.ts (100%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-surface-close-target.ts (100%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-surface-tombstone-schema.ts (100%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-tab-close.ts (94%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-tab-id-schema.ts (100%) rename src/shared/workspace-layout/{ => terminal-surfaces}/terminal-tab-id.ts (95%) rename src/shared/workspace-layout/{ => terminal-topology}/legacy-terminal-row-claims.ts (95%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-layout-equality.ts (98%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-layout-leaf-retirement.ts (98%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-layout-pty-ownership-depth.test.ts (93%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-layout-pty-ownership.ts (99%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-layout-snapshot-schema.ts (91%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-leaf-move.ts (97%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-owner-invariants.ts (91%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-pane-layout-tree.ts (96%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-pane-placement.ts (93%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-pane-title-sanitization.ts (98%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-pane-tree.ts (98%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-row-order.ts (94%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-split-outcome.ts (100%) rename src/shared/workspace-layout/{ => terminal-topology}/terminal-tab-ordinal.ts (90%) rename src/shared/workspace-layout/{ => terminal-topology}/unattached-terminal-row-sweep.ts (93%) delete mode 100644 src/shared/workspace-layout/workspace-layout-cold-local-window.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-cold-local.test-fixture.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-command-persistence.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-editor-open-file-id.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-fact-locations.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-legacy-profile.test-fixture.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-load-normalization.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-minted-ids.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-published-editor-view.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-published.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-record-patch.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-round-trip.test.ts delete mode 100644 src/shared/workspace-layout/workspace-layout-views-owned-file.test.ts delete mode 100644 tests/e2e/cross-version-wire/cross-version-session-tabs-retirement-proof.unit.test.ts delete mode 100644 tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts delete mode 100644 tests/e2e/paired-desktop-server-held-pane-reopen.spec.ts delete mode 100644 tests/e2e/paired-desktop-server-layout-frames.spec.ts delete mode 100644 tests/e2e/paired-desktop-server-layout-intents.spec.ts delete mode 100644 tests/e2e/paired-desktop-server-layout-writes.spec.ts diff --git a/.github/workflows/layout-owner-e2e.yml b/.github/workflows/layout-owner-e2e.yml index 7899e74285f..97655dae6fc 100644 --- a/.github/workflows/layout-owner-e2e.yml +++ b/.github/workflows/layout-owner-e2e.yml @@ -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 diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index bdafee73c71..150195e1643 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -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 diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index b6aab0dd399..42ad4f6d659 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -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", diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index 9c64faa83c3..b1d9bf1fd21 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -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, diff --git a/config/scripts/performance/terminal-stream/terminal-shutdown-byte-limit-benchmark.mjs b/config/scripts/performance/terminal-stream/terminal-shutdown-byte-limit-benchmark.mjs index 874600c3049..7eed0f984b9 100644 --- a/config/scripts/performance/terminal-stream/terminal-shutdown-byte-limit-benchmark.mjs +++ b/config/scripts/performance/terminal-stream/terminal-shutdown-byte-limit-benchmark.mjs @@ -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` } diff --git a/config/scripts/static-analysis/repository-layout/check-workspace-layout-rules.mjs b/config/scripts/static-analysis/repository-layout/check-workspace-layout-rules.mjs index 402dc443fd3..5a9952afa0a 100644 --- a/config/scripts/static-analysis/repository-layout/check-workspace-layout-rules.mjs +++ b/config/scripts/static-analysis/repository-layout/check-workspace-layout-rules.mjs @@ -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' diff --git a/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs b/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs index ff23fadcffa..6a1be20214d 100644 --- a/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs +++ b/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs @@ -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'], diff --git a/config/scripts/workflow/source-routing/pr-layout-oracle-route.mjs b/config/scripts/workflow/source-routing/pr-layout-oracle-route.mjs index e81577e5f4c..18929a56156 100644 --- a/config/scripts/workflow/source-routing/pr-layout-oracle-route.mjs +++ b/config/scripts/workflow/source-routing/pr-layout-oracle-route.mjs @@ -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-', diff --git a/src/cli/paired-terminal-desktop-reveal.ts b/src/cli/paired-terminal-desktop-reveal.ts index 7b05da725a1..15ac101497d 100644 --- a/src/cli/paired-terminal-desktop-reveal.ts +++ b/src/cli/paired-terminal-desktop-reveal.ts @@ -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' diff --git a/src/main/agent-launch/agent-launch-pane-attachment.ts b/src/main/agent-launch/agent-launch-pane-attachment.ts index 8fc74afe186..2b3eec5daec 100644 --- a/src/main/agent-launch/agent-launch-pane-attachment.ts +++ b/src/main/agent-launch/agent-launch-pane-attachment.ts @@ -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' } diff --git a/src/main/ipc/agent-status-ipc-boundary.ts b/src/main/ipc/agent-status-ipc-boundary.ts index 6c314baf534..437f8ce9f46 100644 --- a/src/main/ipc/agent-status-ipc-boundary.ts +++ b/src/main/ipc/agent-status-ipc-boundary.ts @@ -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< diff --git a/src/main/ipc/pty/ipc/closed-pane-spawn-refusal.ts b/src/main/ipc/pty/ipc/closed-pane-spawn-refusal.ts index 4e783168de2..8b97f170546 100644 --- a/src/main/ipc/pty/ipc/closed-pane-spawn-refusal.ts +++ b/src/main/ipc/pty/ipc/closed-pane-spawn-refusal.ts @@ -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( diff --git a/src/main/ipc/pty/ipc/leaf-move.ts b/src/main/ipc/pty/ipc/leaf-move.ts index cac6f04230f..978b50f64d7 100644 --- a/src/main/ipc/pty/ipc/leaf-move.ts +++ b/src/main/ipc/pty/ipc/leaf-move.ts @@ -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( diff --git a/src/main/ipc/pty/ipc/spawn-agent-launch.ts b/src/main/ipc/pty/ipc/spawn-agent-launch.ts index fa5b6e395ef..26e1064ddf9 100644 --- a/src/main/ipc/pty/ipc/spawn-agent-launch.ts +++ b/src/main/ipc/pty/ipc/spawn-agent-launch.ts @@ -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' diff --git a/src/main/ipc/pty/ipc/spawn-commit.ts b/src/main/ipc/pty/ipc/spawn-commit.ts index 2225817b67a..7735ea7334c 100644 --- a/src/main/ipc/pty/ipc/spawn-commit.ts +++ b/src/main/ipc/pty/ipc/spawn-commit.ts @@ -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' diff --git a/src/main/ipc/pty/ipc/spawn-env.ts b/src/main/ipc/pty/ipc/spawn-env.ts index 034bc8359e6..38e22f94b77 100644 --- a/src/main/ipc/pty/ipc/spawn-env.ts +++ b/src/main/ipc/pty/ipc/spawn-env.ts @@ -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' diff --git a/src/main/ipc/pty/pane/adopt-stable.ts b/src/main/ipc/pty/pane/adopt-stable.ts index cc4a57b15a2..2e330c375ee 100644 --- a/src/main/ipc/pty/pane/adopt-stable.ts +++ b/src/main/ipc/pty/pane/adopt-stable.ts @@ -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 diff --git a/src/main/ipc/pty/runtime/spawn-commit-response.ts b/src/main/ipc/pty/runtime/spawn-commit-response.ts index 2ea9dcfc435..6cb308a5133 100644 --- a/src/main/ipc/pty/runtime/spawn-commit-response.ts +++ b/src/main/ipc/pty/runtime/spawn-commit-response.ts @@ -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, diff --git a/src/main/ipc/pty/runtime/spawn-options.ts b/src/main/ipc/pty/runtime/spawn-options.ts index 8deecb90019..5bfaadf6d60 100644 --- a/src/main/ipc/pty/runtime/spawn-options.ts +++ b/src/main/ipc/pty/runtime/spawn-options.ts @@ -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 diff --git a/src/main/ipc/pty/runtime/spawn-preflight.ts b/src/main/ipc/pty/runtime/spawn-preflight.ts index 750ceb88d58..1c5f61d1138 100644 --- a/src/main/ipc/pty/runtime/spawn-preflight.ts +++ b/src/main/ipc/pty/runtime/spawn-preflight.ts @@ -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 { diff --git a/src/main/ipc/pty/runtime/spawn-state.ts b/src/main/ipc/pty/runtime/spawn-state.ts index 6ba02a090ec..b98f75fad28 100644 --- a/src/main/ipc/pty/runtime/spawn-state.ts +++ b/src/main/ipc/pty/runtime/spawn-state.ts @@ -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' diff --git a/src/main/ipc/pty/spawn-commit-assembly.ts b/src/main/ipc/pty/spawn-commit-assembly.ts index 09df9060ebe..f56f5702f3f 100644 --- a/src/main/ipc/pty/spawn-commit-assembly.ts +++ b/src/main/ipc/pty/spawn-commit-assembly.ts @@ -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' diff --git a/src/main/ipc/pty/spawn-pane-coordinates.ts b/src/main/ipc/pty/spawn-pane-coordinates.ts index 8c7fb062bc1..0fe4de6d343 100644 --- a/src/main/ipc/pty/spawn-pane-coordinates.ts +++ b/src/main/ipc/pty/spawn-pane-coordinates.ts @@ -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 = { diff --git a/src/main/ipc/pty/spawn-pane-env.ts b/src/main/ipc/pty/spawn-pane-env.ts index 60fb748d041..6ab715e0f8e 100644 --- a/src/main/ipc/pty/spawn-pane-env.ts +++ b/src/main/ipc/pty/spawn-pane-env.ts @@ -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 diff --git a/src/main/ipc/renderer-shutdown-checkpoint-sleep-records.test.ts b/src/main/ipc/renderer-shutdown-checkpoint-sleep-records.test.ts index 86d22fac5dd..4605065bb42 100644 --- a/src/main/ipc/renderer-shutdown-checkpoint-sleep-records.test.ts +++ b/src/main/ipc/renderer-shutdown-checkpoint-sleep-records.test.ts @@ -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' diff --git a/src/main/ipc/session-window-view-save.test.ts b/src/main/ipc/session-window-view-save.test.ts index 8917d81a623..ac286503b9f 100644 --- a/src/main/ipc/session-window-view-save.test.ts +++ b/src/main/ipc/session-window-view-save.test.ts @@ -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(() => ({ diff --git a/src/main/ipc/session.ts b/src/main/ipc/session.ts index ea719873f26..f4ad3b5d57c 100644 --- a/src/main/ipc/session.ts +++ b/src/main/ipc/session.ts @@ -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' diff --git a/src/main/persistence/closed-terminal-tab-write-fence.ts b/src/main/persistence/closed-terminal-tab-write-fence.ts index 71e129ded6c..d085f419d84 100644 --- a/src/main/persistence/closed-terminal-tab-write-fence.ts +++ b/src/main/persistence/closed-terminal-tab-write-fence.ts @@ -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 diff --git a/src/main/persistence/loading-store/terminal-bindings/pty-binding-fast-lane.ts b/src/main/persistence/loading-store/terminal-bindings/pty-binding-fast-lane.ts index d12bab2b05d..68e2df2529e 100644 --- a/src/main/persistence/loading-store/terminal-bindings/pty-binding-fast-lane.ts +++ b/src/main/persistence/loading-store/terminal-bindings/pty-binding-fast-lane.ts @@ -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. */ diff --git a/src/main/persistence/loading-store/terminal-bindings/pty-binding-persistence.ts b/src/main/persistence/loading-store/terminal-bindings/pty-binding-persistence.ts index 24c6ce90af0..74e6a58b209 100644 --- a/src/main/persistence/loading-store/terminal-bindings/pty-binding-persistence.ts +++ b/src/main/persistence/loading-store/terminal-bindings/pty-binding-persistence.ts @@ -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, diff --git a/src/main/persistence/loading-store/terminal-bindings/pty-binding-refusals.ts b/src/main/persistence/loading-store/terminal-bindings/pty-binding-refusals.ts index 483bbc42954..eafe3bfb92d 100644 --- a/src/main/persistence/loading-store/terminal-bindings/pty-binding-refusals.ts +++ b/src/main/persistence/loading-store/terminal-bindings/pty-binding-refusals.ts @@ -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' diff --git a/src/main/persistence/loading-store/terminal-bindings/pty-binding-session-update.ts b/src/main/persistence/loading-store/terminal-bindings/pty-binding-session-update.ts index a738f44bd5c..11bdf1186d2 100644 --- a/src/main/persistence/loading-store/terminal-bindings/pty-binding-session-update.ts +++ b/src/main/persistence/loading-store/terminal-bindings/pty-binding-session-update.ts @@ -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' diff --git a/src/main/persistence/loading-store/terminal-bindings/pty-binding-span.ts b/src/main/persistence/loading-store/terminal-bindings/pty-binding-span.ts index ae7d27904cf..6e572eaa4c6 100644 --- a/src/main/persistence/loading-store/terminal-bindings/pty-binding-span.ts +++ b/src/main/persistence/loading-store/terminal-bindings/pty-binding-span.ts @@ -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' diff --git a/src/main/persistence/loading-store/workspace-sessions/layout-editor-views.ts b/src/main/persistence/loading-store/workspace-sessions/layout-editor-views.ts index 5c4e0a6ed31..48d27a780d9 100644 --- a/src/main/persistence/loading-store/workspace-sessions/layout-editor-views.ts +++ b/src/main/persistence/loading-store/workspace-sessions/layout-editor-views.ts @@ -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' diff --git a/src/main/persistence/loading-store/workspace-sessions/session-host-partitions.ts b/src/main/persistence/loading-store/workspace-sessions/session-host-partitions.ts index 88c3453f5f7..7548ed51a15 100644 --- a/src/main/persistence/loading-store/workspace-sessions/session-host-partitions.ts +++ b/src/main/persistence/loading-store/workspace-sessions/session-host-partitions.ts @@ -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' diff --git a/src/main/persistence/loading-store/workspace-sessions/session-snapshot-operations.ts b/src/main/persistence/loading-store/workspace-sessions/session-snapshot-operations.ts index f3ddd05bd70..4b9be5cba5a 100644 --- a/src/main/persistence/loading-store/workspace-sessions/session-snapshot-operations.ts +++ b/src/main/persistence/loading-store/workspace-sessions/session-snapshot-operations.ts @@ -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 diff --git a/src/main/persistence/loading-store/workspace-sessions/workspace-session-snapshot-publication.ts b/src/main/persistence/loading-store/workspace-sessions/workspace-session-snapshot-publication.ts index 23e801471b1..e14007f1878 100644 --- a/src/main/persistence/loading-store/workspace-sessions/workspace-session-snapshot-publication.ts +++ b/src/main/persistence/loading-store/workspace-sessions/workspace-session-snapshot-publication.ts @@ -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, diff --git a/src/main/persistence/migrating-orcad-catalog/destination/orcad-destination-workspace-session.ts b/src/main/persistence/migrating-orcad-catalog/destination/orcad-destination-workspace-session.ts index 655f06c7338..68ed59efe77 100644 --- a/src/main/persistence/migrating-orcad-catalog/destination/orcad-destination-workspace-session.ts +++ b/src/main/persistence/migrating-orcad-catalog/destination/orcad-destination-workspace-session.ts @@ -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', diff --git a/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-session-partitions.ts b/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-session-partitions.ts index de85886068d..adfa007a0d9 100644 --- a/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-session-partitions.ts +++ b/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-session-partitions.ts @@ -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' diff --git a/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-workspace-session.ts b/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-workspace-session.ts index 1341973eae4..7700280c0e3 100644 --- a/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-workspace-session.ts +++ b/src/main/persistence/migrating-orcad-catalog/source/sessions/orcad-source-workspace-session.ts @@ -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' diff --git a/src/main/persistence/moving-wsl-projects/desktop-state/wsl-move-seeded-view-state.ts b/src/main/persistence/moving-wsl-projects/desktop-state/wsl-move-seeded-view-state.ts index ddd44442fb8..0296f166402 100644 --- a/src/main/persistence/moving-wsl-projects/desktop-state/wsl-move-seeded-view-state.ts +++ b/src/main/persistence/moving-wsl-projects/desktop-state/wsl-move-seeded-view-state.ts @@ -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' diff --git a/src/main/persistence/moving-wsl-projects/terminal-state/wsl-move-slept-scrollback.ts b/src/main/persistence/moving-wsl-projects/terminal-state/wsl-move-slept-scrollback.ts index 55cbd4e35a5..f658fba9012 100644 --- a/src/main/persistence/moving-wsl-projects/terminal-state/wsl-move-slept-scrollback.ts +++ b/src/main/persistence/moving-wsl-projects/terminal-state/wsl-move-slept-scrollback.ts @@ -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' diff --git a/src/main/persistence/restoring-sessions/empty-terminal-layout-root-repair.ts b/src/main/persistence/restoring-sessions/empty-terminal-layout-root-repair.ts index 41aa7261d59..081ef6165fc 100644 --- a/src/main/persistence/restoring-sessions/empty-terminal-layout-root-repair.ts +++ b/src/main/persistence/restoring-sessions/empty-terminal-layout-root-repair.ts @@ -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( diff --git a/src/main/persistence/restoring-sessions/pane-identity-migration.ts b/src/main/persistence/restoring-sessions/pane-identity-migration.ts index 906f193b08e..ec8fa7796a7 100644 --- a/src/main/persistence/restoring-sessions/pane-identity-migration.ts +++ b/src/main/persistence/restoring-sessions/pane-identity-migration.ts @@ -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, diff --git a/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.test.ts b/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.test.ts index 1fccc5abfa6..cd8c0080ec2 100644 --- a/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.test.ts +++ b/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.test.ts @@ -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, [ { diff --git a/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.ts b/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.ts index e5b117c12cd..fb92f1e67b0 100644 --- a/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.ts +++ b/src/main/persistence/restoring-sessions/tab-terminal-pane-binding-repair.ts @@ -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[] { diff --git a/src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts b/src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts index c4bbfac5bcc..76405adc0e5 100644 --- a/src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts +++ b/src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts @@ -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' diff --git a/src/main/persistence/restoring-sessions/terminal-layout-normalization.ts b/src/main/persistence/restoring-sessions/terminal-layout-normalization.ts index 971d09450ec..9ccc592d03a 100644 --- a/src/main/persistence/restoring-sessions/terminal-layout-normalization.ts +++ b/src/main/persistence/restoring-sessions/terminal-layout-normalization.ts @@ -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 diff --git a/src/main/persistence/restoring-sessions/unattached-terminal-row-sweep.test.ts b/src/main/persistence/restoring-sessions/unattached-terminal-row-sweep.test.ts index fe15342cc41..3989db60a86 100644 --- a/src/main/persistence/restoring-sessions/unattached-terminal-row-sweep.test.ts +++ b/src/main/persistence/restoring-sessions/unattached-terminal-row-sweep.test.ts @@ -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() - }) }) diff --git a/src/main/persistence/terminal-topology/closed-pane-binding-fence.ts b/src/main/persistence/terminal-topology/closed-pane-binding-fence.ts index 9134c3c0681..77cb27d182a 100644 --- a/src/main/persistence/terminal-topology/closed-pane-binding-fence.ts +++ b/src/main/persistence/terminal-topology/closed-pane-binding-fence.ts @@ -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' diff --git a/src/main/persistence/terminal-topology/cross-partition-tab-move.ts b/src/main/persistence/terminal-topology/cross-partition-tab-move.ts index c2eaf41cefe..538b38da223 100644 --- a/src/main/persistence/terminal-topology/cross-partition-tab-move.ts +++ b/src/main/persistence/terminal-topology/cross-partition-tab-move.ts @@ -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, diff --git a/src/main/persistence/terminal-topology/duplicate-pty-pane-repair.ts b/src/main/persistence/terminal-topology/duplicate-pty-pane-repair.ts index 005b55b574a..fcff563776f 100644 --- a/src/main/persistence/terminal-topology/duplicate-pty-pane-repair.ts +++ b/src/main/persistence/terminal-topology/duplicate-pty-pane-repair.ts @@ -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' diff --git a/src/main/persistence/terminal-topology/editor-renames-across-partitions.test.ts b/src/main/persistence/terminal-topology/editor-renames-across-partitions.test.ts index c96e4f498ea..a4db926d57e 100644 --- a/src/main/persistence/terminal-topology/editor-renames-across-partitions.test.ts +++ b/src/main/persistence/terminal-topology/editor-renames-across-partitions.test.ts @@ -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' diff --git a/src/main/persistence/terminal-topology/editor-tab-layout-apply.test.ts b/src/main/persistence/terminal-topology/editor-tab-layout-apply.test.ts deleted file mode 100644 index 43197bb7b45..00000000000 --- a/src/main/persistence/terminal-topology/editor-tab-layout-apply.test.ts +++ /dev/null @@ -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 = { - [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) - }) -}) diff --git a/src/main/persistence/terminal-topology/layout-command-binding-generations.ts b/src/main/persistence/terminal-topology/layout-command-binding-generations.ts index bad7d4c2539..d51c1bdd839 100644 --- a/src/main/persistence/terminal-topology/layout-command-binding-generations.ts +++ b/src/main/persistence/terminal-topology/layout-command-binding-generations.ts @@ -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 { diff --git a/src/main/persistence/terminal-topology/layout-command-persistence.ts b/src/main/persistence/terminal-topology/layout-command-persistence.ts index 779f03344d4..e9c0589991d 100644 --- a/src/main/persistence/terminal-topology/layout-command-persistence.ts +++ b/src/main/persistence/terminal-topology/layout-command-persistence.ts @@ -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, diff --git a/src/main/persistence/terminal-topology/persistence-closed-pane-binding-fence.test.ts b/src/main/persistence/terminal-topology/persistence-closed-pane-binding-fence.test.ts index fe7e7114726..2423b572a17 100644 --- a/src/main/persistence/terminal-topology/persistence-closed-pane-binding-fence.test.ts +++ b/src/main/persistence/terminal-topology/persistence-closed-pane-binding-fence.test.ts @@ -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 { }) } -async function closeThroughMain( - store: TestStore, - target: TerminalSurfaceCloseTarget -): Promise { - 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) diff --git a/src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts b/src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts deleted file mode 100644 index 133cb702ab2..00000000000 --- a/src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/main/persistence/terminal-topology/terminal-layout-apply.test.ts b/src/main/persistence/terminal-topology/terminal-layout-apply.test.ts index b64d12311b9..799c29d815b 100644 --- a/src/main/persistence/terminal-topology/terminal-layout-apply.test.ts +++ b/src/main/persistence/terminal-topology/terminal-layout-apply.test.ts @@ -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 () => { diff --git a/src/main/persistence/terminal-topology/terminal-layout-create-resend.test.ts b/src/main/persistence/terminal-topology/terminal-layout-create-resend.test.ts index 62fa62d5fde..6280d70c5a1 100644 --- a/src/main/persistence/terminal-topology/terminal-layout-create-resend.test.ts +++ b/src/main/persistence/terminal-topology/terminal-layout-create-resend.test.ts @@ -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' diff --git a/src/main/persistence/terminal-topology/terminal-layout-pane-ops.test.ts b/src/main/persistence/terminal-topology/terminal-layout-pane-ops.test.ts index 522f710b9ac..39a8c1d6dd0 100644 --- a/src/main/persistence/terminal-topology/terminal-layout-pane-ops.test.ts +++ b/src/main/persistence/terminal-topology/terminal-layout-pane-ops.test.ts @@ -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' diff --git a/src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts b/src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts index ef58c777496..cf64e068de4 100644 --- a/src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts +++ b/src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts @@ -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, - target: TerminalSurfaceCloseTarget, - reason: 'user' | 'cleanup' -): Promise { - 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, 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 () => { diff --git a/src/main/persistence/terminal-topology/terminal-leaf-move-fixture.ts b/src/main/persistence/terminal-topology/terminal-leaf-move-fixture.ts index 26918caf720..53fdc10c030 100644 --- a/src/main/persistence/terminal-topology/terminal-leaf-move-fixture.ts +++ b/src/main/persistence/terminal-topology/terminal-leaf-move-fixture.ts @@ -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. diff --git a/src/main/persistence/terminal-topology/terminal-leaf-move.ts b/src/main/persistence/terminal-topology/terminal-leaf-move.ts index 6907df901bd..9976454221a 100644 --- a/src/main/persistence/terminal-topology/terminal-leaf-move.ts +++ b/src/main/persistence/terminal-topology/terminal-leaf-move.ts @@ -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 { diff --git a/src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts b/src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts index be9d42411d4..666ecd3cc1a 100644 --- a/src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts +++ b/src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts @@ -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' diff --git a/src/main/persistence/terminal-topology/terminal-pane-admission.ts b/src/main/persistence/terminal-topology/terminal-pane-admission.ts index 88822bc30e8..7b2caa3a768 100644 --- a/src/main/persistence/terminal-topology/terminal-pane-admission.ts +++ b/src/main/persistence/terminal-topology/terminal-pane-admission.ts @@ -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, diff --git a/src/main/persistence/terminal-topology/terminal-pane-placement-agreement.ts b/src/main/persistence/terminal-topology/terminal-pane-placement-agreement.ts index 7229bf93e6e..3ac2c67fb72 100644 --- a/src/main/persistence/terminal-topology/terminal-pane-placement-agreement.ts +++ b/src/main/persistence/terminal-topology/terminal-pane-placement-agreement.ts @@ -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 = diff --git a/src/main/persistence/terminal-topology/terminal-pane-placement-graft.ts b/src/main/persistence/terminal-topology/terminal-pane-placement-graft.ts index 063e99f02fa..cf5dbec6c7e 100644 --- a/src/main/persistence/terminal-topology/terminal-pane-placement-graft.ts +++ b/src/main/persistence/terminal-topology/terminal-pane-placement-graft.ts @@ -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. */ diff --git a/src/main/persistence/terminal-topology/terminal-topology-commit.ts b/src/main/persistence/terminal-topology/terminal-topology-commit.ts index d3c8168af1c..73b62df3b57 100644 --- a/src/main/persistence/terminal-topology/terminal-topology-commit.ts +++ b/src/main/persistence/terminal-topology/terminal-topology-commit.ts @@ -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, diff --git a/src/main/persistence/terminal-topology/terminal-topology-membership.ts b/src/main/persistence/terminal-topology/terminal-topology-membership.ts index fcf14a4f777..f71e6e8a0ac 100644 --- a/src/main/persistence/terminal-topology/terminal-topology-membership.ts +++ b/src/main/persistence/terminal-topology/terminal-topology-membership.ts @@ -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, diff --git a/src/main/persistence/terminal-topology/window-pane-detach.ts b/src/main/persistence/terminal-topology/window-pane-detach.ts index d8c21f83ffc..a07b5bd70b9 100644 --- a/src/main/persistence/terminal-topology/window-pane-detach.ts +++ b/src/main/persistence/terminal-topology/window-pane-detach.ts @@ -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, diff --git a/src/main/persistence/window-save-divergence.ts b/src/main/persistence/window-save-divergence.ts index bbdf8d2a566..5f8145e3357 100644 --- a/src/main/persistence/window-save-divergence.ts +++ b/src/main/persistence/window-save-divergence.ts @@ -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 diff --git a/src/main/persistence/window-save-reconnect-facts.test.ts b/src/main/persistence/window-save-reconnect-facts.test.ts deleted file mode 100644 index 7e388dba0b7..00000000000 --- a/src/main/persistence/window-save-reconnect-facts.test.ts +++ /dev/null @@ -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' - }) - }) -}) diff --git a/src/main/persistence/window-save-view.ts b/src/main/persistence/window-save-view.ts index f59d90e3db6..241b8d421f3 100644 --- a/src/main/persistence/window-save-view.ts +++ b/src/main/persistence/window-save-view.ts @@ -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> = SESSION_FIELD_OWNERS diff --git a/src/main/runtime/__fixtures__/main-layout-close-fixture.ts b/src/main/runtime/__fixtures__/main-layout-close-fixture.ts index 724c396f612..51bb4a029d7 100644 --- a/src/main/runtime/__fixtures__/main-layout-close-fixture.ts +++ b/src/main/runtime/__fixtures__/main-layout-close-fixture.ts @@ -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 diff --git a/src/main/runtime/agent-session/agent-session-tab-layout.ts b/src/main/runtime/agent-session/agent-session-tab-layout.ts index 713acd1e9bf..3e69855b1e4 100644 --- a/src/main/runtime/agent-session/agent-session-tab-layout.ts +++ b/src/main/runtime/agent-session/agent-session-tab-layout.ts @@ -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' diff --git a/src/main/runtime/browser/headless/serve-browser-tab-layout.ts b/src/main/runtime/browser/headless/serve-browser-tab-layout.ts index 8bbfabfa69c..18c59b48987 100644 --- a/src/main/runtime/browser/headless/serve-browser-tab-layout.ts +++ b/src/main/runtime/browser/headless/serve-browser-tab-layout.ts @@ -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' diff --git a/src/main/runtime/client/events/runtime-notifier-contract.ts b/src/main/runtime/client/events/runtime-notifier-contract.ts index 0715c88c511..16dd0c46e9f 100644 --- a/src/main/runtime/client/events/runtime-notifier-contract.ts +++ b/src/main/runtime/client/events/runtime-notifier-contract.ts @@ -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' diff --git a/src/main/runtime/client/navigation/client-session-tab-selection-projection.ts b/src/main/runtime/client/navigation/client-session-tab-selection-projection.ts index 60253dcba2f..d6cbe43a93e 100644 --- a/src/main/runtime/client/navigation/client-session-tab-selection-projection.ts +++ b/src/main/runtime/client/navigation/client-session-tab-selection-projection.ts @@ -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 diff --git a/src/main/runtime/construction/dark-switch-runtime-capabilities.ts b/src/main/runtime/construction/dark-switch-runtime-capabilities.ts index 3195f45d33d..f4cf787b867 100644 --- a/src/main/runtime/construction/dark-switch-runtime-capabilities.ts +++ b/src/main/runtime/construction/dark-switch-runtime-capabilities.ts @@ -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[] { diff --git a/src/main/runtime/construction/orca-runtime.test.ts b/src/main/runtime/construction/orca-runtime.test.ts index f0ca09dcef6..4b2d35802c6 100644 --- a/src/main/runtime/construction/orca-runtime.test.ts +++ b/src/main/runtime/construction/orca-runtime.test.ts @@ -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') diff --git a/src/main/runtime/orca-runtime-tests/bind-first-pane.test-fixture.ts b/src/main/runtime/orca-runtime-tests/bind-first-pane.test-fixture.ts index dc7be8afda1..0e4a6baf7d7 100644 --- a/src/main/runtime/orca-runtime-tests/bind-first-pane.test-fixture.ts +++ b/src/main/runtime/orca-runtime-tests/bind-first-pane.test-fixture.ts @@ -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 = { diff --git a/src/main/runtime/orca-runtime-tests/orca-runtime-layout-owner-start-fixture.ts b/src/main/runtime/orca-runtime-tests/orca-runtime-layout-owner-start-fixture.ts index b24d77ec8bb..13ea9a0884b 100644 --- a/src/main/runtime/orca-runtime-tests/orca-runtime-layout-owner-start-fixture.ts +++ b/src/main/runtime/orca-runtime-tests/orca-runtime-layout-owner-start-fixture.ts @@ -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', diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-05.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-05.spec.ts deleted file mode 100644 index 5e6db3e3f8e..00000000000 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-05.spec.ts +++ /dev/null @@ -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 - } - 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) - }) -}) diff --git a/src/main/runtime/orca-runtime-tests/workspace-layout-stream-events.test-fixture.ts b/src/main/runtime/orca-runtime-tests/workspace-layout-stream-events.test-fixture.ts index 7978945e743..7456b90e81f 100644 --- a/src/main/runtime/orca-runtime-tests/workspace-layout-stream-events.test-fixture.ts +++ b/src/main/runtime/orca-runtime-tests/workspace-layout-stream-events.test-fixture.ts @@ -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. */ diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts deleted file mode 100644 index b61723fed29..00000000000 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts +++ /dev/null @@ -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 - 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([]) - }) -}) diff --git a/src/main/runtime/rpc/errors.ts b/src/main/runtime/rpc/errors.ts index 36dc87c63aa..56904b4d452 100644 --- a/src/main/runtime/rpc/errors.ts +++ b/src/main/runtime/rpc/errors.ts @@ -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' diff --git a/src/main/runtime/rpc/git-graph-layout-projection.test.ts b/src/main/runtime/rpc/git-graph-layout-projection.test.ts index 804e5487c50..2d90bba17ab 100644 --- a/src/main/runtime/rpc/git-graph-layout-projection.test.ts +++ b/src/main/runtime/rpc/git-graph-layout-projection.test.ts @@ -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) -}) diff --git a/src/main/runtime/rpc/git-graph-layout-projection.ts b/src/main/runtime/rpc/git-graph-layout-projection.ts index 4d4eaa45273..05d598f372c 100644 --- a/src/main/runtime/rpc/git-graph-layout-projection.ts +++ b/src/main/runtime/rpc/git-graph-layout-projection.ts @@ -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 } diff --git a/src/main/runtime/rpc/methods/session-tabs/session-tabs-retirement-proof-delta.ts b/src/main/runtime/rpc/methods/session-tabs/session-tabs-retirement-proof-delta.ts index 192cbaf5d69..aa42520e124 100644 --- a/src/main/runtime/rpc/methods/session-tabs/session-tabs-retirement-proof-delta.ts +++ b/src/main/runtime/rpc/methods/session-tabs/session-tabs-retirement-proof-delta.ts @@ -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 diff --git a/src/main/runtime/rpc/methods/workspace-layout/workspace-layout-apply.test.ts b/src/main/runtime/rpc/methods/workspace-layout/workspace-layout-apply.test.ts index 09f062a2444..0af8a730d3b 100644 --- a/src/main/runtime/rpc/methods/workspace-layout/workspace-layout-apply.test.ts +++ b/src/main/runtime/rpc/methods/workspace-layout/workspace-layout-apply.test.ts @@ -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>(async (batches) => [ - { rename: batches[0]!.renames[0]!, code: 'tab_not_found' } - ]) - const hosts: Record = { '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>() diff --git a/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.test.ts b/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.test.ts index 7183e0b98cf..f689a89b63e 100644 --- a/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.test.ts +++ b/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.test.ts @@ -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' diff --git a/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.ts b/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.ts index d56c9ef9d8a..8a765ebc5c8 100644 --- a/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.ts +++ b/src/main/runtime/rpc/methods/workspace-layout/workspace-layout.ts @@ -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' diff --git a/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts b/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts index a191155093d..82df9196a57 100644 --- a/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts +++ b/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts @@ -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' diff --git a/src/main/runtime/session-lifecycle/projection/mobile-session-tab-merge.ts b/src/main/runtime/session-lifecycle/projection/mobile-session-tab-merge.ts index 9c24712c556..c6d4a03da47 100644 --- a/src/main/runtime/session-lifecycle/projection/mobile-session-tab-merge.ts +++ b/src/main/runtime/session-lifecycle/projection/mobile-session-tab-merge.ts @@ -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, diff --git a/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-result-finalization.ts b/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-result-finalization.ts index bf9ed6af89e..3d608a5fc89 100644 --- a/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-result-finalization.ts +++ b/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-result-finalization.ts @@ -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, diff --git a/src/main/runtime/session-tabs/desktop-pane-title-forwarding.ts b/src/main/runtime/session-tabs/desktop-pane-title-forwarding.ts index 7ea33b96e62..1cd4e579fbc 100644 --- a/src/main/runtime/session-tabs/desktop-pane-title-forwarding.ts +++ b/src/main/runtime/session-tabs/desktop-pane-title-forwarding.ts @@ -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) { diff --git a/src/main/runtime/session-tabs/headless-editor-tabs.ts b/src/main/runtime/session-tabs/headless-editor-tabs.ts index bac1681d096..4282c14d591 100644 --- a/src/main/runtime/session-tabs/headless-editor-tabs.ts +++ b/src/main/runtime/session-tabs/headless-editor-tabs.ts @@ -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 diff --git a/src/main/runtime/session-tabs/headless-mobile-session-absorbed-rows.test.ts b/src/main/runtime/session-tabs/headless-mobile-session-absorbed-rows.test.ts deleted file mode 100644 index 2e3e1c3639b..00000000000 --- a/src/main/runtime/session-tabs/headless-mobile-session-absorbed-rows.test.ts +++ /dev/null @@ -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')]) - }) -}) diff --git a/src/main/runtime/session-tabs/headless-move-leaf-to-new-tab.test.ts b/src/main/runtime/session-tabs/headless-move-leaf-to-new-tab.test.ts deleted file mode 100644 index c41136d7941..00000000000 --- a/src/main/runtime/session-tabs/headless-move-leaf-to-new-tab.test.ts +++ /dev/null @@ -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[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 = 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' } - } - } - } -} diff --git a/src/main/runtime/session-tabs/headless-pane-move-to-new-tab.ts b/src/main/runtime/session-tabs/headless-pane-move-to-new-tab.ts index f0f46d8cb34..6904468f109 100644 --- a/src/main/runtime/session-tabs/headless-pane-move-to-new-tab.ts +++ b/src/main/runtime/session-tabs/headless-pane-move-to-new-tab.ts @@ -7,13 +7,13 @@ import { import { withSelectedTabs } from './headless-tab-group-selection' import type { RuntimeMobileSessionTabGroup } from '../../../shared/runtime-session-contracts' import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id' -import { isValidHostTerminalTabId } from '../../../shared/workspace-layout/terminal-tab-id' +import { isValidHostTerminalTabId } from '../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch' import { isTerminalLeafMoveRequest, type TerminalLeafMoveRequest, type TerminalLeafMoveResult -} from '../../../shared/workspace-layout/terminal-leaf-move' +} from '../../../shared/workspace-layout/terminal-topology/terminal-leaf-move' /** Where the client dropped the new tab, in the host's group ids. */ export type MovedPanePlacement = { targetGroupId: string; index?: number } diff --git a/src/main/runtime/session-tabs/headless-tab-group-moves.ts b/src/main/runtime/session-tabs/headless-tab-group-moves.ts index a1e988d23c3..0ce77456697 100644 --- a/src/main/runtime/session-tabs/headless-tab-group-moves.ts +++ b/src/main/runtime/session-tabs/headless-tab-group-moves.ts @@ -9,7 +9,7 @@ import type { import { buildHeadlessTabGroupMove, buildHeadlessTabGroupSplit -} from '../../../shared/workspace-layout/tab-group-moves' +} from '../../../shared/workspace-layout/tab-groups/tab-group-moves' import { randomUUID } from 'node:crypto' import { withSelectedTabs } from './headless-tab-group-selection' import type { TabGroupLayoutNode } from '../../../shared/tab-types' diff --git a/src/main/runtime/session-tabs/headless-tab-properties.ts b/src/main/runtime/session-tabs/headless-tab-properties.ts index 3d773e1780b..dfdc79dabda 100644 --- a/src/main/runtime/session-tabs/headless-tab-properties.ts +++ b/src/main/runtime/session-tabs/headless-tab-properties.ts @@ -12,7 +12,7 @@ import { cloneTerminalLayoutSnapshot } from '../terminal/surface/mobile-session- import { sameStringRecord, terminalLayoutNodeEqual -} from '../../../shared/workspace-layout/terminal-layout-equality' +} from '../../../shared/workspace-layout/terminal-topology/terminal-layout-equality' type HeadlessPaneLayoutEdit = { tabId: string diff --git a/src/main/runtime/session-tabs/headless-terminal-tab-close.ts b/src/main/runtime/session-tabs/headless-terminal-tab-close.ts index e3945b38041..eb5bd2da4f6 100644 --- a/src/main/runtime/session-tabs/headless-terminal-tab-close.ts +++ b/src/main/runtime/session-tabs/headless-terminal-tab-close.ts @@ -13,7 +13,7 @@ import type { RuntimeMobileSessionTerminalTab } from '../../../shared/runtime-mo import { parseAppSshPtyId } from '../../../shared/ssh-pty-id' import { retireMobileSessionTabSelection } from './mobile-session-tab-selection' import { paneTitlesForDesktopWindow } from './desktop-pane-title-forwarding' -import { appendRetiredTerminalSurfaceProofs } from '../../../shared/workspace-layout/terminal-retirement-proof-ledger' +import { appendRetiredTerminalSurfaceProofs } from '../../../shared/workspace-layout/terminal-surfaces/terminal-retirement-proof-ledger' import type { RuntimePtyWorktreeRecord } from '../terminal/records/runtime-terminal-state-records' import type { TerminalPaneLayoutNode } from '../../../shared/workspace-layout/terminal-tab-types' import type { RuntimeTerminalRename } from '../../../shared/runtime-terminal-contracts' diff --git a/src/main/runtime/session-tabs/main-layout-session-tab-move.ts b/src/main/runtime/session-tabs/main-layout-session-tab-move.ts index 53486cc54db..469b2612a72 100644 --- a/src/main/runtime/session-tabs/main-layout-session-tab-move.ts +++ b/src/main/runtime/session-tabs/main-layout-session-tab-move.ts @@ -13,7 +13,7 @@ import { OrcaRuntimeWithMainLayoutRenames } from './main-layout-renames' import { liveTerminalsOfTab } from '../session-lifecycle/close/closed-tab-terminal' import type { WorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-model' import type { CommandOf } from '../../../shared/workspace-layout/workspace-layout-command-types' -import { isPaneColumnSplitDropNoOp } from '../../../shared/workspace-layout/pane-column-split-drop-no-op' +import { isPaneColumnSplitDropNoOp } from '../../../shared/workspace-layout/tab-groups/pane-column-split-drop-no-op' import { selectMobileSessionTab } from './mobile-session-tab-selection' export abstract class OrcaRuntimeWithMainLayoutTabMoves extends OrcaRuntimeWithMainLayoutRenames { diff --git a/src/main/runtime/session-tabs/structured-session-tab-publication.ts b/src/main/runtime/session-tabs/structured-session-tab-publication.ts index 27d6b3b0a41..4cf0959db74 100644 --- a/src/main/runtime/session-tabs/structured-session-tab-publication.ts +++ b/src/main/runtime/session-tabs/structured-session-tab-publication.ts @@ -9,7 +9,7 @@ import { defaultAgentChatLabel } from '../../../shared/agent-session/agent-sessi import { getHeadlessMobileSessionGroupId } from '../terminal/surface/mobile-session-layout-projection' import type { StructuredSessionSurface } from '../agent-session/structured-agent-session-close' import type { StructuredAgentId } from '../../../shared/agent-session/lifecycle/agent-session-provider-handle' -import { insertTabIdIntoOrder } from '../../../shared/workspace-layout/tab-order' +import { insertTabIdIntoOrder } from '../../../shared/workspace-layout/tab-groups/tab-order' import { chatTabId, resolveChatTabPlacement, diff --git a/src/main/runtime/session-tabs/terminal-create-closed-tab-durability.test.ts b/src/main/runtime/session-tabs/terminal-create-closed-tab-durability.test.ts index 7ff8238df28..a8fcaa5d071 100644 --- a/src/main/runtime/session-tabs/terminal-create-closed-tab-durability.test.ts +++ b/src/main/runtime/session-tabs/terminal-create-closed-tab-durability.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../shared/constants' -import { hasClosedTerminalTabRecord } from '../../../shared/workspace-layout/closed-terminal-tab-tombstones' +import { hasClosedTerminalTabRecord } from '../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones' import type { ExecutionHostId } from '../../../shared/execution-host' import { Store } from '../../persistence/loading-store/store' import { closeTestStores, createSqliteTestStore } from '../../persistence/persistence-test-harness' diff --git a/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-plan.ts b/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-plan.ts index 7e79b381145..950021307ba 100644 --- a/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-plan.ts +++ b/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-plan.ts @@ -2,7 +2,7 @@ import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtim import type { RuntimeTerminalOrphanAdoptionRequest } from '../../../../shared/runtime-terminal-contracts' import type { ExecutionHostId } from '../../../../shared/execution-host' import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' -import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones' +import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' import { terminalOrphanExecutionOwnersEqual } from './terminal-orphan-owner' import type { TerminalWorkspaceLaunchScope } from '../../orchestration/terminal-authority/structured-sessions/runtime-legacy-worker-terminal-recovery-types' diff --git a/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-rollback.test.ts b/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-rollback.test.ts index 5582f1cda85..c6040e3bb44 100644 --- a/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-rollback.test.ts +++ b/src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-rollback.test.ts @@ -11,7 +11,7 @@ import { addWorkspace, emptySession, leaf -} from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture' +} from '../../../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture' import { closeTestStores, createStore, @@ -36,7 +36,6 @@ const testStateDirRef = vi.hoisted(() => ({ dir: '' })) const REPO_ID = 'repo-1' const ORPHAN_TAB = 'tab-orphan' const ORPHAN_LEAF = leaf(2) -const ORPHAN_PANE = `${ORPHAN_TAB}:${ORPHAN_LEAF}` function seededSession(revision: number | undefined): WorkspaceSessionState { const session = addWorkspace(emptySession(), GIT_KEY, [ @@ -155,23 +154,6 @@ describe('orphan adoption whose flush fails', () => { rmSync(testState.dir, { recursive: true, force: true }) }) - it.each([ - ['a repo with no fence yet', undefined], - ['a repo whose fence is armed', 3] - ])('leaves no adopted tab, layout or incarnation behind in %s', async (_label, revision) => { - const store = createStore() - store.setWorkspaceSession(seededSession(revision)) - const before = store.getWorkspaceSession() - - await expect(adoptWithFailingFlush(store, revision ?? 0)).rejects.toThrow('disk_full') - - const after = store.getWorkspaceSession() - expect(after.tabsByWorktree[GIT_KEY]?.map((tab) => tab.id)).toEqual(['tab-kept']) - expect(after.terminalLayoutsByTabId[ORPHAN_TAB]).toBeUndefined() - expect(after.terminalPtyIncarnationsByPaneKey?.[ORPHAN_PANE]).toBeUndefined() - expect(after.tabGroups?.[GIT_KEY]).toEqual(before.tabGroups?.[GIT_KEY]) - }) - it('keeps an unrelated edit that landed while it waited to write', async () => { const store = createStore() store.setWorkspaceSession(seededSession(3)) diff --git a/src/main/runtime/terminal/adoption/runtime-terminal-orphan-session-adoption.ts b/src/main/runtime/terminal/adoption/runtime-terminal-orphan-session-adoption.ts index 9c604046df8..ddca47df479 100644 --- a/src/main/runtime/terminal/adoption/runtime-terminal-orphan-session-adoption.ts +++ b/src/main/runtime/terminal/adoption/runtime-terminal-orphan-session-adoption.ts @@ -1,15 +1,15 @@ import { randomUUID } from 'node:crypto' import type { ExecutionHostId } from '../../../../shared/execution-host' import type { RuntimeTerminalOrphanAdoptionRequest } from '../../../../shared/runtime-terminal-contracts' -import { loadWorkspaceLayoutWithoutReport } 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 { loadWorkspaceLayoutWithoutReport } 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 { collectPersistedTerminalLeafIds } from '../surface/mobile-session-layout-projection' import { collectLayoutLeafIdsInOrder, removeLayoutLeaf -} from '../../../../shared/workspace-layout/terminal-pane-tree' +} from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree' import type { TerminalPaneLayoutNode } from '../../../../shared/workspace-layout/terminal-tab-types' import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records' import { mergeTerminalOrphanGroupLayout } from './terminal-orphan-topology' diff --git a/src/main/runtime/terminal/focus/focus-terminal.ts b/src/main/runtime/terminal/focus/focus-terminal.ts index fea722f6698..78561a831ab 100644 --- a/src/main/runtime/terminal/focus/focus-terminal.ts +++ b/src/main/runtime/terminal/focus/focus-terminal.ts @@ -3,7 +3,7 @@ import type { RuntimeTerminalFocus } from '../../../../shared/runtime-terminal-c import type { RuntimeTerminalReveal, TerminalPaneRevealTarget -} from '../../../../shared/workspace-layout/terminal-pane-reveal' +} from '../../../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal' import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import { getLatestPtyTitle } from '../../workspaces/process-listing/runtime-worktree-status-projection' import { copySleepingAgentLaunchConfig } from '../launch/runtime-agent-launch-resolution' diff --git a/src/main/runtime/terminal/identity/moved-pane-owner-rekey.ts b/src/main/runtime/terminal/identity/moved-pane-owner-rekey.ts index 63e4edb4d22..8875fd0f0d5 100644 --- a/src/main/runtime/terminal/identity/moved-pane-owner-rekey.ts +++ b/src/main/runtime/terminal/identity/moved-pane-owner-rekey.ts @@ -2,7 +2,7 @@ import { agentHookServer } from '../../../agent-hooks/server/agent-hook-server' import { forgetFailedAgentLaunches } from '../../../agent-launch/agent-launch-pane-attachment' import type { Store } from '../../../persistence/loading-store/store' import { makePaneKey, parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' -import type { TerminalLeafMoveRequest } from '../../../../shared/workspace-layout/terminal-leaf-move' +import type { TerminalLeafMoveRequest } from '../../../../shared/workspace-layout/terminal-topology/terminal-leaf-move' import type { OrcaRuntimeService } from '../../orca-runtime' type PaneOwnerRuntime = Pick | undefined diff --git a/src/main/runtime/terminal/identity/runtime-terminal-pane-identity.ts b/src/main/runtime/terminal/identity/runtime-terminal-pane-identity.ts index 1352a8d0ad2..b41c3c2e3a4 100644 --- a/src/main/runtime/terminal/identity/runtime-terminal-pane-identity.ts +++ b/src/main/runtime/terminal/identity/runtime-terminal-pane-identity.ts @@ -5,7 +5,7 @@ import { makePaneKey, parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' -import { isValidHostTerminalTabId } from '../../../../shared/workspace-layout/terminal-tab-id' +import { isValidHostTerminalTabId } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id' import { randomUUID } from 'node:crypto' export function allocateTerminalPaneIdentity(opts: { tabId?: string; leafId?: string }): { diff --git a/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts b/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts index eba069639dd..7c744b51f4b 100644 --- a/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts @@ -19,8 +19,8 @@ import { PROJECT_RUNTIME_REPAIR_REQUIRED_CODE } from '../../../../shared/project import { OrchestrationError } from '../../orchestration/orchestration-error' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' -import { loadWorkspaceLayoutWithoutReport } from '../../../../shared/workspace-layout/workspace-layout-load' -import { nameBasedLoadContext } from '../../../../shared/workspace-layout/workspace-layout-minted-ids' +import { loadWorkspaceLayoutWithoutReport } from '../../../../shared/workspace-layout/persistence/workspace-layout-load' +import { nameBasedLoadContext } from '../../../../shared/workspace-layout/persistence/workspace-layout-minted-ids' import { resolveSessionTabRow } from '../../../../shared/workspace-layout/old-client/old-client-session-tab-rows' function resolveSavedChatForkAnchor( diff --git a/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts b/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts index 5f2c779aee6..f657a5f947f 100644 --- a/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts @@ -14,7 +14,7 @@ import { deriveRemoteRuntimeTerminalCreateHandle } from '../identity/remote-runt import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import { mintServePtySessionId } from '../../../../shared/terminal/pty/lifecycle/pty-session-id-format' import { placeCreatedSessionTab } from '../../../../shared/session-tab-placement' -import { insertTabIdIntoOrder } from '../../../../shared/workspace-layout/tab-order' +import { insertTabIdIntoOrder } from '../../../../shared/workspace-layout/tab-groups/tab-order' import type { OrcaRuntimeWithBuildPtyTerminalSummary as PtyTerminalSummaryMethods } from '../query/pty-terminal-summary' import { buildHeadlessMobileSessionTabGroups, diff --git a/src/main/runtime/terminal/launch/runtime-pane-admission.ts b/src/main/runtime/terminal/launch/runtime-pane-admission.ts index d6228a06ba2..5dd1934bba2 100644 --- a/src/main/runtime/terminal/launch/runtime-pane-admission.ts +++ b/src/main/runtime/terminal/launch/runtime-pane-admission.ts @@ -5,7 +5,7 @@ import { normalizeExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host' -import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree' +import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree' import type { TerminalPaneAdmission, TerminalPaneAdmissionOutcome @@ -13,7 +13,7 @@ import type { import type { LayoutRefusalCode } from '../../../../shared/workspace-layout/workspace-layout-command-types' import type { RuntimePtyController } from '../lifecycle/runtime-pty-controller-contract' import type { RuntimeStore } from '../../construction/runtime-store-contract' -import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/workspace-layout-published' +import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' import { paneStartFailure, type PaneResumeOutcomes, diff --git a/src/main/runtime/terminal/launch/terminal-split-in-main.ts b/src/main/runtime/terminal/launch/terminal-split-in-main.ts index c4bb3129b04..0b011c412f2 100644 --- a/src/main/runtime/terminal/launch/terminal-split-in-main.ts +++ b/src/main/runtime/terminal/launch/terminal-split-in-main.ts @@ -13,7 +13,7 @@ import { resolvePtyFirstSize, type PtyGridSize } from '../../../ipc/pty/pty-firs import { isTerminalLeafId, makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import { randomUUID } from 'node:crypto' import { paneStartFailure } from '../surface/pane-start-statuses' -import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree' +import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree' import { recordFreshLaunch } from './launch-evidence' export type SplitPaneSource = { worktreeId: string; tabId: string; leafId: string } diff --git a/src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts b/src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts deleted file mode 100644 index 48dbb0fa884..00000000000 --- a/src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import '../../orca-runtime-tests/runtime-test-module-registration.spec' -import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' -import { - HEADLESS_LEAF_ID, - TEST_WORKTREE_ID, - TEST_WORKTREE_PATH, - makeRuntimeStoreWithWorkspaceSession, - makeWorkspaceSessionWithHeadlessTerminal -} from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec' -import { createMobileCreateTestNotifier } from '../../orca-runtime-tests/orca-runtime-test-scenario-builders.spec' -import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree' -import { ProvisioningRuntime } from '../../orca-runtime-tests/orca-runtime-layout-owner-start-fixture' -import { - committingSpawn, - registerBoundPty -} from '../../orca-runtime-tests/bind-first-pane.test-fixture' - -vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) - -const TAB_ID = 'host-tab' -const NEW_LEAF_ID = '22222222-2222-4222-8222-222222222222' - -function graph() { - return { - tabs: [ - { - tabId: TAB_ID, - worktreeId: TEST_WORKTREE_ID, - title: 'shell', - activeLeafId: HEADLESS_LEAF_ID, - layout: null - } - ], - leaves: [ - { - tabId: TAB_ID, - worktreeId: TEST_WORKTREE_ID, - leafId: HEADLESS_LEAF_ID, - paneRuntimeId: 1, - ptyId: 'pty-0', - paneTitle: null - } - ] - } -} - -describe('a split of a pane the window draws, in a layout-owner build', () => { - function setup() { - const store = makeRuntimeStoreWithWorkspaceSession(makeWorkspaceSessionWithHeadlessTerminal()) - const notifier = { ...createMobileCreateTestNotifier(vi.fn()), closeTerminalPane: vi.fn() } - const runtime = new ProvisioningRuntime(store.runtimeStore) - runtime.setNotifier(notifier) - Object.assign(runtime, { - resolveTerminalWorkspaceLaunchScope: vi.fn(async () => ({ - id: TEST_WORKTREE_ID, - path: TEST_WORKTREE_PATH, - connectionId: null, - repo: null, - folderWorkspace: null - })) - }) - const statusesSeen: unknown[] = [] - const spawn = vi.fn(async (args: { worktreeId?: string; tabId?: string; leafId?: string }) => { - statusesSeen.push(runtime.startStatusOf(`${args.tabId}:${args.leafId}`)) - return { id: `pty-split-${spawn.mock.calls.length}` } - }) - runtime.setPtyController({ - spawn: committingSpawn(runtime, spawn), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null, - getCwd: async () => '/repo/src' - }) - // The source pane's terminal runs, bound in main's layout as its spawn commit left it. - registerBoundPty(runtime, 'pty-0', TEST_WORKTREE_ID, null, { - tabId: TAB_ID, - leafId: HEADLESS_LEAF_ID - }) - runtime.syncWindowGraph(1, graph()) - const handle = runtime.getTerminalHandleForPaneKey(makePaneKey(TAB_ID, HEADLESS_LEAF_ID)) - const leafIds = () => - collectLayoutLeafIdsInOrder(store.getSession().terminalLayoutsByTabId[TAB_ID]!.root!) - return { - ...store, - notifier, - runtime, - spawn, - statusesSeen, - handle: handle!, - splitTerminal: vi.mocked(notifier.splitTerminal), - leafIds - } - } - - it('main writes the pane and starts its terminal there, in the source pane’s folder; the window is not asked', async () => { - const { runtime, handle, splitTerminal, leafIds, spawn, statusesSeen } = setup() - - const split = await runtime.splitTerminal(handle, { - direction: 'vertical', - command: 'make test' - }) - - expect(splitTerminal).not.toHaveBeenCalled() - expect(leafIds()).toEqual([HEADLESS_LEAF_ID, split.leafId]) - expect(spawn).toHaveBeenCalledTimes(1) - expect(spawn.mock.calls[0]![0]).toMatchObject({ - tabId: TAB_ID, - leafId: split.leafId, - cwd: '/repo/src', - resolvedWorkspace: { - id: TEST_WORKTREE_ID, - path: TEST_WORKTREE_PATH, - connectionId: null, - repo: null, - folderWorkspace: null - }, - command: 'make test', - cols: 120, - rows: 40 - }) - // Publish starting status to prevent views spawning duplicates, then clear it after binding. - expect(statusesSeen).toEqual([{ status: 'starting' }]) - expect(runtime.startStatusOf(makePaneKey(TAB_ID, split.leafId ?? ''))).toBeNull() - expect(split.tabId).toBe(TAB_ID) - }) - - it('a split no one asked to see leaves the phones’ active tab alone; one asked for takes it', async () => { - const { runtime, handle } = setup() - const listed = () => runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) - const before = (await listed()).activeTabId - - await runtime.splitTerminal(handle, { direction: 'vertical', activate: false }) - expect((await listed()).activeTabId).toBe(before) - - const shown = await runtime.splitTerminal(handle, { direction: 'horizontal' }) - expect((await listed()).activeTabId).toBe(`${TAB_ID}::${shown.leafId}`) - }) - - it('a window split whose terminal fails to start keeps its pane, showing why', async () => { - const { runtime, leafIds, spawn } = setup() - spawn.mockRejectedValueOnce(new Error('shell not found')) - - await expect( - runtime.splitTerminalPane( - { worktreeId: TEST_WORKTREE_ID, tabId: TAB_ID, leafId: HEADLESS_LEAF_ID }, - { direction: 'vertical', newLeafId: NEW_LEAF_ID, keepPaneOnStartFailure: true } - ) - ).rejects.toThrow('shell not found') - - expect(leafIds()).toEqual([HEADLESS_LEAF_ID, NEW_LEAF_ID]) - expect(runtime.startStatusOf(makePaneKey(TAB_ID, NEW_LEAF_ID))).toEqual({ - status: 'failed', - reason: 'shell not found' - }) - }) - - it('a CLI split whose terminal fails to start leaves no pane, and the CLI hears why', async () => { - const { runtime, handle, leafIds, spawn, notifier } = setup() - spawn.mockRejectedValueOnce(new Error('shell not found')) - - await expect(runtime.splitTerminal(handle, { direction: 'vertical' })).rejects.toThrow( - 'shell not found' - ) - - expect(leafIds()).toEqual([HEADLESS_LEAF_ID]) - expect(notifier.closeTerminalPane).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/runtime/terminal/launch/terminal-split-retry.test.ts b/src/main/runtime/terminal/launch/terminal-split-retry.test.ts deleted file mode 100644 index ce6fd7f9e9e..00000000000 --- a/src/main/runtime/terminal/launch/terminal-split-retry.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { OrcaRuntimeService } from '../../orca-runtime' -import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' -import { TerminalSplitOutcomeError } from '../../../../shared/workspace-layout/terminal-split-outcome' - -const WORKTREE_ID = 'repo-1::/workspace' -const TAB_ID = 'tab-1' -const SOURCE_LEAF_ID = '11111111-1111-4111-8111-111111111111' - -type Leaf = { leafId: string; paneRuntimeId: number; ptyId: string | null } - -function createHarness() { - const rendererSplitTerminal = vi.fn() - const runtime = new OrcaRuntimeService() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the split path calls only splitTerminal on the notifier. - runtime.setNotifier({ splitTerminal: rendererSplitTerminal } as never) - const publish = (extra: Leaf[] = []): void => { - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: TAB_ID, - worktreeId: WORKTREE_ID, - title: 'Terminal', - activeLeafId: SOURCE_LEAF_ID, - layout: { type: 'leaf', leafId: SOURCE_LEAF_ID } - } - ], - leaves: [{ leafId: SOURCE_LEAF_ID, paneRuntimeId: 1, ptyId: 'pty-source' }, ...extra].map( - (leaf) => ({ tabId: TAB_ID, worktreeId: WORKTREE_ID, ...leaf }) - ) - }) - } - publish() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: issueHandle and leaves are protected; the test issues the source handle as a graph reader would. - const internals = runtime as unknown as { - issueHandle: (leaf: unknown) => string - leaves: Map - } - const handle = internals.issueHandle([...internals.leaves.values()][0]) - return { runtime, handle, rendererSplitTerminal, publish } -} - -async function timedOutSplit(harness: ReturnType): Promise<{ - error: TerminalSplitOutcomeError - leafId: string -}> { - const split = harness.runtime - .splitTerminal(harness.handle, { command: 'agent --task 1' }) - .catch((error: unknown) => error) - await vi.advanceTimersByTimeAsync(10_000) - const error = await split - if (!(error instanceof TerminalSplitOutcomeError)) { - throw new Error(`expected a pending split, got ${String(error)}`) - } - return { error, leafId: error.data.splitId } -} - -describe.skipIf(ORCA_LAYOUT_OWNER_IS_MAIN)( - 'window-made split that outlives its wait (#32169)', - () => { - beforeEach(() => { - vi.useFakeTimers() - }) - afterEach(() => { - vi.useRealTimers() - }) - - it('keeps waiting for a pane the window already listed while its terminal starts', async () => { - const harness = createHarness() - const split = harness.runtime.splitTerminal(harness.handle, { command: 'agent' }) - const leafId = harness.rendererSplitTerminal.mock.calls[0]?.[2]?.newLeafId - harness.publish([{ leafId, paneRuntimeId: 2, ptyId: null }]) - await vi.advanceTimersByTimeAsync(15_000) - harness.publish([{ leafId, paneRuntimeId: 2, ptyId: 'pty-late' }]) - - await expect(split).resolves.toMatchObject({ - leafId, - handle: expect.stringMatching(/^term_/) - }) - }) - - it('a retry returns the late pane without asking the window to split again', async () => { - const harness = createHarness() - const { leafId } = await timedOutSplit(harness) - harness.publish([{ leafId, paneRuntimeId: 2, ptyId: 'pty-late' }]) - - await expect( - harness.runtime.splitTerminal(harness.handle, { retrySplit: leafId }) - ).resolves.toMatchObject({ leafId, tabId: TAB_ID }) - expect(harness.rendererSplitTerminal).toHaveBeenCalledTimes(1) - }) - - it('a retry before the pane appears waits for it without asking the window again', async () => { - const harness = createHarness() - const { leafId } = await timedOutSplit(harness) - - const retry = harness.runtime.splitTerminal(harness.handle, { retrySplit: leafId }) - harness.publish([{ leafId, paneRuntimeId: 2, ptyId: 'pty-late' }]) - - await expect(retry).resolves.toMatchObject({ leafId }) - expect(harness.rendererSplitTerminal).toHaveBeenCalledTimes(1) - }) - - it('a retry for a pane missing from the graph answers pending, never splitting again', async () => { - const harness = createHarness() - const { leafId } = await timedOutSplit(harness) - harness.publish([{ leafId, paneRuntimeId: 2, ptyId: null }]) - // Absent again: closed or remounting, main cannot tell, so it must not claim either. - harness.publish() - - const retry = harness.runtime - .splitTerminal(harness.handle, { retrySplit: leafId }) - .catch((error: unknown) => error) - await vi.advanceTimersByTimeAsync(10_000) - - await expect(retry).resolves.toMatchObject({ - data: { splitId: leafId, splitMayStillComplete: true } - }) - await expect( - harness.runtime.splitTerminal(harness.handle, { - retrySplit: '22222222-2222-4222-8222-222222222222' - }) - ).rejects.toThrow('no record of split') - expect(harness.rendererSplitTerminal).toHaveBeenCalledTimes(1) - }) - } -) diff --git a/src/main/runtime/terminal/launch/terminal-split-retry.ts b/src/main/runtime/terminal/launch/terminal-split-retry.ts index dd76d6cf6d3..8b07e5d9c1b 100644 --- a/src/main/runtime/terminal/launch/terminal-split-retry.ts +++ b/src/main/runtime/terminal/launch/terminal-split-retry.ts @@ -1,5 +1,5 @@ import { BoundedMap } from '../../../../shared/bounded-map' -import { TerminalSplitOutcomeError } from '../../../../shared/workspace-layout/terminal-split-outcome' +import { TerminalSplitOutcomeError } from '../../../../shared/workspace-layout/terminal-topology/terminal-split-outcome' import { SPLIT_PANE_WAIT_TIMEOUT_MESSAGE } from './terminal-split-wait-timeout' /** Where main asked the window to split; a retry answers from it without asking again. */ diff --git a/src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts b/src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts index 4727d394799..cd44d04166b 100644 --- a/src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts +++ b/src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts @@ -5,14 +5,13 @@ import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../../shared/constants' import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload' -import type { AgentStatusOrchestrationContext } from '../../../../shared/agent-status/contracts/agent-status-types' + import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' -import type { WorkspaceLayoutStreamEvent } from '../../../../shared/workspace-layout/workspace-layout-stream-frames' + import type { Store } from '../../../persistence/loading-store/store' import { WindowSession } from '../../../persistence/terminal-topology/terminal-topology-window-session-fixture' import { emptyTerminalSessionProfile, - FIXTURE_FOLDER_WORKTREE_ID as FOLDER, FIXTURE_GIT_WORKTREE_ID as GIT, openTopologyStore } from '../../../persistence/terminal-topology/terminal-topology-profile-fixture' @@ -71,32 +70,7 @@ function status(workspace: string, overrides: Partial = { } satisfies AgentStatusIpcPayload } -const dispatch = (taskId: string, task: string) => - `You are working inside Orca, a multi-agent IDE. You are a dispatched worker. -Your task ID is: ${taskId} - -=== TASK === -${task}` - describe('main titles agent tabs from its own hook status', () => { - it('gives a batch’s titles of several workspaces in one rename and one frame', async () => { - const { runtime, apply, titleOf } = await hostWith([GIT, FOLDER]) - const frames: WorkspaceLayoutStreamEvent[] = [] - runtime.subscribeWorkspaceLayouts((frame) => frames.push(frame)) - - runtime.recordHookStatusTitle(status(GIT)) - runtime.recordHookStatusTitle(status(FOLDER, { prompt: 'Fix the login page' })) - await runtime.hookStatusTitlesSettled() - - expect(apply.mock.calls.map(([command]) => command.type)).toEqual(['renameTabs']) - expect(frames.filter((frame) => frame.type === 'records')).toHaveLength(1) - expect(frames).toHaveLength(1) - expect([titleOf(GIT), titleOf(FOLDER)]).toEqual([ - 'Refactor the auth middleware to use JWT', - 'Fix the login page' - ]) - }) - it('has the titles in the first snapshot a window that starts after them reads', async () => { const { runtime } = await hostWith([GIT]) @@ -109,68 +83,3 @@ describe('main titles agent tabs from its own hook status', () => { expect(tab?.generatedTitle).toBe('Refactor the auth middleware to use JWT') }) }) - -describe('main keeps the window’s generated-title rules', () => { - async function titled( - statuses: AgentStatusIpcPayload[][], - /** Main's dispatch labels; a list gives each read its own, the last one kept. */ - labels: - | Record[] - | Record = {} - ) { - const host = await hostWith([GIT]) - const reads = [labels].flat() - const read = vi.spyOn(host.runtime['agentOrchestrationProjection'], 'getForPaneKey') - read.mockImplementation((paneKey) => reads.at(-1)?.[paneKey]) - for (const each of reads.slice(0, -1)) { - read.mockImplementationOnce((paneKey) => each[paneKey]) - } - for (const batch of statuses) { - for (const row of batch) { - host.runtime.recordHookStatusTitle(row) - } - await host.runtime.hookStatusTitlesSettled() - } - return host - } - const labelsOf = (taskId: string, displayName: string) => ({ - [paneOf(GIT)]: { - taskId, - dispatchId: `ctx-${taskId}`, - taskTitle: displayName, - displayName - } - }) - - it('replaces a dispatch’s preamble title once main has its labels', async () => { - const prompt = dispatch('task-1', 'Implement the detailed worker instructions') - const host = await titled( - [[status(GIT, { prompt })]], - [{}, labelsOf('task-1', 'Better worker label')] - ) - expect(host.titleOf(GIT)).toBe('Implement the detailed worker') - - host.runtime.recordHookStatusTitle(status(GIT, { prompt, state: 'done' })) - await host.runtime.hookStatusTitlesSettled() - expect(host.titleOf(GIT)).toBe('Better worker label') - }) - - it('does not re-pin task A’s label onto a later dispatch of task B', async () => { - const { titleOf } = await titled( - [ - [ - status(GIT, { - prompt: dispatch('task-a', 'Implement task A worker instructions') - }) - ], - [ - status(GIT, { - prompt: dispatch('task-b', 'Implement task B worker instructions') - }) - ] - ], - labelsOf('task-a', 'Worker A label') - ) - expect(titleOf(GIT)).toBe('Implement task B worker instructions') - }) -}) diff --git a/src/main/runtime/terminal/layout/editor-view-layout-apply.test.ts b/src/main/runtime/terminal/layout/editor-view-layout-apply.test.ts index 0fca7a2941d..6b305f8cc54 100644 --- a/src/main/runtime/terminal/layout/editor-view-layout-apply.test.ts +++ b/src/main/runtime/terminal/layout/editor-view-layout-apply.test.ts @@ -5,7 +5,7 @@ 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 { LayoutEditorView } from '../../../../shared/workspace-layout/layout-editor-view' +import type { LayoutEditorView } from '../../../../shared/workspace-layout/editor-tabs/layout-editor-view' import type { Store } from '../../../persistence/loading-store/store' import { WindowSession } from '../../../persistence/terminal-topology/terminal-topology-window-session-fixture' import { diff --git a/src/main/runtime/terminal/layout/editor-view-partition-changes.test.ts b/src/main/runtime/terminal/layout/editor-view-partition-changes.test.ts index 7f16df156f0..c601b3cab01 100644 --- a/src/main/runtime/terminal/layout/editor-view-partition-changes.test.ts +++ b/src/main/runtime/terminal/layout/editor-view-partition-changes.test.ts @@ -6,7 +6,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../../shared/constants' -import type { LayoutEditorView } from '../../../../shared/workspace-layout/layout-editor-view' +import type { LayoutEditorView } from '../../../../shared/workspace-layout/editor-tabs/layout-editor-view' import type { Store } from '../../../persistence/loading-store/store' import { WindowSession } from '../../../persistence/terminal-topology/terminal-topology-window-session-fixture' import { diff --git a/src/main/runtime/terminal/layout/headless-terminal-split-layout.ts b/src/main/runtime/terminal/layout/headless-terminal-split-layout.ts index 7cd9ccdb767..fa25665aaa1 100644 --- a/src/main/runtime/terminal/layout/headless-terminal-split-layout.ts +++ b/src/main/runtime/terminal/layout/headless-terminal-split-layout.ts @@ -5,7 +5,7 @@ import type { import { insertLeafBeside, removeLayoutLeaf -} from '../../../../shared/workspace-layout/terminal-pane-tree' +} from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree' /** Persist headless splits so snapshot rebuilds cannot collapse them to a stale single pane. */ export function buildHeadlessTerminalSplitLayout( diff --git a/src/main/runtime/terminal/layout/runtime-layout-apply.ts b/src/main/runtime/terminal/layout/runtime-layout-apply.ts index 01777cc7ea9..ee45084e55c 100644 --- a/src/main/runtime/terminal/layout/runtime-layout-apply.ts +++ b/src/main/runtime/terminal/layout/runtime-layout-apply.ts @@ -6,7 +6,7 @@ import type { RenameTabsCommand, UntitledTab } from '../../../../shared/workspace-layout/workspace-layout-command-types' -import type { TerminalLeafMoveRequest } from '../../../../shared/workspace-layout/terminal-leaf-move' +import type { TerminalLeafMoveRequest } from '../../../../shared/workspace-layout/terminal-topology/terminal-leaf-move' import { renameTabCommandsOf } from '../../../../shared/workspace-layout/workspace-layout-commands' import { markAgentLaunchesClosedByUser } from '../../../agent-launch/agent-launch-pane-attachment' import { isWslMoveWorkspaceSessionFrozen } from '../../../persistence/moving-wsl-projects/quiescence/wsl-move-scope-guard' diff --git a/src/main/runtime/terminal/layout/runtime-terminal-spawn-placement.ts b/src/main/runtime/terminal/layout/runtime-terminal-spawn-placement.ts index 34764a1e041..ba68e50dd9c 100644 --- a/src/main/runtime/terminal/layout/runtime-terminal-spawn-placement.ts +++ b/src/main/runtime/terminal/layout/runtime-terminal-spawn-placement.ts @@ -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 { TerminalPaneSplitDirection } from '../../../../shared/workspace-layout/terminal-tab-types' import type { RuntimeTerminalPresentation } from '../../../../shared/runtime-terminal-contracts' diff --git a/src/main/runtime/terminal/lifecycle/closed-pane-of-exited-pty.ts b/src/main/runtime/terminal/lifecycle/closed-pane-of-exited-pty.ts index 17675d31c78..ea58e80e0cb 100644 --- a/src/main/runtime/terminal/lifecycle/closed-pane-of-exited-pty.ts +++ b/src/main/runtime/terminal/lifecycle/closed-pane-of-exited-pty.ts @@ -1,5 +1,5 @@ // Close removes the pane before exit; resolve it by PTY and incarnation in closed-pane records, then retire that entry (L80 Q2). -import { closedTerminalPaneOf } from '../../../../shared/workspace-layout/closed-terminal-panes' +import { closedTerminalPaneOf } from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-panes' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' import type { RuntimeStore } from '../../construction/runtime-store-contract' import type { ExecutionHostId } from '../../../../shared/execution-host' diff --git a/src/main/runtime/terminal/lifecycle/exited-split-pane-layout-close.ts b/src/main/runtime/terminal/lifecycle/exited-split-pane-layout-close.ts index d6e397c7efa..639d5b66b58 100644 --- a/src/main/runtime/terminal/lifecycle/exited-split-pane-layout-close.ts +++ b/src/main/runtime/terminal/lifecycle/exited-split-pane-layout-close.ts @@ -1,4 +1,4 @@ -import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree' +import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' import type { RetiredTerminalSurface } from '../surface/mobile-session-terminal-retirement' import { exitedSplitPaneOutcome, type ExitedSplitPaneFacts } from './exited-split-pane-rule' diff --git a/src/main/runtime/terminal/lifecycle/orca-runtime-exited-split-pane-close.test.ts b/src/main/runtime/terminal/lifecycle/orca-runtime-exited-split-pane-close.test.ts index e6173e952be..fa976a12069 100644 --- a/src/main/runtime/terminal/lifecycle/orca-runtime-exited-split-pane-close.test.ts +++ b/src/main/runtime/terminal/lifecycle/orca-runtime-exited-split-pane-close.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../../shared/constants' -import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree' +import '../../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-session-contracts' import { OrcaRuntimeService } from '../../orca-runtime' @@ -109,37 +109,7 @@ function splitRuntime(): { runtime: OrcaRuntimeService; session: () => Workspace return { runtime, session: () => session } } -const panes = (session: WorkspaceSessionState) => - collectLeafIds(session.terminalLayoutsByTabId.tab?.root) - describe('layout-owner build: main closes an exited split pane itself', () => { - it('keeps a split pane whose process failed, behind its "Terminal exited" card', async () => { - const { runtime, session } = splitRuntime() - await runtime.onPtyExit('pty-right', 1, undefined, { providerExitObserved: true }) - expect(panes(session())).toEqual([LEFT, RIGHT]) - }) - - it('keeps a split pane on a -1 nothing vouches for, as the window does on lost contact', async () => { - const { runtime, session } = splitRuntime() - await runtime.onPtyExit('pty-right', -1) - expect(panes(session())).toEqual([LEFT, RIGHT]) - }) - - it('keeps a split pane on a -1 the host confirmed, so recovery can start a shell in it', async () => { - const { runtime, session } = splitRuntime() - await runtime.onPtyExit('pty-right', -1, undefined, { hostExitConfirmed: true }) - expect(panes(session())).toEqual([LEFT, RIGHT]) - }) - - it('keeps a split pane whose exit came with no code, as when its socket closed', async () => { - const { runtime, session } = splitRuntime() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: node-pty reports no exit code on a socket close, despite its types. - await runtime.onPtyExit('pty-right', undefined as unknown as number, undefined, { - providerExitObserved: true - }) - expect(panes(session())).toEqual([LEFT, RIGHT]) - }) - it('tells the phone a closed pane retired, and sends it nothing for a pane main keeps', async () => { const phone = async (runtime: OrcaRuntimeService) => { const tabs = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`) @@ -159,21 +129,4 @@ describe('layout-owner build: main closes an exited split pane itself', () => { await kept.onPtyExit('pty-right', 1, undefined, { providerExitObserved: true }) expect(await phone(kept)).toEqual({ version: 1, retired: [] }) }) - - it('keeps a newborn split that died while shown before any input or output', async () => { - const { runtime, session } = splitRuntime() - runtime.terminalRunFacts.recordSpawnCommit({ id: 'pty-right' }, { ptyId: 'pty-left' }) - await runtime.onPtyExit('pty-right', 0, undefined, { - providerExitObserved: true, - viewShown: true - }) - expect(panes(session())).toEqual([LEFT, RIGHT]) - }) - - it('closes that newborn split when it ended unseen, as a hidden pane always closed', async () => { - const { runtime, session } = splitRuntime() - runtime.terminalRunFacts.recordSpawnCommit({ id: 'pty-right' }, { ptyId: 'pty-left' }) - await runtime.onPtyExit('pty-right', 0, undefined, { providerExitObserved: true }) - expect(panes(session())).toEqual([LEFT]) - }) }) diff --git a/src/main/runtime/terminal/lifecycle/pty-registration.ts b/src/main/runtime/terminal/lifecycle/pty-registration.ts index 7c2238096f5..a9aabbd60a0 100644 --- a/src/main/runtime/terminal/lifecycle/pty-registration.ts +++ b/src/main/runtime/terminal/lifecycle/pty-registration.ts @@ -9,7 +9,7 @@ import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session import { recordFreshLaunch, retireLaunch } from '../launch/launch-evidence' import type { DaemonLaunchNote } from '../../../daemon/protocol/daemon-launch-note-protocol' import { OrcaRuntimeWithDaemonLaunchNote } from './daemon-launch-note' -import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-tab-id' +import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-tab-id' import { isTerminalLeafId, makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import { isTuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent-config' import { spawnSurfaceClaimSequence } from '../surface/pty-recorded-surface-topology' diff --git a/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts b/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts index f05674811c6..49840767fcc 100644 --- a/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts +++ b/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts @@ -23,7 +23,7 @@ import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types' import type { PtyIncarnationId } from '../../../../shared/terminal/pty/lifecycle/pty-incarnation' 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 { ExecutionHostId } from '../../../../shared/execution-host' import type { PtyProviderBufferSnapshot } from '../../../providers/pty-provider-contract' import type { PtyProcessInfo } from '../../../providers/pty-process-info' @@ -32,7 +32,7 @@ import type { TerminalProcessInspection as PtyProcessInspection } from '../../.. import type { WriteSettlement } from '../../../../shared/terminal/pty/lifecycle/pty-write-settlement' import type { StartupCwdFallback } from '../../../../shared/startup-cwd-fallback' import type { TerminalInputKind } from '../../../../shared/terminal/input/terminal-input-kind' -import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/workspace-layout-published' +import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' export type PtyInventoryRefreshOptions = { includeForegroundProcessEvidence?: boolean diff --git a/src/main/runtime/terminal/output/terminal-orchestration-authority.ts b/src/main/runtime/terminal/output/terminal-orchestration-authority.ts index 7c350e70396..403f3032430 100644 --- a/src/main/runtime/terminal/output/terminal-orchestration-authority.ts +++ b/src/main/runtime/terminal/output/terminal-orchestration-authority.ts @@ -9,7 +9,7 @@ import { parseLegacyNumericPaneKey, parsePaneKey } 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 { RecentPtyOutputBuffer } from './recent-pty-output-buffer' import type { ProjectExecutionRuntimeResolution } from '../../../../shared/execution-host/project-execution-runtime' import { resolveLocalProjectRuntimeForWorktreeId } from '../../../workspaces/execution-host/local-project-runtime-resolution' diff --git a/src/main/runtime/terminal/query/pty-terminal-summary.ts b/src/main/runtime/terminal/query/pty-terminal-summary.ts index 5d49d792e0f..559e76f1cd8 100644 --- a/src/main/runtime/terminal/query/pty-terminal-summary.ts +++ b/src/main/runtime/terminal/query/pty-terminal-summary.ts @@ -19,7 +19,7 @@ import { makePaneKey, parsePaneKey } 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 { ptyHoldsRecordedSurface, type PtySurfaceTopology diff --git a/src/main/runtime/terminal/records/runtime-leaf-record-projection.ts b/src/main/runtime/terminal/records/runtime-leaf-record-projection.ts index 5e13b11a881..08df636e05d 100644 --- a/src/main/runtime/terminal/records/runtime-leaf-record-projection.ts +++ b/src/main/runtime/terminal/records/runtime-leaf-record-projection.ts @@ -4,16 +4,16 @@ import type { PtyLivenessVerdict } from '../../../../shared/terminal/pty/lifecycle/pty-liveness-verdict' import type { RuntimeSyncedTab } from '../../../../shared/runtime-session-contracts' import { resolveTerminalTabTitle } from '../../../../shared/tab-title-resolution' -import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree' -import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load-types' +import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree' +import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-types' import { paneKeyOf, tabsInOrder, type LayoutTerminalTab } from '../../../../shared/workspace-layout/workspace-layout-model' -import type { PaneTerminalFact } from '../../../../shared/workspace-layout/workspace-layout-pane-status' -import { resolveWorkspaceSelection } from '../../../../shared/workspace-layout/workspace-layout-views' -import { pickStoredFields } from '../../../../shared/workspace-layout/stored-record-fields' +import type { PaneTerminalFact } from '../../../../shared/workspace-layout/commands/workspace-layout-pane-status' +import { resolveWorkspaceSelection } from '../../../../shared/workspace-layout/client-views/workspace-layout-views' +import { pickStoredFields } from '../../../../shared/workspace-layout/persistence/stored-record-fields' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' import { getPtyTerminalState } from '../wait/terminal-wait-results' diff --git a/src/main/runtime/terminal/recovery/held-pane-reopen.ts b/src/main/runtime/terminal/recovery/held-pane-reopen.ts index 87350e486ae..e18158bdd86 100644 --- a/src/main/runtime/terminal/recovery/held-pane-reopen.ts +++ b/src/main/runtime/terminal/recovery/held-pane-reopen.ts @@ -1,7 +1,7 @@ // For paired desktop Reopen, stop held newer services, then start still-held panes; main's frames carry the result. import type { NewerDaemonReopenResult } from '../../../../shared/newer-daemon-hold' -import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-published' +import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' import type { RestoredPane } from './restored-pane-start' /** The pane is 'held' in the layouts main publishes: the status a paired desktop's banner shows. */ diff --git a/src/main/runtime/terminal/recovery/shutdown-reconnect-facts.ts b/src/main/runtime/terminal/recovery/shutdown-reconnect-facts.ts index 1b584b21715..e3fd6e64fb1 100644 --- a/src/main/runtime/terminal/recovery/shutdown-reconnect-facts.ts +++ b/src/main/runtime/terminal/recovery/shutdown-reconnect-facts.ts @@ -4,7 +4,7 @@ import { isPaneSleepRecord } from '../../../../shared/agent-session/lifecycle/ag import { isOwnedSshTargetId } from '../../../../shared/execution-host' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import type { SshConnectionStatus } from '../../../../shared/ssh-types' -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' export type ShutdownReconnectFacts = Pick< diff --git a/src/main/runtime/terminal/surface/mobile-session-terminal-persistence-retirement.ts b/src/main/runtime/terminal/surface/mobile-session-terminal-persistence-retirement.ts index eb8a038203e..cc11b9f39e9 100644 --- a/src/main/runtime/terminal/surface/mobile-session-terminal-persistence-retirement.ts +++ b/src/main/runtime/terminal/surface/mobile-session-terminal-persistence-retirement.ts @@ -5,7 +5,7 @@ import { repairMobileSessionTabGroupsAfterRetirement, type RetiredTerminalSurface } from './mobile-session-terminal-retirement' -import { retireLeavesFromTerminalLayout } from '../../../../shared/workspace-layout/terminal-layout-leaf-retirement' +import { retireLeavesFromTerminalLayout } from '../../../../shared/workspace-layout/terminal-topology/terminal-layout-leaf-retirement' import { rebaseWorkspaceSessionTerminalMembership } from './workspace-session-terminal-membership-authority' import { advanceTerminalTopologyRevision } from '../../../persistence/terminal-topology/terminal-topology-membership' diff --git a/src/main/runtime/terminal/surface/mobile-session-terminal-projection.ts b/src/main/runtime/terminal/surface/mobile-session-terminal-projection.ts index df6823d1c71..5c8a38adfc5 100644 --- a/src/main/runtime/terminal/surface/mobile-session-terminal-projection.ts +++ b/src/main/runtime/terminal/surface/mobile-session-terminal-projection.ts @@ -5,7 +5,7 @@ import { resolveTerminalTabTitle } from '../../../../shared/tab-title-resolution import { readCanonicalPtyOwners, resolveCanonicalPtyClaim -} from '../../../../shared/workspace-layout/legacy-terminal-row-claims' +} from '../../../../shared/workspace-layout/terminal-topology/legacy-terminal-row-claims' import { cloneTerminalLayoutSnapshot, collectPersistedTerminalLeafIds, diff --git a/src/main/runtime/terminal/surface/mobile-session-terminal-retirement-proof.ts b/src/main/runtime/terminal/surface/mobile-session-terminal-retirement-proof.ts index 82c45a70ce1..1be2c3cb1bc 100644 --- a/src/main/runtime/terminal/surface/mobile-session-terminal-retirement-proof.ts +++ b/src/main/runtime/terminal/surface/mobile-session-terminal-retirement-proof.ts @@ -6,7 +6,7 @@ import { appendRetiredTerminalSurfaceProofs, dropRetirementProofsForLiveSurfaces, retirementProofKey -} from '../../../../shared/workspace-layout/terminal-retirement-proof-ledger' +} from '../../../../shared/workspace-layout/terminal-surfaces/terminal-retirement-proof-ledger' /** Carry durable host close proofs through renderer snapshots. Inherit worktreeInstanceId on identity-less host writes * so renderer(A) -> host -> renderer(B) cannot launder A's proofs into B. */ diff --git a/src/main/runtime/terminal/surface/mobile-session-terminal-retirement.ts b/src/main/runtime/terminal/surface/mobile-session-terminal-retirement.ts index fb25bc772a5..c7d48fd6816 100644 --- a/src/main/runtime/terminal/surface/mobile-session-terminal-retirement.ts +++ b/src/main/runtime/terminal/surface/mobile-session-terminal-retirement.ts @@ -8,8 +8,8 @@ import type { RuntimeMobileSessionTerminalTab } from '../../../../shared/runtime-mobile-session-tab-contracts' import type { TabGroupLayoutNode } from '../../../../shared/tab-types' -import { appendRetiredTerminalSurfaceProofs } from '../../../../shared/workspace-layout/terminal-retirement-proof-ledger' -import { retireLeavesFromTerminalLayout } from '../../../../shared/workspace-layout/terminal-layout-leaf-retirement' +import { appendRetiredTerminalSurfaceProofs } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-retirement-proof-ledger' +import { retireLeavesFromTerminalLayout } from '../../../../shared/workspace-layout/terminal-topology/terminal-layout-leaf-retirement' export type RetiredTerminalSurface = { worktreeId: string diff --git a/src/main/runtime/terminal/surface/orca-runtime-failed-start-pane-layout-owner.test.ts b/src/main/runtime/terminal/surface/orca-runtime-failed-start-pane-layout-owner.test.ts deleted file mode 100644 index 1a767dc459e..00000000000 --- a/src/main/runtime/terminal/surface/orca-runtime-failed-start-pane-layout-owner.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree' -import { NEWER_DAEMON_HOLD_ERROR } from '../../../../shared/newer-daemon-hold' -import { - setupLayoutOwnerRuntime, - WORKTREE_ID -} from '../../orca-runtime-tests/orca-runtime-layout-owner-start-fixture' -import { PaneOwnerContentionError } from '../../../ipc/pty/pane/pane-spawn-operation' - -vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) - -describe('layout-owner build: a start main could not make keeps its pane, showing why', () => { - it('writes the pane and shows the failure when the start failed before its pane was written', async () => { - const { runtime, spawn, session } = setupLayoutOwnerRuntime() - vi.spyOn(console, 'warn').mockImplementation(() => {}) - - await expect( - runtime.createTerminal(`id:${WORKTREE_ID}`, { - title: 'Setup', - keepPaneOnStartFailure: true, - startFailure: new Error('agent runtime needs repair') - }) - ).rejects.toThrow('agent runtime needs repair') - - expect(spawn).not.toHaveBeenCalled() - const [row] = session().tabsByWorktree[WORKTREE_ID] ?? [] - expect(row?.customTitle).toBe('Setup') - const [leafId] = collectLeafIds(session().terminalLayoutsByTabId[row!.id]?.root) - expect(runtime.startStatusOf(`${row!.id}:${leafId}`)).toEqual({ - status: 'failed', - reason: 'agent runtime needs repair' - }) - }) - - it('keeps the pane it wrote when the terminal itself fails to start', async () => { - const { runtime, spawn, session } = setupLayoutOwnerRuntime() - spawn.mockRejectedValueOnce(new Error('shell not found')) - - await expect( - runtime.createTerminal(`id:${WORKTREE_ID}`, { keepPaneOnStartFailure: true }) - ).rejects.toThrow('shell not found') - - const [row] = session().tabsByWorktree[WORKTREE_ID] ?? [] - expect(row).toBeDefined() - const { tabId, leafId } = spawn.mock.calls[0]![0] - expect(row!.id).toBe(tabId) - expect(runtime.startStatusOf(`${tabId}:${leafId}`)).toEqual({ - status: 'failed', - reason: 'shell not found' - }) - }) - - it("shows the pane held, not failed, when a newer build's terminal service may still run it", async () => { - const { runtime, spawn } = setupLayoutOwnerRuntime() - spawn.mockRejectedValueOnce(new Error(NEWER_DAEMON_HOLD_ERROR)) - - await expect( - runtime.createTerminal(`id:${WORKTREE_ID}`, { keepPaneOnStartFailure: true }) - ).rejects.toThrow(NEWER_DAEMON_HOLD_ERROR) - - const { tabId, leafId } = spawn.mock.calls[0]![0] - expect(runtime.startStatusOf(`${tabId}:${leafId}`)).toEqual({ - status: 'held', - reason: NEWER_DAEMON_HOLD_ERROR - }) - }) - - it('shows no failure when another owner won the pane, whose terminal the window attaches to', async () => { - const { runtime, spawn } = setupLayoutOwnerRuntime() - spawn.mockRejectedValueOnce(new PaneOwnerContentionError()) - - await expect( - runtime.createTerminal(`id:${WORKTREE_ID}`, { keepPaneOnStartFailure: true }) - ).rejects.toThrow('terminal_pane_owner_changed') - - const { tabId, leafId } = spawn.mock.calls[0]![0] - expect(runtime.startStatusOf(`${tabId}:${leafId}`)).toBeNull() - }) - - it('does not bring back a pane the user closed while its start was failing', async () => { - const { runtime, spawn, session } = setupLayoutOwnerRuntime() - spawn.mockImplementationOnce(async ({ tabId }) => { - await runtime.applyLayoutCommand({ - type: 'closeTabs', - workspace: WORKTREE_ID, - tabIds: [tabId!], - recordClose: 'user' - }) - throw new Error('shell not found') - }) - - await expect( - runtime.createTerminal(`id:${WORKTREE_ID}`, { keepPaneOnStartFailure: true }) - ).rejects.toThrow('shell not found') - - expect(session().tabsByWorktree[WORKTREE_ID] ?? []).toEqual([]) - }) -}) diff --git a/src/main/runtime/terminal/surface/orca-runtime-terminal-close-spill-partition.spec.ts b/src/main/runtime/terminal/surface/orca-runtime-terminal-close-spill-partition.spec.ts index 4b7cabfd08c..d2908a7c6a0 100644 --- a/src/main/runtime/terminal/surface/orca-runtime-terminal-close-spill-partition.spec.ts +++ b/src/main/runtime/terminal/surface/orca-runtime-terminal-close-spill-partition.spec.ts @@ -1,23 +1,17 @@ -import { withDurableRuntimeStore } from '../../orca-runtime-tests/runtime-durable-store-fixture' +import '../../orca-runtime-tests/runtime-durable-store-fixture' import { describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../../shared/constants' -import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' -import { hasClosedTerminalTabRecord } from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones' -import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-session-contracts' +import '../../../../shared/execution-host' +import type { ExecutionHostId } from '../../../../shared/execution-host' +import '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones' + import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' -import { buildHeadlessMobileSessionTerminalTabs } from './mobile-session-terminal-projection' -import { OrcaRuntimeService } from '../../orca-runtime' +import './mobile-session-terminal-projection' +import '../../orca-runtime' import { terminalSurfaceCloseMutation } from './terminal-surface-close' import { capturePersistedTerminalTabCopy } from './workspace-session-terminal-tab-retirement-identity' export function registerOrcaRuntimeTerminalCloseSpillPartitionTests(): void { - const REPO = { - id: 'repo', - path: '/worktree', - displayName: 'repo', - badgeColor: 'blue', - addedAt: 1 - } as const const WORKTREE_ID = 'repo::/worktree' const LEAF = '11111111-1111-4111-8111-111111111111' const SPILL_HOST_ID: ExecutionHostId = 'runtime:env-1' @@ -51,83 +45,7 @@ export function registerOrcaRuntimeTerminalCloseSpillPartitionTests(): void { } } - function harness(sessions: Map) { - const kill = vi.fn(() => true) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This runtime fixture supplies the persistence methods exercised by the test. - const store = withDurableRuntimeStore({ - getRepos: () => [REPO], - getRepo: (id: string) => (id === REPO.id ? REPO : undefined), - getWorktreeMeta: () => undefined, - getAllWorktreeMeta: () => ({}), - getWorkspaceSessionHostIds: () => [...sessions.keys()], - getWorkspaceSession: (hostId?: ExecutionHostId) => - sessions.get(hostId ?? LOCAL_EXECUTION_HOST_ID) ?? getDefaultWorkspaceSession(), - setWorkspaceSession: (session: WorkspaceSessionState, hostId?: ExecutionHostId) => - sessions.set(hostId ?? LOCAL_EXECUTION_HOST_ID, session), - flushOrThrow: vi.fn() - }) as never - const runtime = new OrcaRuntimeService(store) - runtime.setPtyController({ - write: () => true, - kill, - getForegroundProcess: async () => null, - listProcesses: async () => [{ id: 'target-pty', cwd: REPO.path, title: 'Terminal' }] - }) - const tabs = [...sessions.values()].flatMap((session) => - buildHeadlessMobileSessionTerminalTabs( - WORKTREE_ID, - session.tabsByWorktree[WORKTREE_ID] ?? [], - session - ) - ) - const snapshot: RuntimeMobileSessionTabsSnapshot = { - worktree: WORKTREE_ID, - publicationEpoch: 'headless', - snapshotVersion: 1, - activeGroupId: null, - activeTabId: tabs[0]?.id ?? null, - activeTabType: 'terminal', - tabs: tabs.filter((tab, index) => tabs.findIndex((other) => other.id === tab.id) === index) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the published snapshot map is protected; seeding it stands in for a prior publication. - const internals = runtime as unknown as { - mobileSessionTabsByWorktree: Map - } - internals.mobileSessionTabsByWorktree.set(WORKTREE_ID, snapshot) - return { runtime, kill } - } - describe('terminal close across local and runtime session partitions (#18202)', () => { - it('closes and records every copy when legacy reattach left one in two partitions', async () => { - const sessions = new Map([ - [LOCAL_EXECUTION_HOST_ID, sessionWithTab('unrelated', 'unrelated-pty')], - [SPILL_HOST_ID, sessionWithTab('target', 'target-pty')], - ['runtime:env-0', sessionWithTab('target', 'target-pty')] - ]) - const { runtime } = harness(sessions) - runtime.registerPty('target-pty', WORKTREE_ID, null, { - tabId: 'target', - leafId: LEAF, - incarnationId: 'target-incarnation' - }) - - const terminal = (await runtime.listTerminals()).terminals.find( - (candidate) => candidate.tabId === 'target' - )! - await expect(runtime.closeTerminalTab(terminal.handle)).resolves.toMatchObject({ - tabId: 'target' - }) - - for (const hostId of [SPILL_HOST_ID, 'runtime:env-0'] as const) { - const session = sessions.get(hostId)! - expect(session.tabsByWorktree[WORKTREE_ID]).toEqual([]) - expect( - hasClosedTerminalTabRecord(session.closedTerminalTabTombstonesByTabId, 'target') - ).toBe(true) - } - expect(sessions.get(LOCAL_EXECUTION_HOST_ID)?.tabsByWorktree[WORKTREE_ID]).toHaveLength(1) - }) - it('refuses when a second copy was rebound after the close was asked', () => { const sessions = new Map([ [SPILL_HOST_ID, sessionWithTab('target', 'target-pty')], diff --git a/src/main/runtime/terminal/surface/pane-live-facts.test.ts b/src/main/runtime/terminal/surface/pane-live-facts.test.ts index 3d790dbad7a..b4902d7a107 100644 --- a/src/main/runtime/terminal/surface/pane-live-facts.test.ts +++ b/src/main/runtime/terminal/surface/pane-live-facts.test.ts @@ -10,7 +10,7 @@ import { bindingOf } from './pane-live-facts' import { PaneResumeOutcomes, PaneStartStatuses } from './pane-start-statuses' -import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/workspace-layout-published' +import type { PaneResumeOutcome } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' const LEAF_ID = '11111111-1111-4111-8111-111111111111' const PANE_KEY = `tab-1:${LEAF_ID}` diff --git a/src/main/runtime/terminal/surface/pane-start-statuses.ts b/src/main/runtime/terminal/surface/pane-start-statuses.ts index 10e6c20ddbd..f716070473a 100644 --- a/src/main/runtime/terminal/surface/pane-start-statuses.ts +++ b/src/main/runtime/terminal/surface/pane-start-statuses.ts @@ -5,7 +5,7 @@ import type { PaneResumeOutcome, PaneStartStatus, PublishedWorkspaceLayout -} from '../../../../shared/workspace-layout/workspace-layout-published' +} from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' import { bindingOf, PaneLiveFacts } from './pane-live-facts' export type PaneStartFact = { status: PaneStartStatus; reason?: string } diff --git a/src/main/runtime/terminal/surface/pty-pane-query.ts b/src/main/runtime/terminal/surface/pty-pane-query.ts index d61013ed62f..e8bf61326b8 100644 --- a/src/main/runtime/terminal/surface/pty-pane-query.ts +++ b/src/main/runtime/terminal/surface/pty-pane-query.ts @@ -1,6 +1,6 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' -import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree' -import type { WorkspaceLayoutLoadContext } from '../../../../shared/workspace-layout/workspace-layout-load-types' +import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree' +import type { WorkspaceLayoutLoadContext } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-types' import { isPaneSleepRecord } from '../../../../shared/agent-session/lifecycle/agent-session-resume' import { pairedRuntimeHostOfBinding, @@ -12,7 +12,7 @@ import { import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' 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 type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records' export type PtyPane = { diff --git a/src/main/runtime/terminal/surface/published-pane-live-titles.ts b/src/main/runtime/terminal/surface/published-pane-live-titles.ts index 060455f2a0f..6d5eaaece5d 100644 --- a/src/main/runtime/terminal/surface/published-pane-live-titles.ts +++ b/src/main/runtime/terminal/surface/published-pane-live-titles.ts @@ -1,4 +1,4 @@ -import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-published' +import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' /** `titles` in tab and pane order, null for a pane with none; the same layout when all are null. */ export function withLiveTitles( diff --git a/src/main/runtime/terminal/surface/terminal-surface-close.ts b/src/main/runtime/terminal/surface/terminal-surface-close.ts index 4c28b35c94f..bde21115a65 100644 --- a/src/main/runtime/terminal/surface/terminal-surface-close.ts +++ b/src/main/runtime/terminal/surface/terminal-surface-close.ts @@ -3,12 +3,12 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session import { hasClosedTerminalTabRecord, recordClosedTerminalTabTombstone -} from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones' +} from '../../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones' import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/workspace-layout/terminal-tab-types' -import type { TerminalSurfaceCloseTarget } from '../../../../shared/workspace-layout/terminal-surface-close-target' +import type { TerminalSurfaceCloseTarget } from '../../../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target' import { closeTerminalTabInWorkspaceSession, type WorkspaceSessionTerminalTabCloseResult diff --git a/src/main/runtime/terminal/surface/terminal-surface-index.ts b/src/main/runtime/terminal/surface/terminal-surface-index.ts index c440fcf8735..00e7690c3ed 100644 --- a/src/main/runtime/terminal/surface/terminal-surface-index.ts +++ b/src/main/runtime/terminal/surface/terminal-surface-index.ts @@ -10,12 +10,12 @@ import type { RuntimeSyncedTab, RuntimeSyncWindowGraph } from '../../../../shared/runtime-session-contracts' -import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load-types' +import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-types' import type { LayoutTab, WorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-model' -import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree' +import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree' import { bindingOf } from './pane-live-facts' import { worktreeIdsEqual } from '../../../../shared/worktree/id' import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id' diff --git a/src/main/runtime/terminal/surface/workspace-layout-frame-model-check.ts b/src/main/runtime/terminal/surface/workspace-layout-frame-model-check.ts index aafbe2ce07f..f6fa812b6b7 100644 --- a/src/main/runtime/terminal/surface/workspace-layout-frame-model-check.ts +++ b/src/main/runtime/terminal/surface/workspace-layout-frame-model-check.ts @@ -1,7 +1,7 @@ // Applying each records frame must equal a fresh partition projection; tests check every frame and dev builds log Store-write mismatches (workspace-layout-frame-observer.ts). -import { stableJson } from '../../../../shared/workspace-layout/workspace-layout-load-report' -import { applyWorkspaceLayoutRecordChange } from '../../../../shared/workspace-layout/workspace-layout-records' +import { stableJson } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-report' +import { applyWorkspaceLayoutRecordChange } from '../../../../shared/workspace-layout/published-layout/workspace-layout-records' import type { LayoutFrameObservation } from './workspace-layout-frame-observer' /** Empty when the frame holds exactly what changed. */ diff --git a/src/main/runtime/terminal/surface/workspace-layout-frame-observer.ts b/src/main/runtime/terminal/surface/workspace-layout-frame-observer.ts index 9ebf47fc0b9..06d03000961 100644 --- a/src/main/runtime/terminal/surface/workspace-layout-frame-observer.ts +++ b/src/main/runtime/terminal/surface/workspace-layout-frame-observer.ts @@ -1,9 +1,9 @@ // Tests check every records frame against a fresh projection and freeze partition reads; development logs structural frame mismatches, while release builds do neither. import { logLayoutFrameModelErrors } from './workspace-layout-frame-model-check' -import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load-types' -import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-published' -import type { WorkspaceLayoutRecordsFrame } from '../../../../shared/workspace-layout/workspace-layout-stream-frames' +import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-types' +import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' +import type { WorkspaceLayoutRecordsFrame } from '../../../../shared/workspace-layout/published-layout/workspace-layout-stream-frames' export type LayoutFrameObservation = { frame: WorkspaceLayoutRecordsFrame diff --git a/src/main/runtime/terminal/surface/workspace-layout-operation-frames.test.ts b/src/main/runtime/terminal/surface/workspace-layout-operation-frames.test.ts index fe4b8ade105..3aaa3a4e3ff 100644 --- a/src/main/runtime/terminal/surface/workspace-layout-operation-frames.test.ts +++ b/src/main/runtime/terminal/surface/workspace-layout-operation-frames.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../../shared/constants' import type { ExecutionHostId } from '../../../../shared/execution-host' import type { GeneratedTabTitle } from '../../../../shared/workspace-layout/workspace-layout-command-types' -import type { WorkspaceLayoutStreamEvent } from '../../../../shared/workspace-layout/workspace-layout-stream-frames' + import type { Store } from '../../../persistence/loading-store/store' import { WindowSession } from '../../../persistence/terminal-topology/terminal-topology-window-session-fixture' import { @@ -17,7 +17,7 @@ import { } from '../../../persistence/terminal-topology/terminal-topology-profile-fixture' import { applyWindowLayoutCommand } from '../layout/runtime-layout-apply' import { installLayoutFrameModelCheck } from './workspace-layout-frame-model-check.test-fixture' -import { WorkspaceLayoutStream } from './workspace-layout-stream' +import './workspace-layout-stream' const layoutOwner = vi.hoisted(() => ({ isMain: true })) vi.mock('../../../../shared/layout-owner-switch', () => ({ @@ -62,14 +62,6 @@ async function storeWith(tabs: Record): Promise { const tabIdOf = (workspace: string, index: number) => `tab-${workspace.replace(/\W/g, '-')}-${index}` -function streamOver(store: Store): WorkspaceLayoutStream { - return new WorkspaceLayoutStream({ - store: () => store, - homeHostId: homeOf, - liveTitle: () => null - }) -} - function titles(tabs: Record): GeneratedTabTitle[] { return Object.entries(tabs).flatMap(([workspace, count]) => Array.from({ length: count }, (_, index) => ({ @@ -100,38 +92,6 @@ afterEach(() => { } }) -describe('one records frame per main operation', () => { - it('publishes a pane bind as one binding record, and a same-id respawn as one more', async () => { - const store = await storeWith({ [GIT]: 2 }) - const frames: WorkspaceLayoutStreamEvent[] = [] - streamOver(store).subscribe((frame) => frames.push(frame)) - const bind = (incarnationId: string) => - store.persistPtyBinding({ - worktreeId: GIT, - tabId: tabIdOf(GIT, 1), - leafId: leafOf(1), - ptyId: 'pty-1', - incarnationId - }) - - await bind('inc-1') - await bind('inc-2') - - const binding = (ptyGeneration: number) => [ - { - key: GIT, - change: 'records', - bindings: { set: [{ leafId: leafOf(1), ptyId: 'pty-1', ptyGeneration }] } - } - ] - expect(frames.map((frame) => frame.type === 'records' && frame.workspaces)).toEqual([ - binding(1), - binding(2) - ]) - expect(check.failures).toEqual([]) - }) -}) - describe('a throwing operation listener', () => { it('fails neither the committed operation nor the listeners after it', async () => { const store = await storeWith({ [GIT]: 1 }) diff --git a/src/main/runtime/terminal/surface/workspace-layout-projection.ts b/src/main/runtime/terminal/surface/workspace-layout-projection.ts index 5dd2873215e..a9a6181f6f2 100644 --- a/src/main/runtime/terminal/surface/workspace-layout-projection.ts +++ b/src/main/runtime/terminal/surface/workspace-layout-projection.ts @@ -2,17 +2,17 @@ import type { ExecutionHostId } from '../../../../shared/execution-host' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' -import { loadWorkspaceLayoutWithoutReport } from '../../../../shared/workspace-layout/workspace-layout-load' -import { stableJson } from '../../../../shared/workspace-layout/workspace-layout-load-report' +import { loadWorkspaceLayoutWithoutReport } from '../../../../shared/workspace-layout/persistence/workspace-layout-load' +import { stableJson } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-report' import type { LoadedWorkspaceLayout, WorkspaceLayoutLoadContext -} from '../../../../shared/workspace-layout/workspace-layout-load-types' -import { nameBasedLoadContext } from '../../../../shared/workspace-layout/workspace-layout-minted-ids' +} from '../../../../shared/workspace-layout/persistence/workspace-layout-load-types' +import { nameBasedLoadContext } from '../../../../shared/workspace-layout/persistence/workspace-layout-minted-ids' import { publishWorkspaceLayout, type PublishedWorkspaceLayout -} from '../../../../shared/workspace-layout/workspace-layout-published' +} from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' import type { RuntimeStore } from '../../construction/runtime-store-contract' export type ProjectedLayout = { layout: PublishedWorkspaceLayout; json: string } diff --git a/src/main/runtime/terminal/surface/workspace-layout-stream.test.ts b/src/main/runtime/terminal/surface/workspace-layout-stream.test.ts index fab3572edd8..f721e7a5b7a 100644 --- a/src/main/runtime/terminal/surface/workspace-layout-stream.test.ts +++ b/src/main/runtime/terminal/surface/workspace-layout-stream.test.ts @@ -4,15 +4,14 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session import { localDesktopSession, relaySshSession -} from '../../../../shared/workspace-layout/workspace-layout-profile.test-fixture' +} from '../../../../shared/workspace-layout/persistence/workspace-layout-profile.test-fixture' import { - emptySession, FOLDER_KEY, GIT_KEY, SSH_KEY -} from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture' +} from '../../../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture' import { WorkspaceLayoutStream } from './workspace-layout-stream' -import { collectLayoutEvents } from '../../orca-runtime-tests/workspace-layout-stream-events.test-fixture' +import '../../orca-runtime-tests/workspace-layout-stream-events.test-fixture' const SSH_HOST: ExecutionHostId = 'ssh:target-1' @@ -66,15 +65,6 @@ function setup() { return { ...fake, stream, homes, partitions, liveTitles, liveTitle } } -const flush = () => new Promise((resolve) => queueMicrotask(resolve)) - -function subscribeEvents(stream: WorkspaceLayoutStream) { - const collector = collectLayoutEvents() - const subscription = stream.subscribe(collector.listener) - collector.seed(subscription.snapshot) - return { events: collector.events, collector, subscription } -} - describe('WorkspaceLayoutStream', () => { it('snapshots each workspace from its owning partition only', () => { const { stream, homes } = setup() @@ -94,115 +84,4 @@ describe('WorkspaceLayoutStream', () => { }).subscribe(() => {}).snapshot expect(strayless.map((entry) => entry.key)).toEqual([GIT_KEY, FOLDER_KEY]) }) - - it('numbers each change frame, so a reply can name the frame holding its write', async () => { - const { stream, write } = setup() - const { events, subscription: first } = subscribeEvents(stream) - const session = localDesktopSession() - write(LOCAL_EXECUTION_HOST_ID, session) - await flush() - expect(events).toEqual([]) - expect(stream.settledSeq()).toBe(first.seq) - - session.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() - write(LOCAL_EXECUTION_HOST_ID, session) - // Read before the queued flush runs: the write's frame goes out first. - const replySeq = stream.settledSeq() - expect(events).toEqual([expect.objectContaining({ key: GIT_KEY, seq: first.seq + 1 })]) - expect(replySeq).toBe(first.seq + 1) - await flush() - expect(events).toHaveLength(1) - - first.unsubscribe() - const second = stream.subscribe(() => {}) - expect(second.seq).toBeGreaterThanOrEqual(replySeq) - second.unsubscribe() - }) - - it('keeps delivering to every listener when one throws', async () => { - const { stream, write } = setup() - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - stream.subscribe(() => { - throw new Error('listener failed') - }) - const { events } = subscribeEvents(stream) - const session = localDesktopSession() - session.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() - write(LOCAL_EXECUTION_HOST_ID, session) - await flush() - expect(events.map((event) => [event.type, event.key])).toEqual([['workspace', GIT_KEY]]) - write(LOCAL_EXECUTION_HOST_ID, structuredClone(session)) - await flush() - expect(events).toHaveLength(1) - warn.mockRestore() - }) - - it('keeps every invented id when an earlier workspace or tab goes away', async () => { - // Headless rows without tab-bar or pane layout require synthesized groups and panes. - const rowsOnly = (keys: string[], tabs: string[]): WorkspaceSessionState => { - const session = emptySession() - for (const key of keys) { - session.tabsByWorktree[key] = tabs.map((tab, index) => ({ - id: `${key}-${tab}`, - ptyId: null, - worktreeId: key, - title: tab, - customTitle: null, - color: null, - sortOrder: index, - createdAt: Number(tab.slice(1)) - })) - } - return session - } - const keys = ['repo-1::/a', 'repo-1::/b', 'repo-1::/c'] - const fake = fakeStore(new Map([[LOCAL_EXECUTION_HOST_ID, rowsOnly(keys, ['t1', 't2'])]])) - const stream = new WorkspaceLayoutStream({ - store: () => fake.store, - homeHostId: () => LOCAL_EXECUTION_HOST_ID, - liveTitle: () => null - }) - const { events, subscription } = subscribeEvents(stream) - const before = new Map( - subscription.snapshot.map((entry) => [entry.key, JSON.stringify(entry.layout)]) - ) - fake.write(LOCAL_EXECUTION_HOST_ID, rowsOnly(keys.slice(1), ['t1', 't2'])) - await flush() - expect(events).toEqual([{ type: 'removed', key: 'repo-1::/a', seq: expect.any(Number) }]) - - const withoutFirstTab = rowsOnly(keys.slice(1), ['t2']) - fake.write(LOCAL_EXECUTION_HOST_ID, withoutFirstTab) - await flush() - const after = new Map( - events.flatMap((event) => (event.type === 'workspace' ? [[event.key, event.layout]] : [])) - ) - for (const key of keys.slice(1)) { - const earlier = JSON.parse(before.get(key)!) - const later = after.get(key)! - expect(later.groups.map((group) => group.id)).toEqual( - earlier.groups.map((group: { id: string }) => group.id) - ) - expect(JSON.stringify(later.tabs)).toBe(JSON.stringify(earlier.tabs.slice(1))) - } - }) - - it('re-projects every partition after a failed projection', async () => { - const { stream, store, write } = setup() - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { events } = subscribeEvents(stream) - store.getWorkspaceSession.mockImplementationOnce(() => { - throw new Error('read failed') - }) - const local = localDesktopSession() - local.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() - write(LOCAL_EXECUTION_HOST_ID, local) - await flush() - expect(events).toEqual([]) - // A later write elsewhere must recover the local edit missed by the failed projection. - write(SSH_HOST, relaySshSession('target-1')) - await flush() - expect(events.map((event) => [event.type, event.key])).toEqual([['workspace', GIT_KEY]]) - expect(warn).toHaveBeenCalledTimes(1) - warn.mockRestore() - }) }) diff --git a/src/main/runtime/terminal/surface/workspace-layout-stream.ts b/src/main/runtime/terminal/surface/workspace-layout-stream.ts index 521845381a3..eb181b875d6 100644 --- a/src/main/runtime/terminal/surface/workspace-layout-stream.ts +++ b/src/main/runtime/terminal/surface/workspace-layout-stream.ts @@ -4,18 +4,18 @@ import { randomUUID } from 'node:crypto' import type { ExecutionHostId } from '../../../../shared/execution-host' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' -import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load-types' -import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-published' +import type { LoadedWorkspaceLayout } from '../../../../shared/workspace-layout/persistence/workspace-layout-load-types' +import type { PublishedWorkspaceLayout } from '../../../../shared/workspace-layout/published-layout/workspace-layout-published' import { diffPublishedLayouts, indexPublishedLayout, type IndexedLayoutRecords, type WorkspaceLayoutRecordChange -} from '../../../../shared/workspace-layout/workspace-layout-records' +} 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' import type { RuntimeStore } from '../../construction/runtime-store-contract' import { withLiveTitles } from './published-pane-live-titles' import { paneStartFactsOf, withPaneStartFacts } from './pane-start-statuses' diff --git a/src/main/runtime/workspaces/persisted-surfaces/has-exact-persisted-terminal-surface-identity.ts b/src/main/runtime/workspaces/persisted-surfaces/has-exact-persisted-terminal-surface-identity.ts index f8dd1f4e3fd..b434a61907b 100644 --- a/src/main/runtime/workspaces/persisted-surfaces/has-exact-persisted-terminal-surface-identity.ts +++ b/src/main/runtime/workspaces/persisted-surfaces/has-exact-persisted-terminal-surface-identity.ts @@ -1,7 +1,7 @@ import { resolveTerminalSessionWorktreeId } from '../catalog/runtime-worktree-path-identity' import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' -import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-pane-tree' +import { layoutContainsLeafId } from '../../../../shared/workspace-layout/terminal-topology/terminal-pane-tree' import { worktreeIdsEqual } from '../../../../shared/worktree/id' import { RuntimeArtifactOperations } from '../../artifacts/runtime-artifact-operations' diff --git a/src/main/runtime/workspaces/session-hosts/folder-workspace-mixed-host-routing.test.ts b/src/main/runtime/workspaces/session-hosts/folder-workspace-mixed-host-routing.test.ts index 474c9cf2e6e..37163a3ca3b 100644 --- a/src/main/runtime/workspaces/session-hosts/folder-workspace-mixed-host-routing.test.ts +++ b/src/main/runtime/workspaces/session-hosts/folder-workspace-mixed-host-routing.test.ts @@ -6,15 +6,15 @@ import { ROUTING_REPOS } from '../../../../shared/folder-workspaces/folder-workspace-host-routing.test-fixture' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' -import { localDesktopSession } from '../../../../shared/workspace-layout/workspace-layout-profile.test-fixture' +import { localDesktopSession } from '../../../../shared/workspace-layout/persistence/workspace-layout-profile.test-fixture' import { emptySession, FOLDER_KEY, GIT_KEY -} from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture' +} from '../../../../shared/workspace-layout/persistence/workspace-layout-session.test-fixture' import { OrcaRuntimeService } from '../../orca-runtime' import { WorkspaceLayoutStream } from '../../terminal/surface/workspace-layout-stream' -import { collectLayoutEvents } from '../../orca-runtime-tests/workspace-layout-stream-events.test-fixture' +import '../../orca-runtime-tests/workspace-layout-stream-events.test-fixture' import { partialRuntimeStore, storeRepo @@ -85,7 +85,6 @@ describe('a workspace the router throws on', () => { const write = () => writeListeners.forEach((listener) => listener(null)) return { stream, route, write } } - const flush = () => new Promise((resolve) => queueMicrotask(resolve)) it('is left out, and every other workspace is still published, logged once', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) @@ -94,25 +93,6 @@ describe('a workspace the router throws on', () => { expect(warn).toHaveBeenCalledTimes(1) }) - it('keeps its last published layout instead of sending a removal', async () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { stream, route, write } = streamThrowingOn(FOLDER_KEY) - route.throws = false - const collector = collectLayoutEvents() - const first = stream.subscribe(collector.listener) - collector.seed(first.snapshot) - const events = collector.events - expect(first.snapshot.map((entry) => entry.key)).toEqual([GIT_KEY, FOLDER_KEY]) - route.throws = true - write() - await flush() - expect(events).toEqual([]) - const later = stream.subscribe(() => {}).snapshot - expect(later.find((entry) => entry.key === FOLDER_KEY)?.layout).toEqual( - first.snapshot.find((entry) => entry.key === FOLDER_KEY)?.layout - ) - }) - it('still fails the whole projection in a window-owned build', () => { mocks.layoutOwnerIsMain = false vi.spyOn(console, 'warn').mockImplementation(() => {}) diff --git a/src/main/ssh/migration-source/orcad-migration-bridged-panes.ts b/src/main/ssh/migration-source/orcad-migration-bridged-panes.ts index 812a35bbf29..f7cba30c64e 100644 --- a/src/main/ssh/migration-source/orcad-migration-bridged-panes.ts +++ b/src/main/ssh/migration-source/orcad-migration-bridged-panes.ts @@ -1,6 +1,6 @@ import type { OrcadMigrationCatalogPayload } from '../../../shared/orcad-migration/orcad-migration-manifest' import type { SshRemotePtyLease } from '../../../shared/ssh-types' -import { collectLeafIds } from '../../../shared/workspace-layout/terminal-pane-layout-tree' +import { collectLeafIds } from '../../../shared/workspace-layout/terminal-topology/terminal-pane-layout-tree' import { normalizeWorkspaceSessionKeyToWorkspaceId } from '../../../shared/workspace-scope' export type OrcadMigrationPaneLeasePredicate = (lease: SshRemotePtyLease) => boolean diff --git a/src/main/window/runtime-window-lifecycle.ts b/src/main/window/runtime-window-lifecycle.ts index 96b3d4f769d..da078c6d3b4 100644 --- a/src/main/window/runtime-window-lifecycle.ts +++ b/src/main/window/runtime-window-lifecycle.ts @@ -5,7 +5,7 @@ import { ipcMain, type BrowserWindow } from 'electron' import type { CreateWorktreeResult } from '../../shared/worktree/create-types' import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' import type { RuntimeMobileSessionTabMove } from '../../shared/runtime-types' -import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-reveal-identity' +import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-surfaces/terminal-reveal-identity' import { runWorktreeChangeInvalidators } from '../ipc/worktrees/listing/worktree-change-invalidators' import { requestAgentLaunchTabPublishFromRenderer } from './agent-launch-tab-publish-relay' import { requestAgentPaneActivityFromRenderer } from './agent-pane-activity-relay' diff --git a/src/main/window/terminal-pane-move-commit-fence.ts b/src/main/window/terminal-pane-move-commit-fence.ts index 2e671be6cd9..ced98706ad8 100644 --- a/src/main/window/terminal-pane-move-commit-fence.ts +++ b/src/main/window/terminal-pane-move-commit-fence.ts @@ -1,4 +1,4 @@ -import type { TerminalLeafMoveResult } from '../../shared/workspace-layout/terminal-leaf-move' +import type { TerminalLeafMoveResult } from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' // Why its own module: the plain-Node runtime commits leaf moves too, so this must stay Electron-free. diff --git a/src/main/window/terminal-pane-move-request-relay.test.ts b/src/main/window/terminal-pane-move-request-relay.test.ts index cd5c63de623..fdbe0c5fb6c 100644 --- a/src/main/window/terminal-pane-move-request-relay.test.ts +++ b/src/main/window/terminal-pane-move-request-relay.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:events' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalLeafMoveResult } from '../../shared/workspace-layout/terminal-leaf-move' +import type { TerminalLeafMoveResult } from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' const ipcEmitter = new EventEmitter() const ipcMainMock = { @@ -49,15 +49,6 @@ async function requestMove() { return { relay, pending, sender, webContents, reply } } -function deferredCommit() { - let finish: (result: TerminalLeafMoveResult) => void = () => {} - const commit = (): Promise => - new Promise((resolve) => { - finish = resolve - }) - return { commit, finish: (result: TerminalLeafMoveResult) => finish(result) } -} - describe('requestTerminalPaneMoveFromRenderer', () => { beforeEach(() => { vi.resetModules() @@ -82,26 +73,4 @@ describe('requestTerminalPaneMoveFromRenderer', () => { reply({ status: 'refused', reason: 'pane_unavailable' }) expect(ipcEmitter.listenerCount('ui:terminalPaneMoveToNewTabReply')).toBe(0) }) - - it('answers moved once the window commits, even when its reply comes after the deadline', async () => { - const { relay, pending, sender } = await requestMove() - const { commit, finish } = deferredCommit() - const committing = relay.commitRelayedPaneMove(sender, move.targetTabId, commit) - await vi.advanceTimersByTimeAsync(10_000) - finish({ status: 'moved', ptyId: 'pty-1' }) - - await expect(committing).resolves.toEqual({ status: 'moved', ptyId: 'pty-1' }) - await expect(pending).resolves.toEqual({ status: 'moved', ptyId: 'pty-1' }) - }) - - it('answers with the commit when the window goes away while it commits', async () => { - const { relay, pending, sender, webContents } = await requestMove() - const { commit, finish } = deferredCommit() - const committing = relay.commitRelayedPaneMove(sender, move.targetTabId, commit) - webContents.emit('render-process-gone') - finish({ status: 'moved', ptyId: 'pty-1' }) - - await expect(committing).resolves.toEqual({ status: 'moved', ptyId: 'pty-1' }) - await expect(pending).resolves.toEqual({ status: 'moved', ptyId: 'pty-1' }) - }) }) diff --git a/src/main/window/terminal-pane-move-request-relay.ts b/src/main/window/terminal-pane-move-request-relay.ts index b43c5fa3145..f7242f2832c 100644 --- a/src/main/window/terminal-pane-move-request-relay.ts +++ b/src/main/window/terminal-pane-move-request-relay.ts @@ -6,7 +6,7 @@ import type { TerminalLeafMoveResult, TerminalPaneMoveToNewTabReply, TerminalPaneMoveToNewTabRequest -} from '../../shared/workspace-layout/terminal-leaf-move' +} from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' import { addFanoutListener } from '../runtime/events/event-listener-fanout' import { abortWhenRendererGone } from '../ipc/renderer-lifetime-abort' import { holdRelayedPaneMove, type RelayedPaneMove } from './terminal-pane-move-commit-fence' diff --git a/src/main/window/terminal-pane-reveal-relay.test.ts b/src/main/window/terminal-pane-reveal-relay.test.ts deleted file mode 100644 index 1bf27204fdf..00000000000 --- a/src/main/window/terminal-pane-reveal-relay.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { EventEmitter } from 'node:events' -import type { BrowserWindow } from 'electron' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { - TerminalPaneRevealRequest, - TerminalPaneRevealTarget -} from '../../shared/workspace-layout/terminal-pane-reveal' -import type { FocusExistingMainWindowOptions } from './focus-existing-window' - -const ipcEmitter = new EventEmitter() -const mocks = vi.hoisted(() => ({ - focusExistingMainWindow: vi.fn<(options: FocusExistingMainWindowOptions) => string>() -})) -const ipcMainMock = { - on: vi.fn((channel: string, listener: (...args: unknown[]) => void) => { - ipcEmitter.on(channel, listener) - }), - removeListener: vi.fn((channel: string, listener: (...args: unknown[]) => void) => { - ipcEmitter.removeListener(channel, listener) - }) -} - -vi.mock('electron', () => ({ app: {}, ipcMain: ipcMainMock })) -vi.mock('./focus-existing-window', () => ({ - focusExistingMainWindow: mocks.focusExistingMainWindow -})) - -const target: TerminalPaneRevealTarget = { - worktreeId: 'repo-1::/srv/app', - tabId: 'tab-1', - leafId: '11111111-1111-4111-8111-111111111111', - executionHostId: 'runtime:env-1' -} - -function createWindow(options: { rendererDestroyed?: boolean } = {}) { - const webContents = Object.assign(new EventEmitter(), { - isDestroyed: () => options.rendererDestroyed === true, - send: vi.fn<(channel: string, request: TerminalPaneRevealRequest) => void>() - }) - const windowEvents = Object.assign(new EventEmitter(), { - isDestroyed: () => false, - webContents - }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this fixture supplies the relay's window/contents event, destruction and send surface. - const mainWindow = windowEvents as unknown as BrowserWindow - const sentRequest = (): TerminalPaneRevealRequest => { - const [channel, request] = webContents.send.mock.calls[0] ?? [] - expect(channel).toBe('ui:revealTerminalPane') - return request ?? { ...target, requestId: '' } - } - return { webContents, mainWindow, windowEvents, sentRequest } -} - -async function loadRelay() { - return (await import('./terminal-pane-reveal-relay')).revealTerminalPaneInWindow -} - -function reply(sender: unknown, requestId: string, revealed: boolean): void { - ipcEmitter.emit('ui:terminalPaneRevealReply', { sender }, { requestId, revealed }) -} - -describe('revealTerminalPaneInWindow', () => { - beforeEach(() => { - ipcEmitter.removeAllListeners() - mocks.focusExistingMainWindow.mockReset() - }) - - afterEach(() => { - vi.clearAllTimers() - vi.useRealTimers() - }) - - it('raises the window once the targeted renderer has revealed the pane', async () => { - const revealTerminalPaneInWindow = await loadRelay() - const { webContents, mainWindow, sentRequest } = createWindow() - const pending = revealTerminalPaneInWindow(mainWindow, target) - const { requestId } = sentRequest() - expect(sentRequest()).toEqual({ ...target, requestId }) - - // Visible request IDs confer no authority; only the targeted window can answer. - reply({}, requestId, false) - reply(webContents, 'other-request', false) - reply(webContents, requestId, true) - - await expect(pending).resolves.toBe(true) - expect(mocks.focusExistingMainWindow).toHaveBeenCalledTimes(1) - const focus = mocks.focusExistingMainWindow.mock.calls[0]?.[0] - expect(focus?.getWindow()).toBe(mainWindow) - expect(focus?.canOpenWindow?.()).toBe(false) - expect(ipcEmitter.listenerCount('ui:terminalPaneRevealReply')).toBe(0) - }) -}) diff --git a/src/main/window/terminal-pane-reveal-relay.ts b/src/main/window/terminal-pane-reveal-relay.ts index 837cc459556..3c652fc04f6 100644 --- a/src/main/window/terminal-pane-reveal-relay.ts +++ b/src/main/window/terminal-pane-reveal-relay.ts @@ -3,7 +3,7 @@ import { app, type BrowserWindow } from 'electron' import type { TerminalPaneRevealReply, TerminalPaneRevealTarget -} from '../../shared/workspace-layout/terminal-pane-reveal' +} from '../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal' import { focusExistingMainWindow } from './focus-existing-window' import { requestRendererReply } from './renderer-reply-request' diff --git a/src/main/window/terminal-tab-close-request-relay.ts b/src/main/window/terminal-tab-close-request-relay.ts index 734da561101..5afd1db16c5 100644 --- a/src/main/window/terminal-tab-close-request-relay.ts +++ b/src/main/window/terminal-tab-close-request-relay.ts @@ -5,7 +5,7 @@ import { ipcMain, type BrowserWindow } from 'electron' import type { TerminalTabCloseRequest, TerminalTabCloseResponse -} from '../../shared/workspace-layout/terminal-tab-close' +} from '../../shared/workspace-layout/terminal-surfaces/terminal-tab-close' import { addFanoutListener, addFanoutListeners } from '../runtime/events/event-listener-fanout' const TERMINAL_TAB_CLOSE_TIMEOUT_MS = 20_000 diff --git a/src/main/workspaces/removal/worktree-removal-close-records.test.ts b/src/main/workspaces/removal/worktree-removal-close-records.test.ts index 22c07c01fc6..6146832e6e9 100644 --- a/src/main/workspaces/removal/worktree-removal-close-records.test.ts +++ b/src/main/workspaces/removal/worktree-removal-close-records.test.ts @@ -13,7 +13,7 @@ import { tmpdir } from 'node:os' import { getDefaultWorkspaceSession } from '../../../shared/constants' import { toSshExecutionHostId } from '../../../shared/execution-host' import { folderWorkspaceKey } from '../../../shared/workspace-scope' -import type { ClosedTerminalTabTombstonesByTabId } from '../../../shared/workspace-layout/closed-terminal-tab-tombstones' +import type { ClosedTerminalTabTombstonesByTabId } from '../../../shared/workspace-layout/terminal-surfaces/closed-terminal-tab-tombstones' import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' diff --git a/src/preload/api/pty-api.ts b/src/preload/api/pty-api.ts index 51d56a86038..84f850521ab 100644 --- a/src/preload/api/pty-api.ts +++ b/src/preload/api/pty-api.ts @@ -17,7 +17,7 @@ import type { TerminalInputKind } from '../../shared/terminal/input/terminal-inp import type { TerminalLeafMoveRequest, TerminalLeafMoveResult -} from '../../shared/workspace-layout/terminal-leaf-move' +} from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' import type { ProjectExecutionRuntimeResolution } from '../../shared/execution-host/project-execution-runtime' import type { PtyListedSession, @@ -34,7 +34,7 @@ import type { AgentKind, LaunchSource, RequestKind } from '../../shared/telemetr import type { TerminalSideEffectBatch } from '../../shared/terminal/output/terminal-side-effect-facts' import type { TerminalViewAttributes } from '../../shared/terminal/protocol/queries/terminal-view-attributes' import type { TuiAgent } from '../../shared/agent-launch/catalog/tui-agent' -import type { TerminalPanePlacement } from '../../shared/workspace-layout/terminal-pane-placement' +import type { TerminalPanePlacement } from '../../shared/workspace-layout/terminal-topology/terminal-pane-placement' import type { AgentPaneLaunchRequest } from '../../shared/agent-launch/contracts/agent-pane-launch-request' import type { PtyManagementApi } from './pty-management-api' import type { TerminalProcessInspection } from '../../shared/terminal/pty/lifecycle/terminal-process-inspection' diff --git a/src/preload/api/pty-bridge-session-control.ts b/src/preload/api/pty-bridge-session-control.ts index fc6066b6d48..66a25ed8679 100644 --- a/src/preload/api/pty-bridge-session-control.ts +++ b/src/preload/api/pty-bridge-session-control.ts @@ -13,7 +13,7 @@ import type { TerminalInputKind } from '../../shared/terminal/input/terminal-inp import type { TerminalLeafMoveRequest, TerminalLeafMoveResult -} from '../../shared/workspace-layout/terminal-leaf-move' +} from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' import type { SavedResumeDecision, TerminalLaunchResult, @@ -33,7 +33,7 @@ import type { TerminalViewAttributes } from '../../shared/terminal/protocol/quer import type { PtyMainDeliveryDiagnostics } from '../../shared/terminal/pty/diagnostics/pty-delivery-diagnostics' import type { AgentKind, LaunchSource, RequestKind } from '../../shared/telemetry-events' import type { PreloadApi } from '../api-types' -import type { TerminalPanePlacement } from '../../shared/workspace-layout/terminal-pane-placement' +import type { TerminalPanePlacement } from '../../shared/workspace-layout/terminal-topology/terminal-pane-placement' import type { AgentPaneLaunchRequest } from '../../shared/agent-launch/contracts/agent-pane-launch-request' export const ptySessionControlApi = { diff --git a/src/preload/api/ui-bridge-clipboard-and-window-controls.ts b/src/preload/api/ui-bridge-clipboard-and-window-controls.ts index 1aa0df2b6ab..d3cfacc2098 100644 --- a/src/preload/api/ui-bridge-clipboard-and-window-controls.ts +++ b/src/preload/api/ui-bridge-clipboard-and-window-controls.ts @@ -8,7 +8,7 @@ import { type RichMarkdownContextMenuTableTarget } from '../../shared/rich-markdown-context-menu' import type { RuntimeNavigationTarget } from '../../shared/runtime-navigation' -import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surface-close-target' +import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target' import type { ClipboardImageThumbnail } from '../../shared/clipboard-image' import type { ReadClipboardTextOptions } from '../../shared/clipboard-text' import type { diff --git a/src/preload/api/ui-bridge-terminal-and-session-tabs.ts b/src/preload/api/ui-bridge-terminal-and-session-tabs.ts index c324db9e5fa..56c17ce2f56 100644 --- a/src/preload/api/ui-bridge-terminal-and-session-tabs.ts +++ b/src/preload/api/ui-bridge-terminal-and-session-tabs.ts @@ -4,7 +4,7 @@ import type { } from '../../shared/file-access/runtime-editor-file-open' import { ipcRenderer } from 'electron' import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry' -import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-reveal-identity' +import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-surfaces/terminal-reveal-identity' import type { AgentLaunchTabPublishReply, AgentLaunchTabViewerRule, @@ -15,7 +15,7 @@ import type { PaneLaunchRecipe } from '../../shared/terminal/pty/lifecycle/termi import type { TerminalPaneRevealReply, TerminalPaneRevealRequest -} from '../../shared/workspace-layout/terminal-pane-reveal' +} from '../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal' import type { AgentProviderSessionMetadata, SleepingAgentLaunchConfig diff --git a/src/preload/api/ui-command-event-api.ts b/src/preload/api/ui-command-event-api.ts index f9895cec986..66c5e346f88 100644 --- a/src/preload/api/ui-command-event-api.ts +++ b/src/preload/api/ui-command-event-api.ts @@ -21,11 +21,11 @@ import type { PaneLaunchRecipe } from '../../shared/terminal/pty/lifecycle/termi import type { TerminalPaneMoveToNewTabReply, TerminalPaneMoveToNewTabRequest -} from '../../shared/workspace-layout/terminal-leaf-move' +} from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' import type { TerminalPaneRevealReply, TerminalPaneRevealRequest -} from '../../shared/workspace-layout/terminal-pane-reveal' +} from '../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal' import type { AgentPaneActivityReply, AgentPaneActivityRequest @@ -35,7 +35,7 @@ import type { ProjectMovePauseRequest, ProjectMovePauseResponse } from '../../shared/project-move-pause' -import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surface-close-target' +import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target' import type { WorktreeDefaultTabsLaunch, WorktreeSetupLaunch, @@ -60,11 +60,11 @@ import type { RuntimeTerminalCreateRequestPayload, RuntimeTerminalPresentation } from '../../shared/runtime-types' -import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-reveal-identity' +import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-surfaces/terminal-reveal-identity' import type { TerminalTabCloseRequest, TerminalTabCloseResponse -} from '../../shared/workspace-layout/terminal-tab-close' +} from '../../shared/workspace-layout/terminal-surfaces/terminal-tab-close' import type { SessionTabCloseRequest, SessionTabCloseResponse diff --git a/src/preload/api/workspace-session-api.ts b/src/preload/api/workspace-session-api.ts index ccd70c8004b..34fb8f6019a 100644 --- a/src/preload/api/workspace-session-api.ts +++ b/src/preload/api/workspace-session-api.ts @@ -4,7 +4,7 @@ import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types' import type { ExecutionHostId } from '../../shared/execution-host' -import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surface-close-target' +import type { TerminalSurfaceCloseTarget } from '../../shared/workspace-layout/terminal-surfaces/terminal-surface-close-target' export type WorkspaceSessionApi = { session: { diff --git a/src/renderer/src/app-shell/startup-window-parity-differences.test-fixture.ts b/src/renderer/src/app-shell/startup-window-parity-differences.test-fixture.ts deleted file mode 100644 index 63d431f1588..00000000000 --- a/src/renderer/src/app-shell/startup-window-parity-differences.test-fixture.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Shared expected differences between disk restore and Loader-rewritten startup views, each attributed to its Loader rule. - -import { parseExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' -import { sweepUnattachedTerminalRows } from '../../../shared/workspace-layout/unattached-terminal-row-sweep' -import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types' - -export const GIT = 'repo-1::/Users/dev/orca' - -/** Main's startup sweep in a layout-owner build; a paired server's partition passes through. */ -export function sweptAtStartup( - hostId: ExecutionHostId, - session: WorkspaceSessionState -): WorkspaceSessionState { - return parseExecutionHostId(hostId)?.kind === 'runtime' - ? session - : sweepUnattachedTerminalRows(session) -} - -export type KnownDifference = { - fixture: string - /** The Loader rule that makes the window show something else. */ - rule: string - /** View paths that differ; see `projectStartupWindowView`. */ - paths: string[] -} - -/** Differences to fix. Entries may only be removed: fix the rule, then delete its entry. */ -export const KNOWN_DIFFERENCES: readonly KnownDifference[] = [] - -export type AcceptedDifference = Omit & { - reason: string - /** Each differing view path with its stored and normalized values; absent means not shown. */ - values: Record -} - -/** Differences that are not bugs: the user sees the same thing, or the window is the outlier. */ -export const ACCEPTED_DIFFERENCES: readonly AcceptedDifference[] = [ - { - fixture: 'row terminal differs from its focused pane’s', - rule: 'row.ptyId rewrite: the Loader sets the row terminal to its focused pane’s, which changes what the tab reattaches', - reason: 'the pane’s own terminal binding wins on mount, so the user sees the same terminal', - values: { [`${GIT} terminal tab-a reattaches`]: { stored: 'pty-stale', normalized: 'pty-a' } } - }, - { - fixture: 'row with no terminal while its pane has one', - rule: 'row.ptyId rewrite: the Loader sets the row terminal to its focused pane’s, which changes what the tab reattaches', - reason: 'the pane’s own terminal binding wins on mount, so the user sees the same terminal', - values: { [`${GIT} terminal tab-a reattaches`]: { stored: null, normalized: 'pty-a' } } - }, - { - fixture: 'legacy surface tombstone on a pane still showing its terminal', - rule: 'tombstones: the Loader closes a pane a legacy surface tombstone names; the window ignores the tombstone and shows the pane', - reason: 'the window is the outlier: main already retires panes these tombstones name', - values: { - [`${GIT} group 0 tabs`]: { stored: ['tab-a', 'tab-b'], normalized: ['tab-a'] }, - [`${GIT} tab tab-b`]: { - stored: { kind: 'terminal', shows: 'tab-b', group: 0, label: 'Terminal 2' } - }, - [`${GIT} terminal tab-b phone and CLI title`]: { stored: 'Terminal 2' }, - [`${GIT} terminal tab-b panes`]: { stored: { pane: 'pty-b' } }, - [`${GIT} terminal tab-b focused pane`]: { stored: 'root' }, - [`${GIT} terminal tab-b reattaches`]: { stored: 'pty-b' } - } - }, - { - fixture: 'tab-bar label differing from the row title', - rule: 'label over title: the Loader writes the tab-bar label into the row title', - reason: - 'the desktop tab shows the label, which is unchanged; only phones and the CLI show the row title', - values: { - [`${GIT} terminal tab-a phone and CLI title`]: { stored: 'Terminal 1', normalized: 'vim' } - } - } -] diff --git a/src/renderer/src/app-shell/startup-window-parity.test.ts b/src/renderer/src/app-shell/startup-window-parity.test.ts deleted file mode 100644 index ad55364b52d..00000000000 --- a/src/renderer/src/app-shell/startup-window-parity.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Compare window startup from disk with the Loader round trip. Enumerate each divergence as a known defect or accepted difference with its reason. - -import { describe, expect, it, vi } from 'vitest' -import type * as AgentStatusModule from '@/lib/agent-status/aggregation/worktree-agent-status' -import type { ExecutionHostId } from '../../../shared/execution-host' -import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types' -import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-legacy-profile.test-fixture' -import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load' -import { WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-profile.test-fixture' -import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save' -import { createStoreSessionMockApi } from '../store/testing/app-store-session-api.test-fixture' -import { createTestStore } from '../store/testing/app-store-test-fixture' -import { GIT, sweptAtStartup } from './startup-window-parity-differences.test-fixture' -import { - bootWindowFromPartitions, - projectStartupWindowView -} from './startup-window-view.test-fixture' - -// Use legacy window boot as the oracle for main-owned layout loading. -const layoutOwner = vi.hoisted(() => ({ isMain: false })) -vi.mock('../../../shared/layout-owner-switch', () => ({ - get ORCA_LAYOUT_OWNER_IS_MAIN() { - return layoutOwner.isMain - } -})) -vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) -vi.mock('@/lib/agent-status/aggregation/worktree-agent-status', async (importOriginal) => { - const actual = await importOriginal() - return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) } -}) - -createStoreSessionMockApi() - -/** Main's startup sweep, then the shared Loader's round trip, as main would persist it. */ -function normalize(hostId: ExecutionHostId, session: WorkspaceSessionState): WorkspaceSessionState { - let next = 0 - layoutOwner.isMain = true - try { - const loaded = loadWorkspaceLayout(hostId, sweptAtStartup(hostId, structuredClone(session)), { - mintId: () => `minted-${++next}`, - mintLeafId: () => `00000000-0000-4000-9000-${String(++next).padStart(12, '0')}` - }) - return saveWorkspaceLayout(loaded) - } finally { - layoutOwner.isMain = false - } -} - -const PROFILES = [...WORKSPACE_LAYOUT_PROFILES, ...LEGACY_WORKSPACE_LAYOUT_PROFILES] - -describe('the window shows the same session before and after the shared Loader rewrites it', () => { - it('keeps the tab-bar twin selected when a selected legacy copy is dropped', async () => { - const profile = PROFILES.find( - (entry) => entry.name === 'legacy row repeating a tab-bar tab’s terminal, selected' - )! - const view = projectStartupWindowView( - await bootWindowFromPartitions(createTestStore(), [ - { hostId: profile.hostId, session: normalize(profile.hostId, profile.build()) } - ]) - ) - expect(view['active.terminal']).toBe('tab-a') - expect(view[`${GIT} active terminal`]).toBe('tab-a') - }) - - it('restores terminals and saved edit tabs from a session before the tab bar', async () => { - const profile = PROFILES.find( - (entry) => entry.name === 'session from before the tab bar, with an open file' - )! - const view = projectStartupWindowView( - await bootWindowFromPartitions(createTestStore(), [ - { hostId: profile.hostId, session: profile.build() } - ]) - ) - expect(view[`${GIT} group 0 tabs`]).toEqual(['/Users/dev/orca/src/app.ts', 'tab-a', 'tab-b']) - expect(view).toHaveProperty([`${GIT} tab /Users/dev/orca/src/app.ts`]) - }) -}) diff --git a/src/renderer/src/app-shell/startup-window-view.test-fixture.ts b/src/renderer/src/app-shell/startup-window-view.test-fixture.ts deleted file mode 100644 index bfdef958c4c..00000000000 --- a/src/renderer/src/app-shell/startup-window-view.test-fixture.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Boot stored partitions through real startup hydration, then compare user-visible state. - -import { vi } from 'vitest' -import { getDefaultWorkspaceSession } from '../../../shared/constants' -import { - LOCAL_EXECUTION_HOST_ID, - parseExecutionHostId, - type ExecutionHostId -} from '../../../shared/execution-host' -import type { TabGroupLayoutNode } from '../../../shared/tab-types' -import type { - TerminalLayoutSnapshot, - TerminalPaneLayoutNode -} 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 { fetchWorkspaceSessionWithRuntimeHostOwners } from '@/lib/workspace/persistence/workspace-session-host-hydration' -import { collectFolderWorkspaceKeysFromSession } from '@/lib/workspace/persistence/workspace-session-hydration-keys' -import type { AppState } from '../store/types' -import type { createTestStore } from '../store/testing/app-store-test-fixture' -import { reconcileHydratedWorkspaceTabModels } from './reconcile-hydrated-workspace-tab-models' - -/** One stored session partition and the host it is stored under. */ -export type StoredPartition = { hostId: ExecutionHostId; session: WorkspaceSessionState } - -/** First-paint local repos for referenced Git workspaces, without listings. Remote-server catalogs are skipped at startup. */ -function firstPaintRepos(partitions: readonly StoredPartition[]): AppState['repos'] { - const repos = new Map() - for (const { hostId, session } of partitions) { - const host = parseExecutionHostId(hostId) - if (host?.kind === 'runtime') { - continue - } - for (const key of Object.keys(session.tabsByWorktree)) { - const id = getRepoIdFromWorktreeId(key) - if (!key.startsWith(`${id}::`) || repos.has(id)) { - continue - } - repos.set(id, { - id, - path: key.slice(id.length + 2), - displayName: id, - badgeColor: '#000', - addedAt: 0, - connectionId: host?.kind === 'ssh' ? host.targetId : null, - executionHostId: hostId - }) - } - } - return [...repos.values()] -} - -/** Match use-app-startup-hydration’s call order on a fresh test store. */ -export async function bootWindowFromPartitions( - store: ReturnType, - partitions: readonly StoredPartition[] -): Promise { - let minted = 0 - // Group and tab ids the window mints must match across the two boots being compared. - const randomUUID = vi - .spyOn(globalThis.crypto, 'randomUUID') - .mockImplementation(() => `00000000-0000-4000-8000-${String(++minted).padStart(12, '0')}`) - try { - store.setState({ repos: firstPaintRepos(partitions) }) - const read = async (hostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID) => - structuredClone( - partitions.find((partition) => partition.hostId === hostId)?.session ?? - getDefaultWorkspaceSession() - ) - const hostIdsOfKind = (kind: 'ssh' | 'runtime') => - partitions - .map((partition) => partition.hostId) - .filter((hostId) => parseExecutionHostId(hostId)?.kind === kind) - const sessionRead = await fetchWorkspaceSessionWithRuntimeHostOwners( - { get: read, listHostIds: async () => hostIdsOfKind('ssh') }, - store.getState().repos, - hostIdsOfKind('runtime') - ) - const actions = store.getState() - const options = { - additionalValidWorkspaceKeys: collectFolderWorkspaceKeysFromSession(sessionRead.session) - } - actions.hydrateWorkspaceSession(sessionRead.session, { - ...options, - runtimeHostIdByWorkspaceSessionKey: sessionRead.runtimeHostIdByWorkspaceSessionKey, - contestedHostWorkspaceSessions: sessionRead.contestedHostWorkspaceSessions, - contestedPrimaryHostBySessionKey: sessionRead.contestedPrimaryHostBySessionKey, - hydratedRuntimeSessionHostIds: sessionRead.hydratedRuntimeSessionHostIds - }) - actions.hydrateTabsSession(sessionRead.session, options) - actions.hydrateEditorSession(sessionRead.session, options) - actions.hydrateBrowserSession(sessionRead.session, options) - reconcileHydratedWorkspaceTabModels( - sessionRead.session, - store.getState().reconcileWorktreeTabModels - ) - return store.getState() - } finally { - randomUUID.mockRestore() - } -} - -type PaneView = { pane: string | null } | { split: string; first: PaneView; second: PaneView } - -/** Panes by position, each naming the terminal it binds; leaf ids are not shown. */ -function paneView(node: TerminalPaneLayoutNode, layout: TerminalLayoutSnapshot): PaneView { - if (node.type === 'leaf') { - return { pane: layout.ptyIdsByLeafId?.[node.leafId] ?? null } - } - return { - split: node.direction, - first: paneView(node.first, layout), - second: paneView(node.second, layout) - } -} - -function leafPosition( - node: TerminalPaneLayoutNode, - leafId: string | null, - path = '' -): string | null { - if (node.type === 'leaf') { - return node.leafId === leafId ? path || 'root' : null - } - return ( - leafPosition(node.first, leafId, `${path}1`) ?? leafPosition(node.second, leafId, `${path}2`) - ) -} - -type GroupLayoutView = number | { split: string; first: GroupLayoutView; second: GroupLayoutView } - -function groupLayoutView(node: TabGroupLayoutNode, groupIds: string[]): GroupLayoutView { - if (node.type === 'leaf') { - return groupIds.indexOf(node.groupId) - } - return { - split: node.direction, - first: groupLayoutView(node.first, groupIds), - second: groupLayoutView(node.second, groupIds) - } -} - -/** Project visible path/value pairs. Name groups by position (IDs are invisible); retain tab IDs because they must survive restarts. */ -export function projectStartupWindowView(state: AppState): Record { - const view: Record = { - 'active.workspace': state.activeWorktreeId, - 'active.terminal': state.activeTabId - } - const workspaceKeys = new Set([ - ...Object.keys(state.tabsByWorktree), - ...Object.keys(state.unifiedTabsByWorktree), - ...state.openFiles.map((file) => file.worktreeId), - ...Object.keys(state.browserTabsByWorktree) - ]) - for (const key of [...workspaceKeys].sort()) { - const groups = state.groupsByWorktree[key] ?? [] - const groupIds = groups.map((group) => group.id) - const layout = state.layoutByWorktree[key] - view[`${key} groups`] = layout ? groupLayoutView(layout, groupIds) : groupIds.length - groups.forEach((group, index) => { - view[`${key} group ${index} tabs`] = group.tabOrder - view[`${key} group ${index} active tab`] = group.activeTabId - }) - view[`${key} active group`] = groupIds.indexOf(state.activeGroupIdByWorktree[key] ?? '') - view[`${key} active terminal`] = state.activeTabIdByWorktree[key] ?? null - view[`${key} active file`] = state.activeFileIdByWorktree[key] ?? null - view[`${key} active kind`] = state.activeTabTypeByWorktree[key] ?? null - for (const tab of state.unifiedTabsByWorktree[key] ?? []) { - view[`${key} tab ${tab.id}`] = { - kind: tab.contentType, - shows: tab.entityId, - group: groupIds.indexOf(tab.groupId), - label: tab.customLabel ?? tab.label - } - } - for (const row of state.tabsByWorktree[key] ?? []) { - const panes = state.terminalLayoutsByTabId[row.id] - const terminal = `${key} terminal ${row.id}` - // Phones and the CLI show the row title; the desktop tab shows its tab-bar label. - view[`${terminal} phone and CLI title`] = row.customTitle ?? row.title - view[`${terminal} panes`] = panes?.root ? paneView(panes.root, panes) : null - view[`${terminal} focused pane`] = panes?.root - ? leafPosition(panes.root, panes.activeLeafId) - : null - view[`${terminal} reattaches`] = state.pendingReconnectPtyIdByTabId[row.id] ?? null - } - view[`${key} open files`] = state.openFiles - .filter((file) => file.worktreeId === key) - .map((file) => file.filePath) - view[`${key} browser tabs`] = (state.browserTabsByWorktree[key] ?? []).map((tab) => tab.url) - } - return view -} - -/** Each path whose value differs between two views, with both values. */ -export function diffStartupWindowViews( - stored: Record, - normalized: Record -): { path: string; stored: unknown; normalized: unknown }[] { - const paths = [...new Set([...Object.keys(stored), ...Object.keys(normalized)])].sort() - return paths - .filter((path) => JSON.stringify(stored[path]) !== JSON.stringify(normalized[path])) - .map((path) => ({ path, stored: stored[path], normalized: normalized[path] })) -} diff --git a/src/renderer/src/components/native-chat/messages/actions/native-chat-side-chat-placement.ts b/src/renderer/src/components/native-chat/messages/actions/native-chat-side-chat-placement.ts index 9c7a91c8645..80f20a381e0 100644 --- a/src/renderer/src/components/native-chat/messages/actions/native-chat-side-chat-placement.ts +++ b/src/renderer/src/components/native-chat/messages/actions/native-chat-side-chat-placement.ts @@ -1,6 +1,6 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' -import { findSiblingGroupId } from '../../../../../../shared/workspace-layout/tab-group-layout-tree' +import { findSiblingGroupId } from '../../../../../../shared/workspace-layout/tab-groups/tab-group-layout-tree' import { findStructuredLaunchTab } from '@/lib/agent-session/launch/structured-agent-session-launch-surface' import { afterPendingTab } from '@/runtime/workspace-layout/intents/layout-owner-pending-tabs' import { requestNativeChatComposerFocus } from '../../composer/focus/native-chat-composer-focus-intent' diff --git a/src/renderer/src/components/tab-bar/cli-chat-resume-close-terminal.ts b/src/renderer/src/components/tab-bar/cli-chat-resume-close-terminal.ts index 035dd5aaaf1..eb3f40f38f7 100644 --- a/src/renderer/src/components/tab-bar/cli-chat-resume-close-terminal.ts +++ b/src/renderer/src/components/tab-bar/cli-chat-resume-close-terminal.ts @@ -8,7 +8,7 @@ import { toHostSessionPaneKey } from '../../../../shared/workspace-layout/termin import { captureTerminalSurfaceIdentity, isRetainedTerminalSurfaceIdentity -} from '../../../../shared/workspace-layout/terminal-tab-close' +} from '../../../../shared/workspace-layout/terminal-surfaces/terminal-tab-close' import type { RuntimeTerminalResolvePane, RuntimeTerminalWait diff --git a/src/renderer/src/components/tab-bar/tab-context-menu.test.tsx b/src/renderer/src/components/tab-bar/tab-context-menu.test.tsx deleted file mode 100644 index c7cd1dd1609..00000000000 --- a/src/renderer/src/components/tab-bar/tab-context-menu.test.tsx +++ /dev/null @@ -1,429 +0,0 @@ -// @vitest-environment happy-dom -import { useRef } from 'react' -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { DndContext, useSensor, useSensors } from '@dnd-kit/core' -import { SortableContext } from '@dnd-kit/sortable' -import { afterEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import { TooltipProvider } from '../ui/tooltip' -import BrowserTab from './BrowserTab' -import EditorFileTab from './EditorFileTab' -import SortableTab from './SortableTab' -import { TabDragPointerSensor } from '../tab-group/tab-drag-pointer-sensor' -import { TAB_DRAG_ACTIVATION_DISTANCE_PX } from '../tab-group/useTabDragSplit' -import { createStoreSessionMockApi } from '../../store/testing/app-store-session-api.test-fixture' -import { bootWindowFromPartitions } from '../../app-shell/startup-window-view.test-fixture' -import { - addWorkspace, - emptySession, - GIT_KEY, - FOLDER_KEY, - SSH_KEY, - leaf -} from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture' -import { layoutApplyTestTransport } from '@/runtime/workspace-layout/testing/layout-apply-test-transport.test-fixture' -import { loadMainLayoutModel } from '@/runtime/workspace-layout/testing/main-loaded-layout-frames.test-fixture' -import { endLayoutFramePositions } from '@/runtime/workspace-layout/frames/layout-frame-position' -import { openWorkspaceLayoutFrameSync } from '@/runtime/workspace-layout/frames/workspace-layout-frame-sync' -import { isTabStripPointerGestureActive } from './tab-strip-pointer-gesture' - -vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) - -vi.mock('@/runtime/rpc/calls/runtime-rpc-call', async (original) => - ( - await import('@/runtime/workspace-layout/testing/layout-apply-test-transport.test-fixture') - ).runtimeRpcClientWithLayoutApply(await original()) -) -vi.mock('@/runtime/workspace-layout/frames/runtime-layout-client', async (original) => - ( - await import('@/runtime/workspace-layout/testing/layout-apply-test-transport.test-fixture') - ).runtimeLayoutClientWithTestFrames(await original()) -) -vi.mock('@/runtime/workspace-layout/frames/workspace-layout-frame-sync', async (original) => ({ - ...(await original()), - showStartupLayoutFrames: () => {} -})) - -const domWindow = window -const TAB = 'source-unified' -const OTHER = 'other-unified' -const ENTITY = '/repo/source.ts' -const GROUP = 'original-group' -type Kind = 'browser' | 'editor' | 'terminal' -let worktree = GIT_KEY -let stopFrames = () => {} -let drag: string[] = [] -let activations = 0 -const browser = { - id: ENTITY, - worktreeId: GIT_KEY, - label: 'Source', - sessionProfileId: null, - activePageId: 'source-page', - pageIds: ['source-page'], - url: 'https://example.com', - title: 'Source', - loading: false, - faviconUrl: null, - canGoBack: false, - canGoForward: false, - loadError: null, - createdAt: 1 -} - -function Harness({ kind, singleton }: { kind: Kind; singleton: boolean }) { - const pane = useRef(null) - const sensors = useSensors( - useSensor(TabDragPointerSensor, { - activationConstraint: { distance: TAB_DRAG_ACTIVATION_DISTANCE_PX } - }) - ) - const groups = useAppStore((state) => state.groupsByWorktree[worktree]) - const layout = useAppStore((state) => state.layoutByWorktree[worktree]) - const pinned = useAppStore( - (state) => state.unifiedTabsByWorktree[worktree]?.find((tab) => tab.id === TAB)?.isPinned - ) - const common = { - isActive: false, - isPinned: pinned === true, - hasTabsToLeft: false, - hasTabsToRight: !singleton, - tabCount: singleton ? 1 : 2, - onActivate: () => { - activations += 1 - pane.current?.focus() - }, - onClose: () => useAppStore.getState().closeUnifiedTab(TAB), - onCloseOthers: () => {}, - onCloseToLeft: () => {}, - onCloseToRight: () => {}, - onTogglePin: () => - (pinned ? useAppStore.getState().unpinTab : useAppStore.getState().pinTab)(TAB), - dragData: { - kind: 'tab' as const, - worktreeId: worktree, - groupId: GROUP, - unifiedTabId: TAB, - visibleTabId: ENTITY, - tabType: kind, - label: 'Source' - } - } - return ( - -