Skip to content

Main closes, renames, sleeps and starts new tabs' terminals itself, with or without a window (dark, layout-owner build only) - #31571

Merged
Jinwoo-H merged 21 commits into
mainfrom
fd/d1b2-d2-main-starts
Oct 21, 2026
Merged

Jinwoo-H merged 21 commits into
mainfrom
fd/d1b2-d2-main-starts

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2390 $\color{#cf222e}{\Huge{\mathbf{−}}}$​726 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1664
Prod 29 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1342 $\color{#cf222e}{\Huge{\mathbf{−}}}$​197 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1145

ELI5

Orca has a dark build switch that hands ownership of the terminal layout (tabs, panes, their order and names) from the window to the app's background process. This PR moves several more terminal actions to that background process, so they work the same with or without a desktop window open:

  • closing a terminal from a phone or the CLI;
  • renaming a tab or a pane;
  • putting a workspace to sleep;
  • the tabs that agent launches open;
  • starting a plain new tab's first terminal.

Normal builds (switch off) behave exactly as today.

What Changed

All changes are in switch-on builds only (ORCA_LAYOUT_OWNER=main). Relative to a switch-off build, nothing a user sees changes.

  • Phone and CLI closes, renames, pane titles, sleep, agent-launch tabs. Main now applies these to its own layout itself, instead of asking the window to do them and waiting for the window to report back. They now work on a host with no window open.
    • Sleep: switch-on builds call the existing capture, stop and persist step for workspaces main owns. The resume record format is unchanged and stays with its current owner.
    • An SSH sleep goes to the host that owns the workspace.
  • A plain local new tab's first terminal is started by main. The window draws the tab, then asks main to start the terminal under the window's own tab and pane ids (terminal.create with the new optional windowTab and cwd fields). The pane waits, then attaches to the terminal main started.
    • Same folder, size, group and focus as before.
    • If the start fails, the pane says "The terminal could not start: ". A start that times out has a regression test.
    • A startup command is never run twice: if main's start fails, the window does not run it again.
    • Main only accepts this request from this host's own desktop window, checked by the caller identity its IPC channel proved. A paired caller is refused with host_not_owned, exactly as a split addressed by pane is.
  • A Codex account-switch restart only binds into a pane main's layout holds. If main's layout no longer holds that pane, it starts nothing and asks the user again.
  • Deleted worktree-sleep-request-relay and its test, now unused.

Switch-on bugs fixed, each with a fail-first test:

  1. Pinned tabs were closed. runtime-layout-apply.ts forced every closeTabs, so a CLI or phone close through main closed a pinned tab. It now honours refusePinned. Tests: acknowledged-terminal-tab-retirement-main-close.test.ts "refuses a tab pinned before the close and keeps it", and close-mobile-row-in-main-layout.test.ts "refuses a pinned tab…".

  2. A dormant SSH relay session kept running after a close through main. A tab close now also stops the tab's saved SSH relay session. Test: "stops the visible and the dormant SSH terminal of the closed tab".

  3. A refused close cancelled the tab's agent launch. Main marked a tab's in-flight agent launches as closed by the user before applying the close. When a pinned tab refused the close, the mark stayed, so the finished launch took its pane back and dropped its failure. Main now marks only tabs the close actually took. Test: close-mobile-row-in-main-layout.test.ts "leaves a launch running in a pinned tab it refused to close to finish into the tab".

Interim code (ledger L260): after a phone closes a tab or pane through main, retireMainClosedRows (close-mobile-row-in-main-layout.ts) drops the closed rows from what phones list straight away. Without it, phones only see the change at the window's next publish. It goes away once main merges its layout into the phone rows after every layout commit, which covers CLI closes, renames and creates too. Switch-off builds are not affected: a temporary end-to-end check showed phones update within 9–66 ms after a CLI close or rename.

Still on the window's own spawn until PR-3 (ledger L252): new tabs on SSH and WSL, new tabs with a shell override, agent-launch tabs, paste-delivered startups, and F-a. One predicate decides this, startupNeedsWindowSpawn in window-tab-start-in-main.ts, together with the SSH, WSL and shell-override checks in windowTabStartsInMain. PR-3 deletes both.

Why

Main owning the layout is the plan of record. The window-first round trip was the reason phone and CLI actions failed or went stale with no window open. Moving each action into main's single layout command removes that round trip, instead of patching each relay.

Alternatives considered:

  • Re-storing the window's old phone rows on every republish. Rejected: it can undo newer removals. That is why the row drop is interim, pending the merge described above.
  • Leaving the window to start new tabs. Rejected: two owners can then race for the same pane.

Linked Issue

N/A (internal layout-owner track).

Visual Proof

N/A. Switch-off builds are unchanged (see the bundle diff below), and the switch-on build is dark and unshipped.

Testing

  • Static checks: ci-static-gate (every step of pr.yml's static job): all passed at c9b20cd and again at 9cb5b6c.

  • Switch-on unit suite, head vs base: main de7a2d2, per-test diff. Folders: main runtime, persistence and ipc; renderer runtime, store, lib, tab-bar, terminal, terminal-pane, browser-pane, native-chat, right-sidebar and editor; shared workspace-layout. Result: 0 pass-to-fail, 0 new failures (55 failures at base and head, the same set).

  • Switch-off unit suite, same folders: 0 pass-to-fail, 0 new failures.

  • Server specs: switch-on, fresh build at each commit, through the app slot.

    Spec Head 9cb5b6c Base de7a2d2
    headless-serve-focused-terminal-create pass pass
    paired-mobile-desktop-tab-composition pass pass
    paired-desktop-server-layout-frames pass pass
    live-background-terminal-mount-authority pass pass
  • Switch-off bundle diff (unminified, clean throwaway worktrees, head vs de7a2d2):

    • preload: identical.
    • renderer: 2 chunks change. They are gated or inert: a restart-owner stub that returns 'window', the agent-launch branches that only run when main shows the tab, a module move, and a store subscribe parameter.
    • main: the request schema's new optional fields; the windowTab handler, whose start refuses switch-off; class layers whose callers are all switch-gated (main's surfaces are created only switch-on); and the layout.apply force change. layout.apply is not registered switch-off.
  • Latency (switch-on, layout oracle, base and head interleaved, 4 pairs): no case shows a repeatable slowdown of 16 ms or more. The "close pane" case jumps between about 50 ms and about 150 ms at base and head alike. Pooled medians, base vs head: idle 47 vs 63, flood 132 vs 139, titles 197 vs 178. Close-pane samples over 100 ms: 36–69% at base, 48–63% at head. PR-1 does not change the window's own pane-close path.

  • Converted tests: tests that counted the window's own spawn, publish or acknowledgement now check main's equivalent, and each keeps its switch-off form.

    • store-terminal-tab-changes-layout-owner, store-active-worktree-split-groups, tabs-split-create, workspace-layout-frame-held-tabs, codex- and claude-detached-pane-restart, agent-launch-instant-tab, acknowledged-terminal-tab-retirement, close-by-split-handle-layout-owner.
    • cold-restore-terminal-membership: its fixture gains main's layout write.
    • terminal-retirement-async-durability: now a switch-off test. Its switch-on twin checks that main's close answers only once its layout write is durable.
    • worktree-creation-background-mount and worktree-creation-structured-session-host-decline: a startup counts as run when main starts it.
  • New tests:

    • runtime-window-tab-start.test.ts: the real runtime:call path. Main starts the window's tab; a paired caller is refused.
    • window-tab-start-layout-owner and window-tab-start-switch-off: old clients and switch-off builds get today's behaviour.
    • store-create-tab-start-in-main: wait, attach, failure reason, owner change, no second run, closed-tab cleanup, startupNeedsWindowSpawn in both directions.
    • windowless-main-layout-commands-layout-owner, close-mobile-row-in-main-layout, managed-worktree-sleep-layout-owner, main-layout-agent-launch-tabs, codex-restart-main-pane.
  • I manually tested these changes locally (hidden-window e2e only; the switch-on build is dark)

  • Automated tests added/updated

Review

Reviewed by Opus over several rounds, plus delta reviews. Known low items, not fixed here:

  • A close addressed to a tab outside the 32-entry list falls back to the window's own close.
  • An agent-launch verdict that arrives before its publish reply is ignored.
  • Drawn-tab holds that are never drawn have no size cap (ledger L263).
  • The interim row drop has no test for closing one pane of a split.
  • A failed start drops its startup even if main ran nothing. This is deliberate: running it twice is worse.
  • terminal_pane_owner_changed releases the pane at once instead of waiting until the winning terminal is drawn.
  • Not fixed by this PR: L256 and L259 (W4's follow-up on main-layout-close-command.ts).

Agent skill upstream boundary

  • Not applicable.

Notes

  • SSH: close stops go to the host that owns the workspace, and so does an SSH sleep. New tabs on SSH keep the window spawn until PR-3.
  • Older peers: terminal.create's new fields are optional. Older clients get today's behaviour (tested).
  • Folder workspaces: they go through the same workspace-id path.

…ps and agent-launch tabs itself, with or without a window (layout-owner build)
…SH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build)
…ow's ids, and the pane attaches to it (layout-owner build)
…s layout holds, with no window tree (layout-owner build, L197)
…t to, and a failed start keeps the tab's startup
…hy, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build)
… and a failed start's startup never runs twice
…, so no runtime module loads before its base class
…m what phones list at once, not at the window's next publish (layout-owner build)
…w asks over IPC, which labels itself a client (layout-owner build)
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e56bdbd7-009e-4b1f-93da-b61d69c3d139
📥 Commits

Reviewing files that changed from the base of the PR and between c9b20cd and 9cb5b6c.

📒 Files selected for processing: 3
  • src/main/runtime/runtime-layout-apply.test.ts
  • src/main/runtime/runtime-layout-apply.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-row-in-main-layout.test.ts
🚧 Files skipped from review as they are similar to previous changes: 2
  • src/main/runtime/runtime-layout-apply.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-row-in-main-layout.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

📝 Walkthrough

The changes add main-owned handling for terminal startup, agent-launch tabs, terminal closure, layout naming and properties, detached-pane restarts, and managed-worktree sleep. Renderer and runtime code now coordinate with main-owned layout state. The changes also add ownership-specific tests and remove the renderer worktree-sleep relay.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 9cb5b

Mobile tab-property updates can target the wrong tab and fail for surface-addressed requests. Resolve this before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 45.90% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 51 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the primary changes: main-owned terminal closes, renames, sleep, and startup for layout-owner builds. It is long but specific and directly related to the changeset.
Description check Passed The description covers the required high-level summary, changes, rationale, testing, visual proof, compatibility considerations, known limitations, and checklist status. The N/A linked issue is explai…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description covers the required high-level summary, changes, rationale, testing, visual proof, compatibility considerations, known limitations, and checklist status. The N/A linked issue is explained as an internal layout-owner track, and the AI disclosure section is appropriately omitted for an internal contributor.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e92457f9-ae72-4149-93bc-8480bd390870
📥 Commits

Reviewing files that changed from the base of the PR and between 50b07c0 and c9b20cd.

📒 Files selected for processing: 54
  • src/main/ipc/runtime-window-tab-start.test.ts
  • src/main/runtime/acknowledged-terminal-tab-retirement-main-close.test.ts
  • src/main/runtime/acknowledged-terminal-tab-retirement.test.ts
  • src/main/runtime/main-owned-workspace-runtime-fixture.ts
  • src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts
  • src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts
  • src/main/runtime/rpc/methods/agent-launch-tab-publication.ts
  • src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts
  • src/main/runtime/runtime-layout-apply.ts
  • src/main/runtime/session-lifecycle/close/close-by-split-handle-layout-owner.test.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-row-in-main-layout.test.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-row-in-main-layout.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-session-tab.ts
  • src/main/runtime/session-tabs/explicit-terminal-close-stops.ts
  • src/main/runtime/session-tabs/headless-terminal-tab-close.ts
  • src/main/runtime/session-tabs/main-handle-close.ts
  • src/main/runtime/session-tabs/main-layout-agent-launch-tabs.test.ts
  • src/main/runtime/session-tabs/main-layout-agent-launch-tabs.ts
  • src/main/runtime/session-tabs/main-layout-close-command.ts
  • src/main/runtime/session-tabs/main-layout-renames.ts
  • src/main/runtime/terminal-retirement-async-durability.test.ts
  • src/main/runtime/terminal/launch/failed-start-pane.ts
  • src/main/runtime/terminal/launch/main-owned-tab-layout.ts
  • src/main/runtime/terminal/launch/terminal-split-in-main.ts
  • src/main/runtime/terminal/launch/window-tab-start-layout-owner.test.ts
  • src/main/runtime/terminal/launch/window-tab-start-switch-off.test.ts
  • src/main/runtime/windowless-main-layout-commands-layout-owner.test.ts
  • src/main/runtime/workspaces/managed-workspace-operations.ts
  • src/main/runtime/workspaces/managed-worktree-sleep-layout-owner.test.ts
  • src/main/runtime/workspaces/workspace-removal-target.ts
  • src/main/window/worktree-sleep-request-relay.test.ts
  • src/main/window/worktree-sleep-request-relay.ts
  • src/renderer/src/components/terminal-pane/claude-detached-pane-restart.test.ts
  • src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts
  • src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts
  • src/renderer/src/components/terminal-pane/codex-restart-main-pane.test.ts
  • src/renderer/src/components/terminal-pane/codex-restart-main-pane.ts
  • src/renderer/src/hooks/ipc-events/agent-launch-tab-ipc-bridge.ts
  • src/renderer/src/lib/agent-launch-pane-verdict-application.ts
  • src/renderer/src/lib/agent-launch-tab-main-written.test.ts
  • src/renderer/src/lib/agent-launch-tab-main-written.ts
  • src/renderer/src/lib/worktree-creation-background-mount.test.ts
  • src/renderer/src/lib/worktree-creation-structured-session-host-decline.test.ts
  • src/renderer/src/runtime/workspace-layout-frame-held-tabs.test.ts
  • src/renderer/src/store/slices/store-active-worktree-split-groups.test.ts
  • src/renderer/src/store/slices/store-create-tab-start-in-main.test.ts
  • src/renderer/src/store/slices/store-terminal-tab-changes-layout-owner.test.ts
  • src/renderer/src/store/slices/tabs-split-create.test.ts
  • src/renderer/src/store/slices/terminals.ts
  • src/renderer/src/store/terminals/terminal-tab-create-through-layout-owner.ts
  • src/renderer/src/store/terminals/terminal-tab-creation.ts
  • src/renderer/src/store/terminals/window-tab-start-in-main.ts
  • src/shared/agent-launch-tab-publication.ts
  • src/shared/rpc-contract/terminal-unary-params.ts
💤 Files with no reviewable changes: 2
  • src/main/window/worktree-sleep-request-relay.test.ts
  • src/main/window/worktree-sleep-request-relay.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment on lines +144 to +146
const { customTitle, ...tabProps } = props
if (Object.values(tabProps).some((value) => value !== undefined)) {
await this.applyMainRename({ type: 'setTabProps', workspace: worktreeId, tabId, ...tabProps })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'setMainTabProps|setMobileSessionTabProps|resolveMobileSessionHostTabId' src/main/runtime
sed -n '295,330p' src/main/runtime/session-tabs/headless-terminal-tab-close.ts
sed -n '125,170p' src/main/runtime/session-tabs/main-layout-renames.ts

Repository: stablyai/orca

Length of output: 9319


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- resolver and host-tab mapping ---'
sed -n '150,205p' src/main/runtime/workspaces/workspace-file-session-targets.ts
sed -n '1,135p' src/main/runtime/rpc/methods/session-tab-mutation-methods.ts
printf '%s\n' '--- setMobileSessionTabProps complete method ---'
sed -n '270,335p' src/main/runtime/session-tabs/headless-terminal-tab-close.ts
printf '%s\n' '--- relevant type/declaration and callers ---'
rg -n -F --glob '*.ts' -e 'tab::leaf' -e 'setMobileSessionTabProps(' -e 'session.tabs.setTabProps' src/main/runtime src/shared | head -200

Repository: stablyai/orca

Length of output: 15474


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- resolver and host-tab mapping ---'
sed -n '150,205p' src/main/runtime/workspaces/workspace-file-session-targets.ts
printf '%s\n' '--- RPC mapping ---'
sed -n '1,135p' src/main/runtime/rpc/methods/session-tab-mutation-methods.ts
printf '%s\n' '--- setter ---'
sed -n '270,335p' src/main/runtime/session-tabs/headless-terminal-tab-close.ts
printf '%s\n' '--- relevant callers and surface IDs ---'
rg -n -F --glob '*.ts' -e 'tab::leaf' -e 'setMobileSessionTabProps(' -e 'session.tabs.setTabProps' src/main/runtime src/shared | head -200

Repository: stablyai/orca

Length of output: 15455


🏁 Script executed:

printf '%s\n' '--- resolver ---'
nl -ba src/main/runtime/workspaces/workspace-file-session-targets.ts | sed -n '165,195p'
printf '%s\n' '--- setter caller ---'
nl -ba src/main/runtime/session-tabs/headless-terminal-tab-close.ts | sed -n '287,315p'
printf '%s\n' '--- RPC input mapping ---'
nl -ba src/main/runtime/rpc/methods/session-tab-mutation-methods.ts | sed -n '100,125p'
printf '%s\n' '--- surface-id definitions/usages ---'
rg -n -F --glob '*.ts' -e 'tab::leaf' -e 'parseSessionTab' -e 'resolveMobileSessionHostTabId' src/main src/shared | head -160

Repository: stablyai/orca

Length of output: 8950


Filter props before forwarding it to setMainTabProps.

setMobileSessionTabProps resolves hostTabId but passes the full args object as props. Since setMainTabProps removes only customTitle, tabId remains in tabProps. The later spread overwrites the resolved host tab ID with the raw client ID. A terminal surface ID can differ from its host tab ID, so the command can target the wrong tab. The defined tabId also makes the some(...) check true for a customTitle-only request.

🐛 Suggested fix
--- "a/src/main/runtime/session-tabs/main-layout-renames.ts"
+++ "b/src/main/runtime/session-tabs/main-layout-renames.ts"
@@ -141,10 +141,15 @@
     if (!panes) {
       return false
     }
-    const { customTitle, ...tabProps } = props
-    if (Object.values(tabProps).some((value) => value !== undefined)) {
-      await this.applyMainRename({ type: 'setTabProps', workspace: worktreeId, tabId, ...tabProps })
-    }
+    const { customTitle, color, isPinned, viewMode } = props
+    const tabProps = {
+      ...(color !== undefined ? { color } : {}),
+      ...(isPinned !== undefined ? { isPinned } : {}),
+      ...(viewMode !== undefined ? { viewMode } : {})
+    }
+    if (Object.keys(tabProps).length > 0) {
+      await this.applyMainRename({ type: 'setTabProps', workspace: worktreeId, tabId, ...tabProps })
+    }
     if (customTitle !== undefined) {
       // Why by tab: a sleeping or starting pane has no handle for terminal.rename to name (#29975).
       const title = customTitle?.trim() || null

Add coverage for a terminal surface ID and a customTitle-only request. Assert that the command uses the resolved host tab ID and does not send an empty setTabProps command.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { customTitle, ...tabProps } = props
if (Object.values(tabProps).some((value) => value !== undefined)) {
await this.applyMainRename({ type: 'setTabProps', workspace: worktreeId, tabId, ...tabProps })
const { customTitle, color, isPinned, viewMode } = props
const tabProps = {
...(color !== undefined ? { color } : {}),
...(isPinned !== undefined ? { isPinned } : {}),
...(viewMode !== undefined ? { viewMode } : {})
}
if (Object.keys(tabProps).length > 0) {
await this.applyMainRename({ type: 'setTabProps', workspace: worktreeId, tabId, ...tabProps })

currentState.queuePaneAccountRestarts([ptyId], agent)
return
}
const paneOwner = restartPaneOwner(currentState, worktreeId, tab.id, leafId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Inspect how main-owned pane bindings reach the renderer and the restart queue.
rg -n -C 4 'terminalLayoutsByTabId|ptyIdsByLeafId|queuePaneAccountRestarts|provideLayoutFrameTerminalPanes' \
  src/renderer/src/runtime src/renderer/src/store src/renderer/src/components/terminal-pane

Repository: stablyai/orca

Length of output: 45664


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- changed file ---'
nl -ba src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts | sed -n '1,280p'
printf '%s\n' '--- direct symbols ---'
rg -n -F --glob '*.ts' -e 'function locateCodexPane' -e 'const locateCodexPane' -e 'function restartPaneOwner' -e 'const restartPaneOwner' -e 'queuePaneAccountRestarts' -e 'provideLayoutFrameTerminalPanes' -e 'terminalLayoutsByTabId' src/renderer/src/components/terminal-pane src/renderer/src/runtime src/renderer/src/store | sed -n '1,260p'
printf '%s\n' '--- ownership and layout-frame candidates ---'
rg -n -F --glob '*.ts' -e 'paneOwner' -e 'owner' -e 'ptyIdsByLeafId' src/renderer/src/runtime src/renderer/src/components/terminal-pane src/renderer/src/store | rg 'layout|frame|owner|restart|ptyIdsByLeafId|terminal-pane' | sed -n '1,300p'

Repository: stablyai/orca

Length of output: 41447


🏁 Script executed:

set -o pipefail
nl -ba src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts | sed -n '1,280p'
rg -n -F --glob '*.ts' -e 'function locateCodexPane' -e 'function restartPaneOwner' -e 'queuePaneAccountRestarts' -e 'provideLayoutFrameTerminalPanes' -e 'ptyIdsByLeafId' src/renderer/src/components/terminal-pane src/renderer/src/runtime src/renderer/src/store | sed -n '1,320p'

Repository: stablyai/orca

Length of output: 41948


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- restart owner implementation ---'
nl -ba src/renderer/src/components/terminal-pane/codex-restart-main-pane.ts | sed -n '1,240p'
printf '%s\n' '--- restart owner tests ---'
nl -ba src/renderer/src/components/terminal-pane/codex-restart-main-pane.test.ts | sed -n '1,180p'
printf '%s\n' '--- frame terminal pane publication ---'
nl -ba src/renderer/src/runtime/layout-frame-terminal-panes.ts | sed -n '1,260p'
printf '%s\n' '--- relevant diff ---'
git diff de7a2d24f9c3f2090bd39a672fc4ba555073eb67 c9b20cd73f1485774cadf0fedc8a1defba02a48b -- src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts src/renderer/src/components/terminal-pane/codex-restart-main-pane.ts src/renderer/src/runtime/layout-frame-terminal-panes.ts

Repository: stablyai/orca

Length of output: 12503


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- binding and frame synchronization ---'
rg -n -F --glob '*.ts' -e 'terminal-pty-bindings' -e 'ptyIdsByLeafId' -e 'layoutFrameTerminalTab' -e 'terminalTab:' -e 'holdsTerminalPane' src/renderer/src/runtime src/renderer/src/components/terminal-pane src/renderer/src/store | rg 'binding|frame|layout|ptyIdsByLeafId|terminalTab|holdsTerminalPane' | sed -n '1,260p'
printf '%s\n' '--- likely binding definitions ---'
for f in src/renderer/src/components/terminal-pane/terminal-pty-bindings.ts src/renderer/src/runtime/workspace-layout-frame-sync.ts src/renderer/src/runtime/layout-frame-terminal-panes.ts; do
  if test -f "$f"; then echo "--- $f"; nl -ba "$f" | sed -n '1,280p'; fi
done
printf '%s\n' '--- restart queue producers ---'
rg -n -C 5 -F --glob '*.ts' -e 'queuePaneAccountRestarts(' -e 'pendingPaneAccountRestarts' src/renderer/src | sed -n '1,320p'

Repository: stablyai/orca

Length of output: 42148


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- frame sync ownership and bindings ---'
nl -ba src/renderer/src/runtime/workspace-layout-frame-sync.ts | sed -n '300,355p'
nl -ba src/renderer/src/runtime/workspace-layout-frame-bindings.ts | sed -n '1,150p'
printf '%s\n' '--- main-listed pane tests ---'
nl -ba src/renderer/src/runtime/sync-runtime-graph-main-listed-panes.test.ts | sed -n '1,180p'
printf '%s\n' '--- frame binding tests ---'
nl -ba src/renderer/src/runtime/workspace-layout-frame-records.test.ts | sed -n '130,235p'
printf '%s\n' '--- direct unbound-main scenarios ---'
rg -n -C 6 -F --glob '*.ts' -e 'root: null' -e 'ptyIdsByLeafId: {}' -e 'terminalTab: (worktreeId, tabId)' src/renderer/src/runtime src/renderer/src/components/terminal-pane | rg 'main|frame|restart|held|terminalTab|ptyIdsByLeafId|root: null' | sed -n '1,260p'

Repository: stablyai/orca

Length of output: 34221


🏁 Script executed:

set -o pipefail
nl -ba src/renderer/src/runtime/workspace-layout-frame-sync.ts | sed -n '120,305p'
printf '%s\n' '--- frame binding call sites ---'
rg -n -C 12 -F --glob '*.ts' -e 'terminalLayoutsWithReboundFrames' -e 'unshown' -e 'show(terminalLayoutsPatch' src/renderer/src/runtime/workspace-layout-frame-sync.ts src/renderer/src/runtime
printf '%s\n' '--- layout plan binding behavior ---'
nl -ba src/renderer/src/store/terminals/workspace-terminal-layout-plan.ts | sed -n '1,110p'

Repository: stablyai/orca

Length of output: 29647


Resolve main-held panes before the null-leaf fallback.

For a Codex restart, locateCodexPane can find the PTY through the tab-level index while returning leafId: null. The null-leaf branch then kills the old PTY and queues startup for a later mount before restartPaneOwner can inspect main’s published pane. Resolve the leaf from main’s published tab before this fallback, then preserve the fallback when neither layout provides a leaf.

@Jinwoo-H
Jinwoo-H merged commit 0eb2403 into main Oct 21, 2026
53 checks passed
Jinwoo-H added a commit that referenced this pull request Oct 21, 2026
…c-restore-view-attach

Main's layout-command-commit.ts is the one commit module now: it gains C1c's run hooks (live facts, stop-before-write, noted effects), and topology-partition-write.ts is gone. The restored-pane starter keeps its started-terminal cache over main's StartedPty result, and materialize returns main's create result after marking a resumed record live.

Claude-Session: W5b
Jinwoo-H added a commit that referenced this pull request Oct 22, 2026
…ds a terminal asks main's layout (#32002)

* refactor(runtime): every reader of a terminal's pane goes through paneOfPty (L80)

With the layout-owner switch on, main's layout answers which tab and pane hold a terminal, and
nothing reads or writes the PTY record's tabId/paneKey copies or their graph stamp. Switch-off
reads the copies exactly as before.

* refactor(runtime): read a terminal's pane through the runtime's paneOfPty method

* fix(runtime): main's pane index answers paneOfPty before any window graph

A headless host never installs a window graph, so the query asked a publisher that did not exist
and every terminal read as paneless. The index is now made on first query; it publishes only once a
graph is installed, as before.

* test(runtime): pin that a query with no window publishes nothing

* test(runtime): a terminal summary reads its pane once (L80)

* feat(runtime): pin bind-first at registration, and one own-handle rule with or without a window (switch-on only)

* test(runtime): census and tests that every create path binds its pane before it registers (L80)

* test(runtime): a bind-first fixture that stands in for the spawn commit (L80)

* test(runtime): fixtures bind a terminal's pane before registering it, as the spawn commit does (L80)

Also, with the switch on: skip the bind-first check for a paired server's workspace, release a
closed tab's terminal ownership once main has dropped the pane, and publish a pane's agent status
from the terminal main's layout binds it to.

* feat(runtime): an exit after main's close still finds the pane it retires (switch-on only, L80 Q2)

Main's layout write records every pane it drops whose terminal no pane holds any more, in a new
optional session field closedTerminalPanesByPaneKey (same TTL and cap as closed-tab records).
Exit cleanup reads main's layout first, then that record by terminal id and incarnation, and
removes the entry it used. Older builds ignore the field.

* refactor(layout): one dropped-leaf step every live layout edit runs (switch-on only, L80 Q2)

The step receives each dropped pane (host, worktree, tab, leaf, terminal, incarnation), computed
across the whole layout so a moved pane is not dropped, and runs only for a commit that declares a
live edit. Today it records the closed-pane entries; a drop is recorded once per terminal.

* fix(runtime): a phone or headless tab close still stops and releases its live terminals after main's layout drops the panes (switch-on only, L80)

The headless close reads each row's live terminal before main's close drops the panes it is found
by, and a renderer-retired tab releases the owned terminal main left in no pane, as the explicit
close already did. Close tests now commit through main's layout and bind panes first.

* fix(runtime): a local workspace's terminal tab left in a paired server's partition is main's pane (switch-on only, L257)

Older builds could save a local worktree's tab rows into a runtime partition (#18202); its terminals
are local, so main's index takes those tabs for workspaces the router homes locally. A tab or
terminal the local partition also holds is indexed once, from there; SSH and server-owned
workspaces are skipped as before.

* test(layout): the legacy-close comparison leaves out the closed-pane record only layout-owner builds write (L80)

* test(pty): a pane whose failed start withdrew its tab resolves to terminal_not_found in layout-owner builds (L80 #7)

Main's layout no longer holds the pane, so there is no pane to answer for; window-owned builds keep
the disconnected stub from the exited terminal's record.

* test(runtime): orca-runtime suites bind panes first, as the real store and spawn commit do (L80)

Fake PTY spawns commit the binding into the pane the runtime wrote, stores list their partitions,
and SSH sessions sit in their ssh:<conn> partition; terminals registered directly bind first.

* fix(runtime): main's layout names a pane's terminal only when the workspace and stamped incarnation match (switch-on only, L80)

A record made before its adoption registers (a host restart) or a local id reused after one
matched a saved binding by id alone, so a phone or paired desktop could be handed a second handle
for the pane before the terminal's own handle arrived and stay on it.

* test(runtime): a restored pane is only unnamed in the adoption gap, and no local record rescues a paired server's tab (L80)

* test(runtime): recovery, close and routing suites set up terminals as production does (L80)

A recoverable pane is one the host inventory dropped with no exit, live terminals register
rather than appear only in the window graph, and SSH stores route as the real Store does.

* test(runtime): suspects 3, 4 and 5 bind, register and apply as production does; PR-1's main-layout closes in the part-10 and part-11 close tests (L80)

* test(runtime): a fixture store applies main's layout commands as a profile does; PR-1's own closes in the part-05, part-08 and UUID-leaf close tests (L80)

* test(runtime): the profile-store stand-in moves to its own fixture, keeping bind-first under the line limit (L80)

* test(runtime): orphan adoption reads the pane from main's layout with the switch on, not the record copy (L80)

* test(runtime): a phone's pane close and a pinned tab close are main's layout closes with the switch on (#31571), in the no-widening and surface-close tables (L80)

* test(runtime): a paired phone close commits in main's layout with no relay ack when the switch is on (#31571) (L80)

* fix(terminal): a parked pane resumes its agent once main has closed the other pane that held the session (layout-owner build, L80 P1)

* test(runtime): the parked-pane suite's restart holder is a saved pane with no registered terminal, under the line limit (L80 P1)

* fix(terminal): a spawn commit that binds a reused local terminal id unbinds it from the saved pane of the older process (layout-owner build, L80 P2)

* test(runtime): the legacy OSC row test uses the UUID leaf main's layout holds and registers its live terminal with the switch on (L80 E5)

* test(runtime): the pty-exit echo close of a dead headless tab is expected to fail with the switch on until W4's close PR (L255)

* fix(terminal): a terminal record that arrives, changes incarnation or moves workspace after main composed its panes re-composes them, so a restored pane lists once (layout-owner build, L80)

* fix(terminal): a pane bound to a terminal a paired server runs keeps that binding and host, and is never indexed as a local spilled tab (layout-owner build, L80/L257)

* fix(terminal): a pane main owns no longer proves its terminal runs; the host's inventory and the terminal's record decide, so a dropped shell reads disconnected (layout-owner build, L80)

* fix(terminal): an SSH pane whose host says its shell ended keeps its leaf on a negative or failed code, as the window does, so recovery starts a shell in it; a stale handle learns the connected replacement (layout-owner build, L80)

* fix(agent-launch): a terminal main's layout still binds counts as running through a host listing gap, so a launch never starts a second shell in its pane (layout-owner build, L80)

* fix(terminal): a disconnected terminal main still binds stays a stop target, never reads stopped unproven, and keeps an exit wait waiting until a certified exit (layout-owner build, L80)

* test(terminal): the wait unit fixture answers the exit-settled dependency

* test(terminal): realistic fixtures for three phone-row tests: unique leaf ids per pane, and a window that draws main's own panes (layout-owner build, L80)

* fix(terminal): a stale handle learns a pane's replacement only when it was resolved for that pane, its own terminal ended, and the replacement is connected there now (layout-owner build, L80)

* test(session-tabs): switch-on variant for a window's chat placement (main's layout order rules); headless phone tab order is L283

* test(terminal-close): switch-on close reds name their ledger rows (L255, L256, L259a, L259b); a same-id respawn's commit stamps its incarnation; the crossing terminal binds first

* test(terminal): the crossing registration is refused by main's workspace fence (L256 switch-on variant); the placement test's tabs are main's saved layout

* refactor(terminal): the stale-handle guard reads an issue-time pane stamp on the handle record, not a second map; moved-pane, restart and single-reader tests (layout-owner build, L80)

* test(session-tabs): switch-on variants: the desktop's tab name is main's rename, and a mobile-created tab's live shell is the one its commit registers (layout-owner build, L80)

* fix(terminal): a re-issued handle keeps its issue-time pane while no pane holds its terminal (review)

* fix(terminal): stop targets and exit waits use main's holds check (workspace and incarnation), and any exit event settles a later wait (review)

* test(session-tabs): name the switch-on placement and split-group reds L288 and L289

* fix(terminal): an exit drops a closed pane's record in the same write that retires the pane, and an unbind folds into the binding write (layout-owner build, L80)

The exit used to remove main's closed-pane entry in a session write of its own, before the
retirement write. It now reads the entry, and the retirement write for that pane drops it,
re-checked by terminal id and incarnation. An exit that keeps the surface (relay contact lost,
intentional stop) keeps the entry until a confirmed exit or its TTL.

A reused terminal id's unbind is now a pure computation that applyPtyBinding applies through
its existing writes. Two split-pane tests still expected a -1 exit to close the pane; they now
follow the ruling that any negative code keeps it. Static-gate fixes: test fixtures and tests
use typed member access instead of Reflect, and no longer use type assertions.

* test(runtime): the shared fixture store announces its session writes and the L283 store lists its partitions, as a profile Store does; L289's test passes on that store (layout-owner build, L80)

The fixture store bindPaneFirst fills now tells write listeners of every session write, direct or
by a layout command, so main's post-commit merge runs as it does on a real Store. The notice is
not enumerable, so a test's own `{ ...store }` copy, which keeps its writes elsewhere, does not
inherit a subscription it would never fire. With it, the targeted split-group create (L289) passes
at this head, so its it.fails mark goes. L283's test runs on a store that names its partitions and
stays it.fails("L283").

* fix(terminal): recovery checks the caller's handle after the cheap guards again, so the switch-off build re-issues no handle on a refused recovery (layout-owner build, L80)

The stale-handle switch point had moved the handle lookup ahead of the pane, terminal and
workspace checks. That lookup can re-issue a handle, so a switch-off recovery refused on those
checks gained a side effect base never had. The lookup runs after them again, and the switch-on
replacement check lives in one method.

* test(runtime): the layout-owner start fixture's spawn commits the pane binding before main registers the terminal, as the host does (L80 switch-on variant)

The window-tab start test came in with the main merge after bind-first; its fake spawn returned
a terminal no pane was bound to, so registration refused it. The fixture's spawn now binds the
pane first, as every other bind-first fixture does.

* fix(terminal): a new process under a reused terminal id starts with no exit code, so an exit wait on it cannot settle on the old process's code (layout-owner build, L80)

The record of a reused id kept the earlier process's lastExitCode, and with the switch on an exit
wait settles on any recorded code once the terminal reads disconnected. A new incarnation now
clears the code and cause. Also drops the L283 test's partition-list option: the fixture store
already lists its partitions.

* test(runtime): switch-on variants for four tests the full local run never loaded (L80)

- A restored pane's host lists the terminal it started; a pane main owns vouches for none.
- The other pane holding a slept session is in main's layout, so an unverifiable holder blocks
  the resume as it does through a window.
- Main sleeps a workspace whose panes it lists itself, even with a renderer attached (#31571).
- A graph sync no longer reconnects a disconnected terminal of main's pane; the inventory decides.

* test(runtime): the phone Markdown route tests use #31667's realistic store, and the untitled cases wait on L268 with the switch on

The window's tab uses main's id (the file path), and the store lists its partitions and saves each
open editor's unified tab and group, as the window does (the fixture from #31667). With the
switch on, the three untitled cases fail on L268, main's row merge dropping the untitled flag,
which #31667 fixes; they are it.fails until A1b's history includes it.

* refactor(runtime): two merged files back under the line limit, by moving A1b's additions to their helpers (L80)

After the main merge, the holder-pane check and a closed tab's live-terminal lookup pushed two
files past the line limit. The holder check is now paneHoldsSession (main-terminal-surfaces.ts)
behind one method, and the closed tab's lookup is one parent-class method. Behaviour unchanged.

* test(runtime): the headless chat tests open the chat on the publish path, as every product path does; L283 stays it.fails for the main-owned reorder

openChat recorded the chat only in the runtime's snapshot, so main's layout never held it and the
phone's reorder was refused as an unknown tab. On the publish path main's layout holds it; the
reorder is accepted but does not survive the row rebuild after a close (L283, reopened).

* test(runtime): bind-first census follows the one spawn-commit step (#31596); the unbound-registration test keeps its spawn from the suites' stand-in commit

After the main merge both start lanes register in spawn-commit-assembly.ts, after
savePtySpawnPaneBinding (or, adopted, on the live owner's pane); the census lists that file. A
phone create now writes its pane before the spawn, so the suites' stand-in spawn commit bound it
and the test of a path that registers unbound passed for the wrong reason; that test's controller
now opts out of the stand-in commit.

* test(runtime): convert four switch-on tests the disguised full runs hid

Real switch-on runs showed four files whose fakes registered terminals unbound
or confirmed a stop without its exit. Bind first, stamp the source as main
does, and deliver the exit a confirmed stop delivers. Switch-off unchanged.

* test: type the two converted fakes instead of casting them

The PTY controller fake now takes the controller's optional pane fields, and the
switch-on store is a typed RuntimeStore, so neither needs an assertion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant