Skip to content

refactor(terminal): pending panes; drag-out and closes follow main's topology (core C5a) - #26307

Closed
Jinwoo-H wants to merge 6 commits into
topo-c4-reconcilerfrom
topo-c5a-pending-panes
Closed

Jinwoo-H wants to merge 6 commits into
topo-c4-reconcilerfrom
topo-c5a-pending-panes

Conversation

@Jinwoo-H

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

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​483 $\color{#cf222e}{\Huge{\mathbf{−}}}$​418 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​65
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​382 $\color{#cf222e}{\Huge{\mathbf{−}}}$​148 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​234

ELI5

When you open, close or drag out a terminal tab or pane, the window shows the change right away, but main (the part of Orca that saves terminal layout) learns about it a moment later. Since the earlier core steps, the window copies main's layout. That opened two gaps: an update from main that was sent before it heard about your change could undo it on screen (a new tab vanishing, a closed pane coming back), and dragging a pane out could close the new tab it was dragged into. This step keeps your change on screen until main's own layout includes it.

This is core step C5a of the terminal layout refactor (C5 is split into a..d). The core steps are reviewed as stacked PRs and merged together as one unit, so main never gets a half-finished version. Stacked on C4 (#26301).

What Changed

Pending tabs and panes (new store/terminals/terminal-pending-panes.ts). A short list of tabs and panes the window shows or hides ahead of main:

  • Added: createTab marks its tab, and a pane the window creates that main's layout doesn't name yet (a split, or a new tab's first pane) is marked too. It stays until a layout update from main names it.
  • Removed: every tab or pane close already goes through one function, commitTerminalSurfaceClose. It now hides the tab or pane until the window has applied the update that holds main's reply (the reply's publishSeq). This works the same whether the reply or the update arrives first. If main refuses the close, the window shows main's copy again.
  • The layout copy (terminal-topology-mirror.ts) keeps a pending new tab, and doesn't bring back a pending closed one.
  • The pane reconciler keeps a pending pane and never re-adds a pane that was closed here. This replaces C4's two stand-ins (the "spawn still in progress" check and trackRetiredLeafIds).

Drag-out to a new tab (terminal-pane-tab-detach.ts). The window waits for main to move the pane and for main's update to arrive, then only places the new tab where you dropped it, activates it, and moves the live PTY tracking (syncPaneDetachPtyOwnership). It no longer creates the tab or writes either layout itself. Main's update creates the tab, and the reconciler detaches the old pane. Before: if main's update arrived before its reply, the window couldn't find the pane and closed the new tab.

Deleted replaceTerminalLayoutPanePtyId. Which PTY a pane is attached to now comes from main's layout. The Codex restart path already gets the new id through updateTabPtyId, and main records the replacement.

Web clients keep today's window-only detach (the not_held answer). Their tabs live on a remote Orca server, which has no "move a pane" request. Adding one would change what the client and server send each other, and mixed versions would have to negotiate it, so it isn't a small change.

Why

The window can only follow main's layout if its own changes aren't undone while main catches up. One small list, read by the layout copy and the reconciler, does that without new state in each caller. An added tab or pane is cleared when main's layout names it, not by its reply. That way no spawn site needs new code, and a spawn that fails keeps its pane and error on screen, as today. A close is cleared by its reply, because removals are where an older update would show the closed tab or pane again.

Rejected: keeping the window's own tab and layout writes in drag-out (they raced main's update), and adding a pending flag at each of the 28 createTab callers (createTab itself is the one place they all go through).

Linked Issue

STA-9417 (fixed by the core as a unit).

Visual Proof

N/A. This is a stacked core step. The parity check below records the visible behavior.

Testing

  • pnpm tc passes. pnpm run check:code-quality:changed passes. Renderer, shared and tests/e2e/*.unit suites pass, apart from failures caused by this machine's setup (fish config, pg), which C4 also hits.
  • New terminal-pending-panes.test.ts (real store): a new tab survives an update that doesn't include it yet, and goes away when main later drops it. A closed tab doesn't come back from an older update, whether the reply or the update arrives first. A refused close shows main's copy again. A closed pane stays hidden until the update holding the close arrives.
  • Rewritten terminal-pane-tab-detach-commit.test.ts (real store): drag-out shows the new tab once when main's update arrives before the reply, and also when the reply arrives first. A pane closed during the move closes main's new tab, and it never appears. A sibling closed during the move closes the source tab without killing the PTY. A refusal shows the error toast. A repeated drop is ignored.
  • terminal-mounted-layout-reconcile.test.ts: a pending pane is kept, and a pane closed here isn't re-added from an older layout.
  • Parity (pnpm run test:e2e:terminal-layout-parity -- --base 80d45095d2ff --head HEAD, background launch): all 9 scenarios identical to main, including drag-out-to-tab, which failed on C3 and C4.

Known gaps owned by later steps:

  • C5b: adopting an agent's live PTY onto a pane that no surface claims no longer writes the pane's PTY id locally. Main needs to record that attachment itself, as part of splitting each PTY-binding site into its live-tracking and layout halves. Until then, such a pane in a split starts a new shell instead of reattaching.
  • A tab closed because its process exited doesn't send main a close request (main closes it itself), so an older update can show it again briefly.

…topology

Core step C5a (plan B2-6a). A tab or pane this window creates or closes is
pending until main's topology catches up, so the mirror and the pane
reconciler show it ahead of main without letting an older push undo it:

- createTab marks the tab pending; a pane the window makes that main's
  layout doesn't name yet is pending. An add ends when a slice names it.
- Every tab or pane close goes through commitTerminalSurfaceClose, which
  hides it until the mirror holds main's reply publishSeq; a refused close
  shows main's copy again.
- The reconciler keeps pending panes and never re-inserts a closed one;
  this replaces the "spawn still pending" check and trackRetiredLeafIds.
- Drag-out to a new tab waits for main's move and its push, then only
  places, activates and moves the live attachment. It no longer creates
  the tab or writes layouts itself, so a push that beats the reply no
  longer closes the new tab. A pane main doesn't hold keeps the local move.
- replaceTerminalLayoutPanePtyId is deleted: leaf bindings come from main.
A web client's tabs and a paired Orca server's (`runtime:`) worktree tabs
come from the host's snapshot, not main's topology push, so nothing ever
settled a pending tab add made there, and a close there waited on a push
main never sends. One rule now decides it: isTerminalTabMirroredFromMain
(not a web client, not a runtime-owned worktree, not a runtime-hosted
tab) gates the tab add, the pane add, and the mirror's keep. Main's reply
names no publishSeq for a worktree it publishes no slice for, so such a
close settles on the reply.

A pane split in a host tab is still kept until the host lists it: the
host-snapshot sync retains a leaf the window's layout names and the host
cannot resolve yet, so the layout keeps naming it (regression test added).
@Jinwoo-H

Jinwoo-H commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Closing: the layout rework is being redesigned so the runtime holds the only copy of the layout and every window/client is a view. This branch is kept for reference.

@Jinwoo-H Jinwoo-H closed this Oct 8, 2026
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