Skip to content

refactor(terminal): panes follow main's layout for every tab (core C4 of 7) - #26301

Closed
Jinwoo-H wants to merge 6 commits into
topo-c3-mirror-applyfrom
topo-c4-reconciler
Closed

Jinwoo-H wants to merge 6 commits into
topo-c3-mirror-applyfrom
topo-c4-reconciler

Conversation

@Jinwoo-H

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

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​269 $\color{#cf222e}{\Huge{\mathbf{−}}}$​85 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​184
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​93 $\color{#cf222e}{\Huge{\mathbf{−}}}$​132 $\color{#cf222e}{\Huge{\mathbf{−}}}$​39

ELI5

Since C3 the window keeps a copy of main's terminal layout, but the panes on screen still only changed when the window itself split or closed something. With this PR the panes on screen follow that copy. When main adds, removes or resizes a pane, the window does the same.

This is core step C4 of 7 in the terminal layout refactor. The core steps are reviewed as separate stacked PRs and merged together as one unit, so main never gets a half-finished version. This PR is stacked on C3 (#26293) and also contains C1 (#26151) through a merge, because the window can only follow main's layout once main builds the same tree the window does (C1's placement).

What Changed

Step 0, on C1's branch (#26151): when main builds a new tab from the window's placement, it no longer writes the spawn's working directory into startupCwd. That field now comes only from the row the window sent. Before, it was harmless because the window's save overwrote it. Now that main's copy is the one the window shows, it would have added startupCwd to every tab. A tab with no placement row is unchanged.

The pane reconciler now runs for every tab. Before, it ran only for web clients and for tabs on a remote Orca server (isHostAuthoritativeLayout). That check and its gate are deleted. For each layout change it now:

  1. Removes mounted panes whose leaf main dropped, by detaching them (detachPaneForExternalMove, the same path drag-out uses). Detaching never kills the terminal, so a terminal main moved to another tab keeps exactly one owner of its output. A pane that is still starting its terminal (no transport yet, or a spawn still waiting for its id) is kept, because main hasn't bound it yet. The old rule waited for the pane's terminal to exit first, which only made sense when removing meant closing.
  2. Inserts leaves main added. A leaf with a terminal attaches to it. A leaf with no terminal now spawns through the normal pane mount, which sends a split placement that main accepts for a leaf it already has. Before, such a leaf was skipped. Removals run first, so new panes anchor on panes that stay.
  3. Applies divider positions and split orientation in place (applyLayoutGeometry from refactor(terminal): add in-place pane layout geometry apply (unused) #25671, which had no caller), without remounting panes and without reporting a layout change, so nothing is echoed back. A divider the user is dragging is left alone; the drag's end decides.
  4. The reconciler no longer writes the layout back after an insertion.

The active pane is only reset when panes were added or removed, as before, so a title or ratio update never moves focus.

Named change: web and remote clients now follow desktop pane sizes. Because the same reconciler serves web clients and remote-server tabs, dragging a divider on the desktop now moves it on a paired web client too. Before, only a pane added or removed reached them (owner decision D-core-4).

Why

The core makes main the one owner of terminal layout. C3 put main's layout in the window's store; this step makes the panes on screen match it, which is what fixes the cases where the window and main disagreed (the split-right-down and CLI split shapes in C3's parity report). I reused the existing pieces instead of adding new ones: the existing insertion planner, the drag-out detach path for removal, and the B2-2 geometry apply. The pane-is-starting check reads the transport's existing isConnectPending, so there is no new state.

Alternatives I rejected:

  • Closing removed panes (the old path). Closing kills the terminal, which is wrong when main moved it.
  • A new "pending panes" store in this step. That is C5's job, together with turning the window's own writes into main calls; until then the transport already knows which panes are still starting.
  • Keeping the explicit write-back after insertions. The store already holds the layout, so writing it back only produced an echo.

Linked Issue

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

Visual Proof

N/A. This is a stacked core step; the user-visible fix shows once C5 to C7 land. The parity check below records what this step changes.

Testing

  • pnpm tc passes. pnpm run check:code-quality:changed passes. Renderer, shared and persistence unit suites pass (the only failures are local-environment ones that also fail on C3: fish config, ORCA_BACKGROUND_LAUNCH, pg).
  • New terminal-mounted-layout-reconcile.test.ts (happy-dom, real PaneManager):
    • an inserted leaf with no terminal spawns once, with a split placement on its neighbour; a bound one attaches to its terminal;
    • the reconciler persists nothing itself (only the split's own layout callback fires);
    • a removed leaf is detached, not closed; a pane still starting, or one the layout never named, is kept;
    • removals run before insertions;
    • a ratio change is applied in place with no remount and no layout callback; a divider mid-drag is left alone;
    • a terminal main moved to another tab is never killed and has one output owner, in both orders (source first, destination first), using real IPC transports.
  • Updated terminal-live-layout-reconciliation.test.ts (the host-only predicate's tests are gone) and web-session-split-pane-retirement-fences.test.ts (removal now waits for "starting", not for the terminal to exit).
  • Step 0: a placed new tab takes startupCwd only from the window's row; a tab without placement keeps today's value.
  • Parity (pnpm run test:e2e:terminal-layout-parity, background launch, base = the stack's main merge base 80d45095d2ff): create-tab, split-right-down, close-pane, close-tab, reorder-panes, cli-split, restart-restore and folder-workspace are identical to main. The startupCwd and split-shape differences C3 reported are gone.
    • Remaining: drag-out-to-tab fails on this step, as it did on C3. Main's push reaches the window before the move's reply, so the reconciler has already detached the pane, and the window's own drag-out code then can't find it and closes the new tab. Owner: C5, where drag-out waits for main's move and drops its own tab and layout writes.

Known gaps owned by later steps:

  • C5: until the window's own new-tab, split and close actions become main calls with pending panes, a push from main that is older than a local close can briefly put the closed pane back.
  • C6: a divider drag is protected while in progress, but not between drag end and main receiving it (C6's setLayout with its publishSeq).
  • C7: the window still saves the full layout; the store write that a reconciler split triggers becomes presentation-only there.

…or every tab

The pane reconciler ran only for web clients and remote-server tabs. It now
runs for every tab, so a desktop window follows main's layout:

- a leaf main dropped is detached, never killed, so a PTY main moved to
  another tab keeps one output owner; a pane still starting its PTY is
  kept until main binds it;
- removals apply before insertions, and an inserted leaf with no PTY
  spawns through the normal mount;
- divider ratios and orientation apply in place, so web and remote
  clients now follow desktop pane sizes; a divider mid-drag is left alone;
- the write-back after insertions is gone.
@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