Commit Graph
93 Commits
Author SHA1 Message Date
Jinwoo Hong 7cabd3a8dc Layout groundwork (switched off): every question about which pane holds 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.
2026-10-22 17:28:42 -04:00
OrcaWinandOrcaWin 5dbd23364b perf(layout): index records once while patching session lists (#31935)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:20:02 -07:00
Jinwoo Hong f8aa6854c7 A phone's new terminal lands after the tab it names, and its split selects the moved tab as the window does (dark, layout-owner build only) (#31924)
* fix(session-tabs): a tab main's groups lack keeps the place its writer listed it, so a headless phone lists a chat first (layout-owner build, L283)

On a host with no window, a rebuild writes its rows under a group id main's layout lacks. The row
writer's reorder treated every row of that group main does not hold (a chat, a browser page) as a
stray and appended it to the active group, so a chat the user moved first came back last, in both
the groups and the tab list. Strays now go after the id they follow in the writer's order (else
first), the same rule a group main does hold already used; the window publication path shares it.

* fix(session-tabs): a phone's split move and its create after a split tab go through main's layout on a windowless host (layout-owner build, L288, L289)

On a host with no window, a phone's split move only rewrote the runtime's own rows, and its new
terminal anchored after a split tab reached main's layout with no anchor. Every write of the rows
takes main's groups and order, so the split collapsed back into one group and the new tab landed
after the trailing tab. The split is now main's splitGroup command (the moved tab selected in its
new group, as a desktop drop), and the create passes the anchor's tab to main's createTerminalTab,
as a window's create does. An older phone sends no anchor and still gets the end.

* test(session-tabs): type the split move's async reply in the dispatch stub, and read an optional group map (L289)

* fix(session-tabs): a phone's split goes to main's layout only for a tab main holds, as the window sends it (layout-owner build, L289 review)

With the Store's write notices, today's runtime-only split already reached main's groups through
the session write; what it lost on a windowless host was the selection: the moved tab and its new
group did not become the active ones, as they do after a window's drop. The split stays main's
splitGroup for a tab main's groups hold, with the window's checks: a drop main's groups make a
no-op sends nothing (the window's pane-column rule, now in shared), and a refusal is dropped
silently. A row main does not hold (a chat or browser tab) splits as today, with no error. The
create anchor and its layout-owner hand-off move to their own module. The surface test fixture's
session writes now notify listeners, as the Store's do.

* chore(session-tabs): log when main refuses a phone's split, as the window logs its own refusals

* fix(session-tabs): a phone's terminal created after a chat or browser tab main holds lands after it, in its group (layout-owner build, L288 follow-up)

Since browser and chat tabs went main-first, main's layout holds them, but a windowless create
handed main the row id (agent-session:<id>), which is not main's tab id, so main put the new tab
at the end of its first group. The anchor is now resolved to main's own tab, as a window's create
names it; main's read of a workspace's layout is one method the tab-move path shares.

* fix(session-tabs): one move path: main's tab move selects a split tab in its new group and skips the window's no-op split (layout-owner build, L289)

A phone's split now goes only through main's tab move (#31812); this branch's own split routing is
gone. For a split only, main's tab move now does what the window's drop does: it sends nothing for
a drop that changes nothing (the window's pane-column check, reused), and after main applies the
split it selects the moved tab and makes its new group active. Refusals keep that path's codes.

* fix(session-tabs): resolve a create's anchor to main's tab inside the tab-move module, so only the old-client boundary's listed importers read old-client rows (L288 follow-up)

* perf(session-tabs): read main's layout for a create's anchor only when main places it, so switch-off and appending phones skip that load

* fix(session-tabs): restore the row-to-tab lookup the split selection and create anchor use, after #31925 dropped its import (layout-owner build, L288, L289)

#31925 removed the L280 bridge in main's tab move, and with it the move's last use of
resolveSessionTabRow, so it dropped the import; this branch's split selection and create-anchor
lookup still resolve a row to main's tab through it. A row main's layout does not hold is now
refused by main's move (tab_not_found) as #31925 rules; the phone test asserts that.

* test(session-tabs): the L283 phone-order test opens its chat as the host does and uses a store that announces its writes (layout-owner build)

Since #31925, main moves every tab of a workspace it owns and refuses a row its layout does not
hold, and since D3b a chat the host opens is saved in main's layout. The test now opens the chat
through the host's publish path and gives the runtime a profile-like store (partitions listed,
writes announced), so the reorder reaches main's layout and its commit reaches the rows.

* test(session-tabs): type the L283 test's profile-like store as the Store contract declares it

* revert(session-tabs): drop the stray-placement change; main's layout holds chats and moves every row now (L283)

Since D3b a chat the host opens is saved in main's layout, and since #31925 a phone cannot move a
row main does not hold, so the rebuilt-group stray this placed no longer reaches a phone; the
remaining strays are brief, and appending them gives the same order. withStrayIn and its callers
return to main's behaviour, with its unit test. The phone-order spec stays as a guard that a
windowless chat moved first stays first after a rebuild.
2026-10-22 13:12:56 -04:00
Jinwoo Hong ea60ec5d7c Layout-owner builds: a phone's tab move is always the server's own; no ghost editor tabs after restart (#31925)
* Layout groundwork (switched off): main derives agent tab titles from its own hook status

With the layout-owner switch on, main gives agent tabs their generated titles from
the hook-server status it owns, on the desktop, headless serve and orcad: one
renameTabs per status batch, so one write and one frame. It keeps the window's
rules (setting, custom and quick-command titles, first title wins, dispatch labels).
The window stops sending generated titles for tabs main holds; a paired server's
tab, or one main has no tab-bar entry for, is still titled by the window as today.

The title rules move to src/shared (agent-row-primary-text, and the new
agent-generated-tab-title-rules) so both sides share them; switch-off behaviour
is unchanged.

* feat(layout): main applies a phone's or paired client's tab move itself, and a tab move names the tab it follows so a stale index can't misplace it (layout-owner build, L193)

* fix(layout): a move in a group showing a tab main lacks goes the window's way, a twin's agent tab keeps its window title, and orcad subscribes titles through the store attach (layout-owner build)

* chore(layout): main's tab move reaches the old-client translation through its compat entry

* chore(layout): mark the window-only-row move fallback as bridge code (ledger L280)

* test(layout): attaching the hook store follows its rows for main's tab titles once

* chore(layout): the old-client move translation is a compat entry of its own, with no cyclic re-export

* fix(layout): every tab a group shows is main's, so a client's tab move never falls back to the window (layout-owner build)

- Main moves a client's tab itself in a workspace it owns; a row it holds no tab for (a combined
  diff) keeps no place, and an order must still name each row shown once (L280).
- The window publishes an editor row under its tab id only; a file no group shows gets none.
- Main's layout load gives a saved browser record with no tab its tab, as the window's hydration
  repair did, and the window no longer mints one for a workspace main owns.
- Main's layout load drops the saved tabs the window's restore drops (view-less diff, conflict and
  check tabs, old html previews), through one shared rule; the next save writes it back (L286).
- Main lists conflict-review and check-details tabs as the plain file rows the window lists, so
  older clients keep them once the window's rows are gone (L287).

* fix(lint): drop an import the shared restore rule left unused

* test: import the move translation through the folder's public path

* fix(layout): review follow-ups for the move fallback removal (layout-owner build)

- A transient tab stays at load while a view is held for its file, as the window's frame build
  keeps it, not only for its own tab id.
- The repaired browser tab goes to the saved active group only while it holds a tab, as
  selectHydratedActiveGroupId picks it; tests that the id is stable across loads and restarts,
  that a window-owned build restores it once, and that a paired server's record keeps the window's
  repair.
- Two switch-on tests keyed editor drafts by path; drafts are keyed by editorFileKey (an owned id
  in a layout-owner build), so they now use it.

* fix(layout): the repaired browser tab skips a saved active group this load empties

* test(layout): directly dialed SSH browser record is main's; a server's SSH workspace keeps the window repair

* fix(layout): drop view-less editor tabs only in loads that carry main's views, so main's own edits keep a live diff

* test(layout): a direct SSH partition gets the same browser repair and drop with no host contact
2026-10-22 05:26:12 -04:00
Jinwoo Hong 4165c9545c Layout groundwork (switched off): main tells the window whether its start resumed a pane's agent, so the restored banner and reopen count come back (#31908)
* feat(terminal): the window's store shows main's pane start status (layout-owner build)

A status-only frame changes one store entry and no pane tree, so the pane's view stays mounted.

* refactor(terminal): keep the frame sync within its line budget

* feat(terminal): every pane main starts shows 'starting' from before it is written (layout-owner build)

* feat(terminal): a pane main is starting waits and attaches; a failed start shows why (layout-owner build)

Fixes owner-visible row 15: a failed agent or setup start no longer opens a plain shell.

* test(terminal): iterate store listeners directly

* fix(terminal): an exit that reports no code keeps its pane, as lost contact (layout-owner build)

* test(terminal): a pane main started attaches to main's terminal at its own grid

* test(terminal): detach census tests assert main's move and frame with the switch on

* fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)

* fix(terminal): main releases a painted cold restore only for the pane's current terminal (layout-owner build only)

* test(terminal): typed ack args in the painted cold-restore test

* test(terminal): exited-pane tests assert main's close with the switch on

These 8 have failed switch-on since #31130 moved the exit close to main.

* feat(layout): main starts the panes a client's startPane or wake names, through its one restored-pane starter (layout-owner build only)

The model's start and stop effects reach the runtime once the command is written, never the
reply. startPane starts a pane whose binding has a start status, and a slept pane from its record.
A wake keeps a pane's resume record until its start binds; a failed start keeps it and shows why.
With the switch on, main owns a running agent's wake even with a window open.

* fix(layout): a window's wake of a pane it resumed itself is record-only, so main does not start it again (layout-owner build only)

* test(layout): a client's startPane starts the pane before any other trigger

* Revert "fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)"

This reverts commit a646d89b26.

* test(terminal): the cold-restore repaint ack names its pane in the layout-owner build

* fix(layout): a wake keeps the pane's resume record in the pane-owned form, so a host crash resumes the agent again (layout-owner build only)

* test(layout): a failing start in the pane-start runner test throws

* feat(terminal): main writes and starts every split of a pane it holds; the window attaches (layout-owner build)

The window's split, a startup split with its command, and a CLI split all go through main's
terminal.split; main's frame mounts the pane, with or without a window intent. Deletes the
window's split wait and withdraw, the split appear intents, and the queued-split skip and cancel.
The e2e helper uses the real split. A startup split leaves focus on its source pane.

* fix(terminal): the attach-only wait leaves sleeping, resuming and held panes to their restore path

* fix(terminal): a split pane attaches to the terminal main bound it to; a taken pane id is refused

Also restores the proof that a split closed while it starts gets one answer and leaves no
pane or start status behind.

* Reapply "fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)"

This reverts commit 420e605ad0.

* fix(layout): a trigger between a restored pane's start and its bind write joins that start, not a second one (layout-owner build only)

* fix(terminal): a window split main cannot start still shows its pane, failed, with why

* fix(terminal): keep the switch import the D1b-1 merge dropped from the layout restore

* feat(layout): main's starter shows a restored pane starting before any await, and a failed start with its reason; startPane starts a pane whose bound terminal ended (layout-owner build only)

* feat(terminal): a window's attach-only view of a pane attaches to its terminal or starts nothing, keeping the binding for main's start (layout-owner build only)

* feat(terminal): a restored pane with no live terminal asks main to start it and attaches once main binds it; the window's wake, hibernated wake, restart-on-show and gone-terminal fallbacks route through main (layout-owner build only)

* fix(terminal): check a taken pane id first, keep the start's own error, clear a gone pane's status

Three test stores gain the pane-start field the window's store always has.

* test(terminal): the admission stub answers whether its pane is still written; a closed pane's failed start clears

* test(terminal): with the switch on, the window's restores, wakes and restart replacements ask main to start the pane and attach to the terminal main binds, starting and resuming nothing themselves

* refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module

* fix(terminal): a held pane connects as today so main's refusal holds it; main's start follows output; a capacity death after main's resume keeps the resume (layout-owner build only)

* fix(layout): main passes its start options only for commands that start panes; a windowed open resumes the agent under main (layout-owner build only)

* refactor(terminal): break the pane start module cycle; typed test fixtures

* test(terminal): a pane main wrote and the user closed mid-start drops its failed status

* fix(terminal): every waiter on main's start runs, a failed or refused start settles and is asked again on the next view, a hidden gone pane waits for its view, a twin main hands back stays on today's path, and lost contact never starts a pane (layout-owner build only)

* test(terminal): iterate the store listeners directly

* test(terminal): a cold-restore paint applies no window resume with the switch on

* fix(terminal): a split main starts is listed to phones and session.tabs without a window's report

* test(terminal): name the main pane start test helper as a fixture

* fix(terminal): a reconnect shows its pane starting and why it failed; a lost owner race shows no failure; a split no one asked to see keeps the phones' tab

* fix(terminal): a kept create another owner won shows no failure; the window attaches to the winner

* fix(terminal): a pane main starts at relaunch keeps its full history and starts before the window's graph lists its tab (layout-owner build only)

* feat(terminal): main replaces the empty shell a hibernated agent left and resumes the agent when a view asks; a start main could not attach shows main's own terminal_not_started (layout-owner build only)

* test(terminal): main seeds its copy of a restored pane even when a window declared one, with the switch on

* fix(terminal): a pane main started hidden paints once when its view attaches (switch-on builds only)

The view's attach snapshot, taken after main unmarks the hidden gate, already holds the bytes main
dropped, so the reveal no longer asks a second restore from main's buffer.

* refactor(terminal): main's pane start reads the pane from its own layout, not the client projection (switch-on builds only)

* fix(terminal): a pane restart stops its terminal before writing the pane unbound, so a stop that fails keeps the pane (switch-on builds only)

* test(terminal): a start main could not bind shows terminal_not_started through the window's wait, from one copy site (switch-on builds only)

* fix(terminal): main's pane start resumes an agent recorded under the pane's number, as the window did; stale switch-on tests show main's start and the known banner loss (switch-on builds only)

* fix(terminal): review round: a visible view alone retires main's hidden-start drops, by episode; main reads a split pane's numbered record by its number; the window keeps a shell main kept (switch-on builds only)

* fix(terminal): main's start of a pane a window view is attaching to leaves it visible, so the view paints once (switch-on builds only)

* fix(terminal): a window view that attaches while main starts its pane takes main's start, with no second provider attach (switch-on builds only)

* fix(terminal): review round: a view takes main's start only when it carries a payload and still owns its terminal; attaching views are counted, keep main's pre-spawn mark off, and a failed attach hides only what it kept visible (switch-on builds only)

* test(terminal): checked test doubles in the view-join and numbered-record tests; reattach options fit the file budget

* test(e2e): with the switch on, the mount-authority spec keeps the window's bindings, which only main's frames write

Claude-Session: W5b

* test(e2e): the mount-authority spec fits its line budget; the layout-owner build check is one helper

Claude-Session: W5b

* test(layout): an unbound pane holding a resume record starts and keeps the record, since main's start resumes from it

Claude-Session: W5b

* fix(terminal): a pane main resumed holds a running agent's record, so it reads awake, not sleeping (switch-on builds only)

Claude-Session: W5b

* fix(terminal): in a split where no pane owns a numbered record, main resumes the tab's one conversation in pane 1 only (switch-on builds only)

Claude-Session: W5b

* fix(terminal): main marks only the record a resume started from live, never a sleep captured after it (switch-on builds only)

Claude-Session: W5b

* test(terminal): with the switch on, a windowless resume leaves the pane's record a running agent's

Claude-Session: W5b

* test(terminal): the resumed-record test double typechecks

Claude-Session: W5b

* refactor(terminal): with the switch off, main's pane start reads no resume record for the live mark

Claude-Session: W5b

* fix(terminal): main publishes whether its start of a pane resumed the agent, for the window's banner (switch-on builds only)

Claude-Session: W5b

* fix(terminal): a pane main started shows its "session restored" or "resume unavailable" banner again (switch-on builds only)

Claude-Session: W5b

* test(terminal): the resume-banner store double carries no type assertion

Claude-Session: W5b

* fix(terminal): main records a resume outcome only when its start wakes the pane's own sleeping record, and each new start clears the old one (switch-on builds only)

Claude-Session: W5b

* fix(terminal): a renderer reload shows no resume banner again for a pane main started, and the window stops waiting once that pane holds another terminal (switch-on builds only)

Claude-Session: W5b

* refactor(terminal): the merged files fit their line budget: the folder-fallback notice and a hidden pane's wait for its first view are helpers, and the answering-transport lookup moves to its one test caller

Claude-Session: W5b

* fix(terminal): main's resume outcome arrives in the same frame as the terminal it names, a same-id respawn included (switch-on builds only)

Claude-Session: W5b

* fix(terminal): the reopened-terminals toast counts agent sessions main resumed again (switch-on builds only)

Claude-Session: W5b

* refactor(terminal): the attach's resume check names the window's resume type

Claude-Session: W5b

* test(terminal): pin the resume outcome's wire contract: an outcome alone rebinds nothing, it shows once per start, and no saved session holds it

Claude-Session: W5b

* refactor(runtime): the window's layout apply fits the runtime's line budget after the main merge

Claude-Session: W5b

* test(terminal): any create through main that wakes a pane's record publishes its outcome, and the sidebar resume keeps its restored banner (switch-on builds)

Claude-Session: W5b

* refactor(runtime): the layout stream takes main's pane facts from one source, keeping the runtime in its line budget

Claude-Session: W5b

* fix(terminal): a relaunch resume into a pane kept under its saved terminal id shows its banner again, and a start whose bind fails leaves no outcome (switch-on builds only)

Claude-Session: W5b

* fix(terminal): the reopen toast counts only a resume main made for the attached terminal since the attach began, and the banner reads main's outcome once at attach (switch-on builds only)

Claude-Session: W5b

* test(terminal): two pane test stores carry main's resume outcomes, which every attach now reads

Claude-Session: W5b

* test(terminal): the hidden-output repro's test store carries main's resume outcomes

Claude-Session: W5b

* fix(terminal): an SSH same-id resume whose bind writes nothing new keeps its banner, and a start that lost its pane leaves the winner's outcome (switch-on builds only)

Claude-Session: W5b
2026-10-22 04:35:42 -04:00
Jinwoo Hong 01db1b89af Main titles agent tabs and applies phone tab moves itself, and tab moves name the tab they follow (dark, layout-owner build only) (#31812)
* Layout groundwork (switched off): main derives agent tab titles from its own hook status

With the layout-owner switch on, main gives agent tabs their generated titles from
the hook-server status it owns, on the desktop, headless serve and orcad: one
renameTabs per status batch, so one write and one frame. It keeps the window's
rules (setting, custom and quick-command titles, first title wins, dispatch labels).
The window stops sending generated titles for tabs main holds; a paired server's
tab, or one main has no tab-bar entry for, is still titled by the window as today.

The title rules move to src/shared (agent-row-primary-text, and the new
agent-generated-tab-title-rules) so both sides share them; switch-off behaviour
is unchanged.

* feat(layout): main applies a phone's or paired client's tab move itself, and a tab move names the tab it follows so a stale index can't misplace it (layout-owner build, L193)

* fix(layout): a move in a group showing a tab main lacks goes the window's way, a twin's agent tab keeps its window title, and orcad subscribes titles through the store attach (layout-owner build)

* chore(layout): main's tab move reaches the old-client translation through its compat entry

* chore(layout): mark the window-only-row move fallback as bridge code (ledger L280)

* test(layout): attaching the hook store follows its rows for main's tab titles once

* chore(layout): the old-client move translation is a compat entry of its own, with no cyclic re-export

* test(config): the boundary message test names the old-client folder's configured entries
2026-10-22 02:24:37 -04:00
99d18f7a2f Browser, chat and simulator tabs main-first: the window asks main for each change and draws only main's update (dark, layout-owner build only) (#31516)
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)

A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.

Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.

* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)

* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)

* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)

* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)

* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)

* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)

* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)

* wip(d3a-2): a windowless host lists a file main opens on session.tabs at once

* wip(d3a-2): the record key's fields are declared where the key is made

* wip(d3a-2): main publishes an editor tab's view, and a window rebuilds the open file it lacks from it

Main's in-memory editor view now carries the file facts an open file needs; it rides the tab row
(PublishedTab.editorView) and a frame build rebuilds a diff, conflict, preview or check-details tab.
Inert with the switch off: views are set only by layout.apply (switch builds) and read only in frame-sync.

* wip(d3a-2): a switch-on window sends editor opens, closes and promotions to main and draws the tabs from its frames

Layout-owner build only; the switch-off path is unchanged. An open proposes the tab id and sends
openEditorTab with the edit file's record or the tab's view (a reopen also its index); the open
file is written at once and the tab is selected once its frame draws it. Callers that act on the
tab (focus request, link reveal, group focus) wait for that frame through layout-owner-pending-tabs.
A close sends closeTabs; the tab goes with the frame and the file's content after it, and a
refusal keeps both. A promotion sends promotePreviewTab and the frame clears the flag. Deleted:
the drawn editor senders, their before/after captures, the editor drawn-tab holds and the
window-record keep rule (a transient tab is kept by its frame view).

* wip(d3a-2): a switch-on close keeps its selection and a reopen, a link names its group, and main stamps an open's time

Layout-owner build only. After main's frame drops a closed editor tab, the window selects the
group's previous tab and its surface through activateTab and focusGroup, after closeFile's
content cleanup rather than before it; a reopen sent while the close was in flight keeps the
file. openFileForLink's proposed view names the group the open asked for. The window's
openEditorTab sends no createdAt: main stamps the tab's time (ledger L186). Preview promotion
writes no tab itself: the window ends a preview only where a paired server owns the layout.

* wip(d3a-2): editor tests assert switch-on tabs through main's command and frame (test only)

Each store and hook test that assumed an immediate editor-tab draw now runs with main's model
through the T1 transport in apply mode and its frames (tabs-drawn-from-main.test-fixture), and
asserts the tab, its selection and its content after main's frame. The fixture fills the frame
slices a partial editor-and-tabs store lacks.

* wip(d3a-2): main takes a restored move as a command, renames a view tab's path, and keeps one tab per file per group

Layout-owner build only (the switch gates every change; shipped builds unchanged). moveTabToWorkspace no
longer skips a tab "the window's save got there first" (L160): window saves no longer carry the move, so
a repeat is tab_not_found; tab_exists and invalid_params stay. A move or rename may carry a non-plain
tab's new view (its file under the new path or owner), which replaces editorViews[tab] (L180). Main
refuses a moveTab or moveTabToWorkspace that would put a second tab for a file into a group (tab_exists),
and its loader folds a saved session's duplicate into the first, as the window's restore does
(dedupeEditorTabsWithinGroups): the window's frame dedupe hid the second, which no close could reach.

* wip(d3a-2): a switch-on restored move and disk rename are main's commands, and the frame that shows them rekeys the content

Layout-owner build only; the switch-off path is unchanged. A restored editor tab re-owned by its
file's workspace (L157, L158) sends moveTabToWorkspace (a re-own in place sends renameEditorFiles)
under the ids and group the window proposes, with each view tab's new view; a disk rename (L180)
sends renameEditorFiles per workspace and draws nothing first. The window's content rekey (open
file records, drafts, buffers, the editorBuffers echo gate, pendingLiveDiskVerification and
pendingSelfMoveEcho) waits in layout-owner-content-rekeys and the frame flush that first shows main's
change applies it, in the same store update as the frame (a flush now writes one patch). A change no
tab shows is rekeyed on main's answer; a refusal drops it and changes nothing. The move coordinator
and the restored-owner loader wait for that outcome; a refused rename rolls the disk move back.
A rename picks a runtime-owned or floating file's owned id, as main's frames restore it.

Deleted: recordRestoredEditorMoveInLayoutOwner, recordRenamedEditorFilesInLayoutOwner, the
reparent's before/after capture and the drawn-tab holds for moves and renames. Tests that assumed
the window moves or renames first now run through main's T1 transport in apply mode and its frames.

* wip(d3a-2): the static gate's casts and layout-writer ratchet hold for the switch-on move, rename and gap fixes

Layout-owner build only. The content rekey's pure store change stays in rekey-open-files-action
(its writer count drops 4 -> 2, pruned); a twin tab main routes to a paired server ends its
preview through patchTab, the existing tab writer, instead of a new one; a frame-only editor tab
takes its label through the frame build's tab-label facts, read from its view or file record; the
moved file's activation names the moved tab, writing no surface field. Test casts are typed or carry
a SAFETY note.

* wip(d3a-2): the switch-on rename takes the window's pure rekey as an argument, so the two modules do not import each other

Layout-owner build only; no behaviour change. Clears the static gate's no-cycle finding.

* wip(d3b): a switch-on window sends browser, chat and simulator opens, closes and page changes to main and draws them from its frames

Main places the chats its host lists (twin h), a local cancel keeps no tombstone (L155), and the
listing no longer writes chat tab structure where frames draw it (L153).

Co-Authored-By: Claude <noreply@anthropic.com>

* wip(d3b): a switch-on browser, chat or simulator close waits for main's frame, lands an emptied workspace as today, and its tests read main's frames

* wip(d3b): the stale-listing and cross-version chat tests check a switch-on local cancel through main, with a presence precondition and per-test resets

* wip(d3b): main places a tab whose group it lacks in the workspace's first group, not beside the window's anchor (L150)

* wip(d3b): tests for main's chat tab commands from a host's chat changes

* wip(d3b): main's windowless chat tab and chat fork tests apply a switch-on host's chat tab through main's layout

* wip(d3b): the switch-on close and open keep their layout writes in the files the writer ratchet counts them in, and a drawn browser close carries its position instead of a rebuilt layout

* Editor tabs main-first: the window sends each editor action to main and draws editor tabs only from main's frames (dark, layout-owner build only)

With ORCA_LAYOUT_OWNER_IS_MAIN on, editor open (preview replace, open to the side), reopen at
its position, close, close-all, promote, the resolved conflict placeholder, a restored-editor
move and a disk rename are layout commands the window sends before it draws anything. Window
content (open file records, drafts, buffers, the autosave gate) is written at once for an open,
and rekeyed in the same store update as the frame for a move or rename. Main publishes each
non-edit tab's view, keeps views in memory across applies and partition changes, stamps the
tab's time, folds same-file duplicates, and republishes a windowless host's session.tabs after
its own file open. The switch-off path is unchanged.

* wip(d3b): a switch-on close main answers workspace_not_found drops the tab's content as tab_not_found does, and placement reads a missing active-group map safely

* wip(d3b): switch-on browser and chat tests open and close through main's command and draw from its frame (test only)

* wip(d3b): a switch-on chat listing writes no tab structure of a workspace main's frames draw; a host chat title changes only the drawn tab's label

* wip(d3b): main's phone rows show a diff tab and a markdown preview as the window's do, with the preview's source named by the window's open-file id

* wip(d3b): the cleanup close's page-read guard counts its named linear passes, so a per-entry rescan still fails switch-on (test only)

* wip(d3b): the static gate's compat entry, focus-read ratchet and formatting hold for the switch-on test conversions and phone rows

* test(d3a-2): the buffer view index follows a rename main answers

* wip(d3b): placement reads the active-group map the real store always holds; the browser test store seeds it

* wip(d3b): a switch-on background browser open leaves the worktree's remembered browser tab unset, as with the switch off

* wip(d3b): a switch-on chat open selects the chat in its group without moving the focused group, as the window's own open does; its test counts the chats main draws

* wip(d3b): the converted simulator, launch-cancel, cleanup-close and chat-listing tests assert what the window's draws asserted, through main's command and frame (test only)

* wip(d3b): main fences a chat's create on its close even when the close fails for want of the chat host, so a cancelled launch never shows

* wip(d3b): a frame keeps the window's hidden chat launch slot after its neighbour, so a resumed chat keeps its saved place through main's frames

* wip(d3b): a declined chat's terminal is placed after the chat and pinned as it is in one main move, and shown before main closes the chat

* wip(d3b): a switch-on chat close the host refuses asks main to open the chat again, as the window's re-read shows it with the switch off

* wip(d3b): a paired server's workspace, its row naming the server, opens and closes browser and chat tabs without main's layout (test only)

* wip(d3b): a refused chat close comes back only while this machine's host still lists the chat, and a close main answers workspace_not_found removes the window-only tab

* wip(d3b): the twin chat close test names what each build stops: no host off, main's host on (test only)

* wip(d3b): the refused-close reopen takes only a chat agent it can launch

* wip(d3b): a frame's browser selection keeps an unchosen workspace's unset through the restore projection's own rule, not a second write

* wip(d3b): the simulator and paired-server tests type their mock stores without assertions (test only)

* wip(d3b): the switch-off declined-chat placement runs as before, with no wait; only the switch-on move waits for main

* chore(d3b): drop the frame-binding helper the main merge left without a caller

* chore(d3b): drop the import the deleted helper used

* wip(d3b): a chat this machine's window creates is placed only by the window's own open, so a reserved chat slot keeps its place (switch-on)

* wip(d3b): main reads a chat tab change against its layout when its queue reaches it, through the one rule the switch-off save uses, so a close right after an open closes it

* wip(d3b): a declined chat's terminal moves to its index among the tabs main draws, not past a hidden launch slot, so it still replaces the chat (switch-on)

* wip(d3b): main's phone-row comments say which rows the window's own row wins, and a test shows a window-listed diff keeps its dirty state (comments and test only)

* wip(d3b): a switch-on chat open returns only the tab's id and chat, the fields main's frame keeps; no caller reads more

* chore(d3b): the unit-routing census registers the browser suites' drawn-from-main store fixture as a support bridge (test only)

* chore(d3b): the chat tab rule's comment wraps and names its two callers (comment only)

* wip(d3b): main's chat tab queue asks a worktree's host only once it has a saved session, and one failed change no longer stops the ones after it (switch-on)

* wip(d3b): a chat tab batch whose changes cannot be read is logged, and the batches after it still land (switch-on)

* test(d3b): a CLI-chat resume's tab is opened once, by the window through main, after the tab before its hidden slot (test only)

* test(d3b): an SSH worktree has no host to run a chat on, so none of the chat open, close or fix paths reach it (test only)

* chore(d3b): the content tab placement reads folder workspaces through main's workspace-record selector, as the new ratchet requires

* fix(merge): put back main's f47f632 files that the merge took from a later main by mistake (no D3b change)

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 22:32:07 -04:00
Jinwoo Hong ed13ca1e6f Layout groundwork (switched off): main starts and restores every pane, and the window only attaches and paints the host's history (#31567)
* feat(terminal): the window's store shows main's pane start status (layout-owner build)

A status-only frame changes one store entry and no pane tree, so the pane's view stays mounted.

* refactor(terminal): keep the frame sync within its line budget

* feat(terminal): every pane main starts shows 'starting' from before it is written (layout-owner build)

* feat(terminal): a pane main is starting waits and attaches; a failed start shows why (layout-owner build)

Fixes owner-visible row 15: a failed agent or setup start no longer opens a plain shell.

* test(terminal): iterate store listeners directly

* fix(terminal): an exit that reports no code keeps its pane, as lost contact (layout-owner build)

* test(terminal): a pane main started attaches to main's terminal at its own grid

* test(terminal): detach census tests assert main's move and frame with the switch on

* fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)

* fix(terminal): main releases a painted cold restore only for the pane's current terminal (layout-owner build only)

* test(terminal): typed ack args in the painted cold-restore test

* test(terminal): exited-pane tests assert main's close with the switch on

These 8 have failed switch-on since #31130 moved the exit close to main.

* feat(layout): main starts the panes a client's startPane or wake names, through its one restored-pane starter (layout-owner build only)

The model's start and stop effects reach the runtime once the command is written, never the
reply. startPane starts a pane whose binding has a start status, and a slept pane from its record.
A wake keeps a pane's resume record until its start binds; a failed start keeps it and shows why.
With the switch on, main owns a running agent's wake even with a window open.

* fix(layout): a window's wake of a pane it resumed itself is record-only, so main does not start it again (layout-owner build only)

* test(layout): a client's startPane starts the pane before any other trigger

* Revert "fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)"

This reverts commit a646d89b26.

* test(terminal): the cold-restore repaint ack names its pane in the layout-owner build

* fix(layout): a wake keeps the pane's resume record in the pane-owned form, so a host crash resumes the agent again (layout-owner build only)

* test(layout): a failing start in the pane-start runner test throws

* feat(terminal): main writes and starts every split of a pane it holds; the window attaches (layout-owner build)

The window's split, a startup split with its command, and a CLI split all go through main's
terminal.split; main's frame mounts the pane, with or without a window intent. Deletes the
window's split wait and withdraw, the split appear intents, and the queued-split skip and cancel.
The e2e helper uses the real split. A startup split leaves focus on its source pane.

* fix(terminal): the attach-only wait leaves sleeping, resuming and held panes to their restore path

* fix(terminal): a split pane attaches to the terminal main bound it to; a taken pane id is refused

Also restores the proof that a split closed while it starts gets one answer and leaves no
pane or start status behind.

* Reapply "fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)"

This reverts commit 420e605ad0.

* fix(layout): a trigger between a restored pane's start and its bind write joins that start, not a second one (layout-owner build only)

* fix(terminal): a window split main cannot start still shows its pane, failed, with why

* fix(terminal): keep the switch import the D1b-1 merge dropped from the layout restore

* feat(layout): main's starter shows a restored pane starting before any await, and a failed start with its reason; startPane starts a pane whose bound terminal ended (layout-owner build only)

* feat(terminal): a window's attach-only view of a pane attaches to its terminal or starts nothing, keeping the binding for main's start (layout-owner build only)

* feat(terminal): a restored pane with no live terminal asks main to start it and attaches once main binds it; the window's wake, hibernated wake, restart-on-show and gone-terminal fallbacks route through main (layout-owner build only)

* fix(terminal): check a taken pane id first, keep the start's own error, clear a gone pane's status

Three test stores gain the pane-start field the window's store always has.

* test(terminal): the admission stub answers whether its pane is still written; a closed pane's failed start clears

* test(terminal): with the switch on, the window's restores, wakes and restart replacements ask main to start the pane and attach to the terminal main binds, starting and resuming nothing themselves

* refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module

* fix(terminal): a held pane connects as today so main's refusal holds it; main's start follows output; a capacity death after main's resume keeps the resume (layout-owner build only)

* fix(layout): main passes its start options only for commands that start panes; a windowed open resumes the agent under main (layout-owner build only)

* refactor(terminal): break the pane start module cycle; typed test fixtures

* test(terminal): a pane main wrote and the user closed mid-start drops its failed status

* fix(terminal): every waiter on main's start runs, a failed or refused start settles and is asked again on the next view, a hidden gone pane waits for its view, a twin main hands back stays on today's path, and lost contact never starts a pane (layout-owner build only)

* test(terminal): iterate the store listeners directly

* test(terminal): a cold-restore paint applies no window resume with the switch on

* fix(terminal): a split main starts is listed to phones and session.tabs without a window's report

* test(terminal): name the main pane start test helper as a fixture

* fix(terminal): a reconnect shows its pane starting and why it failed; a lost owner race shows no failure; a split no one asked to see keeps the phones' tab

* fix(terminal): a kept create another owner won shows no failure; the window attaches to the winner

* fix(terminal): a pane main starts at relaunch keeps its full history and starts before the window's graph lists its tab (layout-owner build only)

* feat(terminal): main replaces the empty shell a hibernated agent left and resumes the agent when a view asks; a start main could not attach shows main's own terminal_not_started (layout-owner build only)

* test(terminal): main seeds its copy of a restored pane even when a window declared one, with the switch on

* fix(terminal): a pane main started hidden paints once when its view attaches (switch-on builds only)

The view's attach snapshot, taken after main unmarks the hidden gate, already holds the bytes main
dropped, so the reveal no longer asks a second restore from main's buffer.

* refactor(terminal): main's pane start reads the pane from its own layout, not the client projection (switch-on builds only)

* fix(terminal): a pane restart stops its terminal before writing the pane unbound, so a stop that fails keeps the pane (switch-on builds only)

* test(terminal): a start main could not bind shows terminal_not_started through the window's wait, from one copy site (switch-on builds only)

* fix(terminal): main's pane start resumes an agent recorded under the pane's number, as the window did; stale switch-on tests show main's start and the known banner loss (switch-on builds only)

* fix(terminal): review round: a visible view alone retires main's hidden-start drops, by episode; main reads a split pane's numbered record by its number; the window keeps a shell main kept (switch-on builds only)

* fix(terminal): main's start of a pane a window view is attaching to leaves it visible, so the view paints once (switch-on builds only)

* fix(terminal): a window view that attaches while main starts its pane takes main's start, with no second provider attach (switch-on builds only)

* fix(terminal): review round: a view takes main's start only when it carries a payload and still owns its terminal; attaching views are counted, keep main's pre-spawn mark off, and a failed attach hides only what it kept visible (switch-on builds only)

* test(terminal): checked test doubles in the view-join and numbered-record tests; reattach options fit the file budget

* test(e2e): with the switch on, the mount-authority spec keeps the window's bindings, which only main's frames write

Claude-Session: W5b

* test(e2e): the mount-authority spec fits its line budget; the layout-owner build check is one helper

Claude-Session: W5b

* test(layout): an unbound pane holding a resume record starts and keeps the record, since main's start resumes from it

Claude-Session: W5b

* fix(terminal): a pane main resumed holds a running agent's record, so it reads awake, not sleeping (switch-on builds only)

Claude-Session: W5b

* fix(terminal): in a split where no pane owns a numbered record, main resumes the tab's one conversation in pane 1 only (switch-on builds only)

Claude-Session: W5b

* fix(terminal): main marks only the record a resume started from live, never a sleep captured after it (switch-on builds only)

Claude-Session: W5b

* test(terminal): with the switch on, a windowless resume leaves the pane's record a running agent's

Claude-Session: W5b

* test(terminal): the resumed-record test double typechecks

Claude-Session: W5b

* refactor(terminal): with the switch off, main's pane start reads no resume record for the live mark

Claude-Session: W5b

* refactor(terminal): the merged files fit their line budget: the folder-fallback notice and a hidden pane's wait for its first view are helpers, and the answering-transport lookup moves to its one test caller

Claude-Session: W5b

* refactor(runtime): the window's layout apply fits the runtime's line budget after the main merge

Claude-Session: W5b
2026-10-21 22:13:57 -04:00
Kelvin Amoaba ba8f237ade fix(mobile): phone terminal and chat tab names match the desktop (#31384) 2026-10-21 18:23:12 +00:00
Jinwoo Hong cfd9e9c82c Editor tabs main-first: the window asks main for every editor tab change and draws only main's update (dark, layout-owner build only) (#31438)
* Editor tabs main-first: the window sends each editor action to main and draws editor tabs only from main's frames (dark, layout-owner build only)

With ORCA_LAYOUT_OWNER_IS_MAIN on, editor open (preview replace, open to the side), reopen at
its position, close, close-all, promote, the resolved conflict placeholder, a restored-editor
move and a disk rename are layout commands the window sends before it draws anything. Window
content (open file records, drafts, buffers, the autosave gate) is written at once for an open,
and rekeyed in the same store update as the frame for a move or rename. Main publishes each
non-edit tab's view, keeps views in memory across applies and partition changes, stamps the
tab's time, folds same-file duplicates, and republishes a windowless host's session.tabs after
its own file open. The switch-off path is unchanged.

* test(d3a-2): the buffer view index follows a rename main answers

* test(e2e): the old-phone baseline records a windowless server opening the file, as main does since #31291 (test only)
2026-10-21 12:38:35 -04:00
Jinwoo Hong bbc8303b47 A running agent's resume note is not a sleep, so switch-on builds report it connected (dark, layout-owner build only) (#31485)
* fix(layout): a running agent's resume record is not a sleep in switch-on builds

The shared layout load put every pane resume record into leaf.sleeping, so main's
terminal index read a live or quit capture as a slept pane and terminal.list
reported running agents as disconnected and unverifiable. Records of slept panes
(isPaneSleepRecord) now load to leaf.sleeping and running agents' records to
leaf.resume; both save to the same sleepingAgentSessionsByPaneKey entry, so the
disk format is unchanged. The disk-field census names both holders, and a model
rule refuses a pane holding both.

* fix(layout): a waking start wakes only slept panes; a recordOnly wake drops either record

* fix(layout): main's shutdown reconnect facts read a running agent's resume record as awake
2026-10-21 05:03:22 -04:00
Jinwoo Hong 065e6873f6 A paired desktop shows the held-terminal banner and Reopen for a server's held terminals (switch-on builds only) (#31275)
* A paired desktop shows a layout-owner server's held panes on the existing banner; Reopen asks the server (switch-on builds only)

The server's frames carry status 'held' on a pane a newer build's terminal service may still
run. The desktop now lists those panes on its existing newer-build banner, with the same wording
and states. Reopen calls the server's new `layout.reopenHeldPanes` under the server's ids: the
server stops its newer service, as the desktop's own Reopen does, and starts each pane still
held. The desktop starts nothing; the server's next frames clear the status and bind the pane.
A refusal or failure shows the banner's failed state.

Served and advertised (`layout.reopenHeldPanes.v1`) only by a layout-owner build, to paired
desktops (not phones). A server without the capability, or that publishes no status, shows no
banner, as today.

* test(layout): held server pane stays Reopening until the reattach frame, stays failed after a refusal, and shows nothing without the reopen capability

* fix(layout): a held pane's Reopen acts only on its own host and on main's published status (switch-on builds only)

- Reopen needs the clicked pane's key; a pane that already left the banner's store does nothing,
  so a server banner clicked as its frame drops the pane never stops this machine's newer service.
- Local versus server is decided by the pane's host, not by whether it has a release.
- The banner portal for a pane with no terminal handle exists only in a layout-owner build.
- The server reopens only panes its published layout shows held, the status the desktop saw.

* test(layout): Reopen ignores a pane rebound to a new terminal whose held entry is still stored

* test(layout): the dropped-pane Reopen test checks for the reopen method, since the view report shares the RPC client
2026-10-20 22:20:05 -04:00
Jinwoo HongandE2E Test 6e589bb4ba Layout-owner build: phones see main's terminal and editor rows (A2, dark) (#31276)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* test(layout): terminal reply fixtures captured from the window-owned build, and main's switch-on proofs

* wip(layout): main's terminal surface index feeds the runtime graph (dark)

* wip(layout): main's panes stand in for the window's terminal list (dark)

* test(layout): the window sends main-listed panes as mount facts (dark)

* test(layout): surface proofs use public runtime calls

* chore: lint

* feat(layout): close by handle closes in main's layout; the close-time relist goes (dark)

* feat(layout): main's binding outranks a caller's pane claim on the terminal record (dark)

* test(layout): window-list tests stay on the rollback build; switch-on mount-fact cases (dark)

* wip(layout): main's terminal index reads E1's shared projection and follows its operation ends (dark)

* refactor(layout): the main-surfaces host lives beside the session-tabs readers, keeping the graph file under its line cap (dark)

* fix(layout): main's panes skip the window-era membership fence, follow workspace routing, and list a terminal when it registers; window close only hands the graph back (dark)

* refactor(runtime): explicit-close PTY stops move unchanged to their own module, for the file's line cap

* test(layout): the cold-restore table runs in a layout-owner build too: one push per registration

* test(layout): the parked and automation pane tests assert the same window report in both builds (mount facts with the switch on)

* chore: one layout-owner switch import after the restack on D1a

* feat(layout): main's terminal.list rows carry the host's verdict; a pane with a status is never live (dark)

* chore(layout): fit main's surfaces under the lint gates after the D1a restack

* feat(layout): every pane status D1a publishes keeps a row from reading live; remote host answers pass through (dark)

* feat(layout): a drag-out moves main's own binding and refuses the home copy's last pane; the window's terminal claim is not consulted (L127, dark)

* fix(layout): a tab close by a starting pane's handle stops the tab's terminals; a starting pane is never exited on the inventory alone; terminal.list lists every saved pane main owns; the pane claim lookup leaves the output path (dark)

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

* fix(layout): main's index reads routing only for a workspace two partitions hold, the fence reads ownership once, a legacy pane keeps its claim; switch-on tests assert main's answers after a window closes (dark)

* refactor(runtime): a worktree stop's terminal collection moves unchanged to its own module, for the file's line cap

* fix(layout): main's index routes every workspace as the layout stream does, re-reading routing only on a write or a terminal registration; a worktree stop names every terminal main binds; a starting pane is unverifiable (dark)

* feat(layout): a workspace main owns publishes its terminal and editor session.tabs rows from main's layout; the window still supplies browser, chat and view-only rows until D3b (A2, dark)

* chore: lint

* test: type the row fixtures

* chore(layout): main's session.tabs projection is an old-client entry, imported only by main's terminal surface index

* fix(layout): main's session.tabs rows carry the tab's name, the window's agent status and markdown file id, list rows in tab order, and keep groups and selection consistent (A2, dark)

* refactor(layout): the projection's group order moves unchanged to its own old-client module, for the file's line cap; lint

* feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark)

* test(layout): a windowless server's layout change reaches a subscribed phone's session.tabs rows (dark)

* fix(layout): a window-only row keeps its place in the rows, the window's selected row lands in a group that selects it, and a workspace with no main group keeps the window's groups (A2, dark)

* fix(layout): a pane main has no binding for yet keeps the window's terminal in its phone row, so an SSH restore lists it pending (A2, dark; ledger L243)

* fix(session-tabs): phones show the window's live selection, focused pane, dirty state and titles over main's saved view; exited terminals stay gone; stray rows join the active group (switch-on builds only)

* fix(session-tabs): main's groups stand with main's selection; certified exits only; a selected stray row joins main's active group

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 22:10:25 -04:00
Jinwoo HongandE2E Test 70b2197c97 The window shows tab, title, move and divider changes only from main's update, never drawing them itself (dark, layout-owner build only) (#31257)
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)

A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.

Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.

* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)

* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)

* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)

* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)

* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)

* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)

* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)

* A tab move shows from main's frame alone; the window no longer re-applies it or adopts main's group id (dark, layout-owner build only)

On main's answer the window only selects the moved tab in its new group, a view choice the
frame does not carry. While main's stream is down it still moves the tab itself, as the frame
sync's closed-stream rule says, but under its own group id: the reopened stream's snapshot
redraws main's copy.

The three switch-on census files for tab moves now pass without a switch mock: the context
menu's move routes to the tab bar's move, the drag hook's pane-column split goes to main in a
layout-owner build, and the tab bar's own move is pinned on a paired server's workspace. A new
frame test pins each move through main and its frame.

* A tab's rename, color, pin, view and a pane's title show from main's frame alone; the window no longer applies them again (dark, layout-owner build only)

* A divider drag sends main the ratios it moved, not the window's tree, and an in-tab pane move shows from main's frame; the ratio hold is gone (dark, layout-owner build only)

* Divider ratio params and their command tests get their own modules (file size)

* Layout-writer baseline pruned: the tab move no longer writes the layout from the window

* The window never draws a tab prop, pane title or in-tab move itself; while main's stream is down the reconnect's snapshot shows them (dark, layout-owner build only)

* View-mode test seeds its paired-server workspace without a type assertion

* A moved tab is selected in the frame flush that shows the move, unless the user selected since; main's answer no longer selects it (L120, dark, layout-owner build only)

* Frame sync stays within its line budget

* Frame sync writes its patches through the moved-tab selection, within its line budget

* The moved tab's selection reuses activateTab's own store update, so no new layout writer is added (L120)

* Format the tab-move frame test

* A tab moved while main's stream is down is selected by the reconnect's snapshot that shows the move (L120)

* A generated tab title main gave is drawn by its frame alone; the window writes only titles left to it as today, and the rename test checks the phone graph republishes (dark, layout-owner build only)

* A divider ratio names the split it means, and main refuses it where that path holds another, so the window snaps back to main's ratios; a drag back to main's ratio before it answers is sent too (dark, layout-owner build only)

* test(layout-owner): tab moves drawn from frames make no window tab or group writes

* test(layout-owner): a reopened tab shows its title and color from main's frame

* test(layout-owner): the tab bar's preview pin through main ends the preview and counts a use

* test(layout-owner): read main's generated-title renames without a type assertion

* test(layout-owner): type the sent renames so the title helper reads them without an assertion

* The frame-apply check selects the moved tabs the flush selected, so the tab-move tests exit clean and a wrong selection still fails (dark, layout-owner build only)

* test(layout-owner): the title batch's live labels are checked in both builds; only the generated title differs

* Main keeps a tab's first agent-generated title unless the window's title replaces it, so a second prompt while main's stream is down no longer renames the tab (dark, layout-owner build only)

* test(layout-owner): a revealed terminal's tab name is drawn from main's frame, not its reply (dark, layout-owner build only)

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 20:58:51 -04:00
Jinwoo HongandE2E Test 94ebd4a5c0 Main starts each restored terminal pane once, at its last size, and names sleeping and held panes (layout-owner build only) (#31171)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a layout command that changes a pane's terminal raises its binding generation (dark, layout-owner build only)

* fix(layout): main shows unshown live terminals again when a client attaches or the terminal service is replaced (dark, layout-owner build only)

Headless servers had no retry: a terminal the startup pass declined stayed tabless until the
next restart. The pass now also runs when a client authenticates and after the local terminal
provider is installed or replaced, one pass at a time. A terminal whose pane record fails to
save after main bound it is no longer reported as left without a pane.

* feat(layout): main stores each pane's last grid on its tab layout (dark, layout-owner build only)

A new main-only setPaneGrid command records the grid a pane's terminal was last sized to, on
its leaf. It is saved as the layout's optional lastGridsByLeafId, which older builds drop on
read, so a downgrade loses only the hint. It moves with its pane.

* feat(layout): main remembers a resized terminal's grid on its pane and starts the pane at it (dark, layout-owner build only)

Each accepted resize, from a window or a client, is written once per burst as the pane's last
grid. A pane main holds starts at that grid, after a size the caller measured and before its
group's size, then 120x40.

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's grid resized just before quit is saved, and a grid change publishes nothing (dark, layout-owner build only)

The desktop and serve quit and orcad's shutdown write any grid still inside its one-second
burst before the final store flush; a quit with none waiting adds no await. A grid is main's
own record: it is not in a published pane, so a resize burst sends no layout frame, and its
write patches only the pane's tab.

* feat(layout): a pane's status names sleeping, resuming and held, and what a status on a bound pane means (dark, layout-owner build only)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): main starts fresh a restored pane bound to another workspace's terminal (dark, layout-owner build only)

The window's restore rule, now in main for every host: a pane whose stored terminal id was
minted for another workspace is unbound, so it starts fresh, and that terminal, if live, is
placed in its own workspace. The id check moves to shared, which the window now imports.

* fix(layout): a start refused because a newer build's terminal service may still run the pane shows it held, not failed (dark, layout-owner build only)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* feat(layout): main's one start of a restored pane with no live terminal, shared by every non-window trigger (dark, layout-owner build only)

Restores from the pane's own stored terminal and resumes a local agent record; concurrent
triggers share one start, and a start that lost to another starter resolves to the winner's
terminal. Not wired yet.

* feat(layout): a client's first view and a CLI start of a restored pane go through main's one starter (dark, layout-owner build only)

Twin j: the phone's first-view start of a pending row moves into its own runtime layer and,
in the layout-owner build, runs through RestoredPaneStarter, which A1's send, wait, read and
wake call as ensureRestoredPaneStarted. It restores from the pane's own stored terminal, resumes
the agent the host recorded there, and resolves once the terminal is registered. With the switch
off, the phone's start is unchanged.

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* test(layout): a window restore, a client first view and a CLI send of one restored pane start one terminal (layout-owner build)

* fix(layout): the restore pass releases a pane from another workspace's terminal through main's layout command, and two files stay under the line limit (dark, layout-owner build only)

A new main-only releasePaneTerminal command unbinds a pane from the terminal it names and stops
nothing, so the pass writes no layout outside a command. The stored terminal layout schema moves
to its own file, and a pane's own size (measured, else its last grid) is one helper.

* test(layout): drop a needless spread in the pane release test

* feat(layout): main shows a restored pane whose terminal is gone as sleeping, and a window treats a status-only frame as a binding change (dark, layout-owner build only)

After a census that answered, each local pane bound to a terminal the host no longer runs gets
status 'sleeping' and keeps the id it restores from; any bind ends the status. Relay and paired
server panes are their own host's to judge. A window rebuilds nothing for a frame that only
changes a pane's status.

* test(layout): restored history reaches each start across two host restarts with no client acking the cold restore

A guard, not a fail-first test: the cold-restore ack only drops the daemon's in-memory copy
(daemon-pty-session-control.ts) and the re-anchored checkpoint carries the history
(daemon-pty-spawn-result.ts), so a host with no client keeps it. C1c moves the ack into main.

* test(layout): the activation gate and worktree-reopen tests assert the same behaviour through main's command and frame (layout-owner build)

In a layout-owner build main places live terminals and draws them through its frames, so the
gate's adoption tests now check the window makes no tab or binding of its own, adopts only what
main's frames hold, and resumes no agent over a live terminal main has not placed yet. The
worktree-reopen tests feed main's frame for the restored pane and answer layout.apply, checking
one tab bound to the live terminal and one resumed tab across reopens.

* fix(layout): a restored SSH pane reattaches over its relay, and the restore starter returns the terminal it started (layout-owner build)

An SSH terminal id names its connection, not a workspace, so the rule that drops another
workspace's terminal no longer releases it or starts the pane fresh. The starter now resolves
to the id its start registered instead of re-reading the stored layout, whose bind write can
still be in flight, and a tab saved with no layout counts as one held pane.

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(layout): the restore starter's start result types a missing terminal id as none

* fix(layout): a workspace's first tab waits for main's placement of its live terminals, so activation opens no extra shell beside one (layout-owner build)

A window activating a workspace before main's placement pass had read the host census found no
tab and asked main for the first one; main admitted it into the still-empty workspace, then the
pass added the live terminal's own tab beside it. An initial create now waits out a pass in flight.

* fix(layout): the restore starter never resolves to another workspace's live terminal, and a first view of a pane main dropped is refused (layout-owner build)

* test(layout): the first-tab placement test overrides the worktree map with its declared type

* fix(layout): the first-surface wait lives with main's placement pass and keeps the runtime file under the line limit

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(layout): a workspace's first surface waits only for the placement pass running when it arrives, and the switch-off build applies commands as before

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(layout): a quit's pane-grid writes never hold the stop past one second, and orcad awaits nothing for them when none wait

The grid flush before the final save (desktop and serve quit, orcad stop) is now raced against a
1 s budget; a grid it misses only means that pane restarts at the default size. With the switch
off no grid is ever recorded, so the flush is null and orcad no longer awaits even that.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* fix(layout): a quit's grid flush swallows its own failures, keeps its budget timer live, and the quit's no-await note names it

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

* fix(layout): a quit writes pending pane grids before the SSH shutdown, and a workspace's first tab waits at most 0.5 s for main's placement pass

The quit's grid flush (still capped at 1 s) now runs before beginSshShutdown, so nothing is awaited
between it and the final save; with no grid waiting, both start synchronously as before. A first tab
waits for the running placement pass at most 500 ms, then is admitted. The quit test's SSH mock now
names the module the quit imports.

* test(layout): the quit tests prove the final save starts in the same turn as the SSH shutdown, in order after the PTY kill

* chore: keep main's bytes for files the merge's format hook rewrote (native-chat formatting is #31184's)

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 16:16:05 -04:00
Jinwoo HongandE2E Test 997d34b043 Layout groundwork (switched off): main creates, starts and closes terminal tabs and panes itself (#31130)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 15:01:25 -04:00
Jinwoo HongandE2E Test ea16d1b11c Key a layout frame's editor file records the same in every build, so a reader built without the switch applies a layout-owner host's order (dark, layout-owner build only) (#31140)
Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 10:15:33 -04:00
1bfb45654f A layout-owner server tells a paired desktop it takes layout changes (dark, layout-owner build only) (#31131)
* feat(layout-owner): a layout-owner server advertises that it takes a paired desktop's layout intents (dark)

status.get adds layout.intents.v1 only in an ORCA_LAYOUT_OWNER=main build. It versions the
layout.subscribe frame shape and the layout.apply contract a paired desktop will rely on.
Tests pin the layout methods open to a paired desktop and closed to phones, and that an older
desktop still reaches every session.tabs mutation on such a server.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(layout-owner): read the paired-desktop frames and seed the server without casts

Co-Authored-By: Claude <noreply@anthropic.com>

* review: the capability comment says only what the code does, and tests pin each next check

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 09:35:14 -04:00
Jinwoo HongandE2E Test cf94325fb5 fix(layout): main shows live terminals no pane shows and derives what the next startup reconnects (dark, layout-owner build only) (#31118)
Main runs one pass at startup and each window load, with or without a window
(desktop, orca serve, orcad): every live local terminal no pane names is bound
to the pane its record names while main still holds it free, else shown in a new
tab bound to it. The window no longer asks main to adopt terminals; it only reads
that main holds them. The window's layout.apply loses its adopt field.

Main also derives activeWorktreeIdsOnShutdown, remoteSessionIdsByTabId and
activeConnectionIdsAtShutdown from its own layout, terminal records and SSH
connections, replacing the window's copies in every save and the quit checkpoint.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 09:00:10 -04:00
Jinwoo HongandE2E Test 03e9a03e9e feat(layout-owner): main opens CLI and phone files itself, in the window's live group (dark) (#31123)
Behind ORCA_LAYOUT_OWNER_IS_MAIN, a CLI or phone file open is applied by main as an
openEditorTab layout command, so a host with no window can open files. The window
reports each workspace's active group as an optional view fact on syncWindowGraph,
and shows main's tab once the frame that draws it arrives. Editor language
detection moves to shared so main can name a file's language.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 08:48:53 -04:00
Jinwoo HongandE2E Test a2e0f20b1e Main sends one layout update per action, with only what changed (dark, layout-owner build only) (#31109)
* A respawned terminal's pane rebinds in one write, and the window shows a binding-only frame without a rebuild (dark, layout-owner build only)

Main: when the window's spawn for a restored pane finds its process gone, the spawn that holds
the pane's reservation keeps the dead binding and its fresh bind swaps it (the restart swap), so
the pane is never published unbound: a same-id respawn publishes no frame, a new id one. A failed
spawn still retires the dead binding. Other callers and window-owned builds retire early as before.

Window: a frame that differs from the shown one only in panes' terminal bindings updates the
store's pane trees as a rebuild would, falling back to a rebuild for holds and shared terminals.

* Dead-owner respawn tests expect the layout-owner build's one swapped bind instead of the early retire

* Main publishes one records frame per layout operation, and the window resubscribes on a missed frame (dark, layout-owner build only)

Main: a durable mutation (a layout.apply, the renameTabs batch, a binding write, a repair) is
one operation. It names the partitions it wrote, and at its end the layout stream publishes
once: one `records` frame holding only the changed records (tab rows, workspace groups, a pane
tree per tab, a binding record per pane, editor files, browser tabs, removed workspaces), each
keyed by main's own ids. Writes no operation scopes (window saves) still publish when their
task ends. The snapshot and every records frame carry a stream epoch. Each pane's binding
carries `ptyGeneration`, which rises on every binding write in this process, so a same-id
respawn is one frame with one binding record. Readers beside the stream get the end of each
operation and the stream's own per-partition loaded model (`workspaceLayoutReads`), so they
need no second Loader pass, with or without a window.

Window: the cache applies records frames to its full layouts, so the existing flush and the
binding-only path are unchanged. A records frame of another epoch or out of sequence, a
malformed frame, or one the cache cannot apply resubscribes at once for a fresh snapshot.

Tests install main's model check, which applies each frame to what subscribers held and
compares it with a fresh projection; dev builds log a mismatch, release builds run none.

* E1 review fixes: keep a swapped binding, bound the window's resyncs, guard operation listeners (dark, layout-owner build only)

- A spawn whose swap bind persisted no longer retires that pane's binding when a later commit
  step fails; a pane on an SSH host keeps the early retire, since its dead owner lives there.
- The window resubscribes at once at most three times before a frame applies cleanly, then takes
  the loss path's backoff. A skipped frame of a newer type still advances its position.
- A throwing operation listener is logged; it no longer fails a committed mutation.
- Tests freeze the shared partition model a reader gets; the dev-build model check skips frames
  that only change live titles. Generation and model-check comments say what is true.

* Keep the layout stream's backoff reset after a single resync; only a resync storm holds it

* A layout stream that recovers from a resync storm reopens on the normal delay again

* Move the replaced pane owner into its own module so the stable owner no longer imports its replacement (dark, layout-owner build only)

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 08:19:53 -04:00
Jinwoo HongandE2E Test 4e998861e1 test: shared switch-on fixtures so 71 layout tests pass with main owning the layout (#31076)
* test: shared switch-on fixtures so 71 layout-owner test files run with the switch on

- layoutApplyTestTransport answers layout.apply (accept, apply to main's model with its frame, pending, refuse)
- main's loaded layout as snapshot frames, shared from the frame-sync test
- tab-layout maps for the editor store harnesses; missing store methods in the main harnesses
- workspaceRepos/registerWorkspaceRepos so main's load keeps seeded layouts
- ORCA_LAYOUT_OWNER=main runs vitest with the switch on
- the 4 rollback-build tests stay switch-off, labelled

* test: keep switch-off assertions exact, settle only after layout commands, reset the transport per test

* test: parse the transport's command without Reflect.get; name the staged-session type

* test: pin the test transport in the layout.apply census; route the autosave fixture as unit support

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 06:50:11 -04:00
Jinwoo HongandE2E Test b822132a75 Agent titles from one status batch reach main as one rename (dark, layout-owner build only) (#31048)
* Send a status batch's generated tab titles to main as one rename (dark, layout-owner build only)

* Share the window's as-today refusal rule with the title batch; cover a failed batch and a paused workspace

* Move the renameTabs request schema to its own module, under the file's line limit

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 04:38:28 -04:00
764e09bf2b No empty extra terminal tab from an old saved terminal record (dark, layout-owner build only) (#31045)
* fix(layout): main sweeps a stored terminal row the window would not reattach, so no empty extra tab shows (dark, layout-owner build only)

A stored terminal row with no tab-bar entry became a tab in main's layout even when nothing
would reattach it, so with main owning the layout the window showed an empty extra terminal tab
that today's build never shows. Today's window drops such a row at startup: it keeps one only
when its startup reconnect plan names a terminal for it (the row's own terminal in a workspace
live at shutdown and not on SSH, where that id is only a wake hint; an SSH relay session id; or a
split row's own pane), and removes the rest with their pane layouts.

With the switch on, main's Store load now applies that same sweep once, beside the existing
tab-level binding repair, so main's layout, frames and saves never hold the row. Sleeping agent
records stay, as they do in today's window. A selection naming a swept row moves as the window's
sweep moves it.

The window-parity tests now restore with today's window as the oracle and load as main does with
the switch on (its startup sweep, then the Loader), so they hold under either build; the ghost-tab
entry is removed and each kept and dropped shape has a profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(layout): the startup sweep keeps an SSH row whose own terminal the window may reattach (dark, layout-owner build only)

The window ignores an SSH row's own terminal id only when it finds the worktree listed; an SSH
folder workspace or an unlisted SSH worktree reattaches by that id. Main cannot see the listing
at load, so the sweep keeps such a row rather than risk deleting a terminal the window keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(layout): the startup sweep keeps a row any partition's shutdown list names live (dark, layout-owner build only)

The window reads one shutdown list merged from every partition, so a stale list in one partition
can mark another's workspace live. The sweep now keeps a row live by its own partition's rule or
any partition's list, so it never drops a row the window keeps. A session from before the tab
bar is swept as the window sweeps it (parity profile added).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:57:46 -04:00
Jinwoo HongandE2E Test d47e3de088 Three small fixes in the layout-owner build: repo reads, dragged-out pane folder, cancelled chat reader (dark, layout-owner build only) (#31003)
* Main routes a floating terminal or removed project without reading every project (dark, layout-owner build only)

In a layout-owner build, finding a worktree's save partition checked for a
project registered on two machines by listing every project on every call,
including ids that cannot belong to a project. Skip that list for ids with no
registered project, and reuse the list a hydration pass already read.

* A dragged-out pane with no shell yet opens its new tab in the pane's folder (dark, layout-owner build only)

In a layout-owner build, main builds the dragged-out pane's new tab and the
window shows main's tab, so the window's folder for the pane never reached it
and the new shell started in the source tab's folder. The window now sends the
pane's folder with the move as an optional field, and main uses it when the
moved pane has no terminal yet. An older main drops the field and keeps today's
folder.

* A chat cancelled while it is being created stays closed when the late reply arrives (dark, layout-owner build only)

In a layout-owner build, cancelling a new chat on this computer closes it on
the host without writing a cancellation record, and the host still answers
the in-flight create as successful. That answer then rebuilt the closed chat's
conversation reader, which nothing would ever close. The cancel now notes the
closed chat in memory, and the create answer skips the reader for it. No
message between the window and the host changes.

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 01:39:20 -04:00
Neil 40ac8e5f9b fix(remote): keep a paired host's undelivered tab close across a reload (#30758)
* fix(remote): keep a paired host's undelivered tab close across a reload

* fix(remote): show a tab again when its held close expires

* fix(remote): one choke point shows every held close the store drops

* fix(remote): keep held closes while the pairing is unknown; tidy dispositions

* fix(remote): save a held close for a long path-named editor tab
2026-10-19 09:40:17 -07:00
Neil 59a9da22a0 refactor(pane-layout): retire the unread intent tag; renamedLeafIds is the one layout delta (#30734)
No host ever read intent: 'gesture'. The client stops sending it, the params
schema strips it from older clients, and docs/reference records the rule that a
snapshot never carries intent.
2026-10-19 08:01:07 -07:00
Jinwoo Hong 6c40f9190d Editor files that share a path across two hosts: the tab lookup, the load report and the phone close match the right file (dark, layout-owner build only) (#30569)
* fix(layout): two owners' files at one path keep their own host, load report entry and phone retire (layout-owner build only)

The bare-path tab's file lookup took the first record of its path, the load report keyed file
records by path alone, and a headless host's phone retire found and removed records by path.

* fix(layout): a path-id preview tab's flag stays on its own owner's record, and a stale phone id retires nothing (layout-owner build only)
2026-10-19 01:48:21 -04:00
Jinwoo Hong 613c5986e3 Editor records keyed by owner, phone active file, and a bound on frame holds (dark, layout-owner build only) (#30473)
* fix(layout): editor file records keyed by path and owner, so two hosts' files at one path stay apart (layout-owner build only)

Opening a runtime-environment file at a path a local file already held replaced the local record,
the stored-session patch and unsaved drafts matched by path alone, a move took every owner's record
of the path, and closing a bare-path tab removed both owners' records.

* fix(mobile): a windowless host's editor tab under its owned id shows as active, and retiring it clears the active file (layout-owner build only)

* fix(layout): a layout change's tab and pane holds end 15 s after it was sent when main never answers (layout-owner build only)

* fix(layout): main's view reads and prunes keep an active file under its owned id (layout-owner build only)
2026-10-18 21:34:37 -04:00
Brennan Benson ecd83b7b48 Resume terminal conversations in native chat without waiting for History (#29150)
* fix(native-chat): identify the owner when resume create is refused

* test(native-chat): consolidate transcript refusal cases

* test(native-chat): keep ownership coverage at the launch boundary

* test(native-chat): update adoption fixture for early ownership admission

* test(native-chat): align resume admission assertions with host ownership

* test(native-chat): keep RPC coverage focused on ownership proof

* test(native-chat): assert blank request presence before optional source

* fix(native-chat): admit the actual CLI source during chat creation

* fix(native-chat): satisfy source admission lint checks

* test(native-chat): provide the waiting hook prompt

* test(native-chat): isolate source admission fixtures from host homes

* test(native-chat): canonicalize adoption fixture directories

* test(native-chat): preserve early ownership admission in source stack

* Resume CLI conversations directly in native chat

* fix(native-chat): preserve resume refusal behavior and honest transcript feedback

* fix(native-chat): retain import read evidence in typed result

* Use the header test fixture record type

* Consolidate launch owner imports and correct feedback assertion

* Keep existing resume translation in runtime catalog

* fix(native-chat): preserve admitted source and prepared execution home

* fix(chat): retain source evidence for dispatched adoption

* fix(chat): keep source capture within module limits

* Keep deferred chat publication in the shared launch owner

* test(chat): allow host evidence in create routing

* Satisfy launch publication checks and scope slot cleanup

* Preserve canonical tab membership on projected reorders

* Use recorded launch ownership for surface recovery

* Reserve deferred chat order through existing group actions

* Pass the active layout owner into launch cancellation

* fix(native-chat): admit an unsaved transcript under a linked account folder

A missing transcript stayed unresolved while its account root resolved through
symlinks, so containment failed and a not-yet-saved conversation showed a load
error instead of the unsaved notice.

* refactor(native-chat): move the newest-item import window into its own module

Keeps journal-legacy-import.ts under the line limit after the source
identity observer landed on top of the streaming import.

* fix(native-chat): restore the refusal error import lost in the main merge

* test(native-chat): pass the layout owner in the layout-owner cancel test

main's new test predates launch cancellation taking the active layout owner.

* fix(native-chat): admit Pi transcripts against the root Session History walks

A PI_CODING_AGENT_DIR that already ends in sessions, or any custom folder,
made admission expect <dir>/sessions while the lookup searched <dir>, so a
found conversation was refused.

* refactor(native-chat): admit and look up CLI transcripts in one store per account

Each agent's import described where an account keeps its conversations twice,
once for the path check and once for the lookup, and the two could disagree
(the linked-folder and Pi custom-root refusals). The import now names one
conversation store per account; the create path resolves it once and both the
hook-path check and the fallback lookup judge that same store. OpenCode's
store is its exact database.

* fix(native-chat): keep a History resume that joins a terminal resume alive when the terminal closes

A History click that joins a pending terminal-tab resume now takes the
launch over: the terminal hand-off runs at join time and the create
resumes by conversation id, so closing the terminal before create no
longer abandons the shared launch.

* refactor(native-chat): move the launch source hand-off into its own module

* fix: pick the chat that adopts a header resume the way History does
2026-10-18 15:22:17 -07:00
Jinwoo Hong eaa1ee131d A CLI-started terminal shows once in its workspace, and a pane close main has no pane for closes in the window (dark, layout-owner build only) (#30391)
* fix(layout): a workspace only main held shows main's tab on the window's first visit (dark, layout-owner build only)

* fix(layout): a live terminal no pane shows goes through main, which names the tab it holds; a pane close main has no pane for closes here (dark, layout-owner build only)

* test: no type assertions in the CLI-first tests

* refactor: the switch-off build keeps today's live-terminal mint as it was

* review: frame sync under the line limit; the first-visit test names exactly main's tabs

* fix(layout): adopt takes the recorded pane only while it is unbound

* fix(layout): a refused live-terminal adoption is declined, not counted as shown
2026-10-18 16:25:49 -04:00
Neil ecb1d6859c refactor: compatibility folders name their entry modules instead of an index barrel (#30269)
Deletes remote-layout-snapshot/index.ts and workspace-layout/old-client/index.ts. The compat-boundary
check now lists each folder's entry modules, so deleting the folder plus its listed importers'
import lines still removes the path.
2026-10-18 01:36:12 -07:00
Jinwoo Hong 2a8b4c31a5 Phone and old-client tab lists keep editor tabs under owned ids (dark, layout-owner build only) (#30216)
With the layout-owner switch on, the session-tabs projection and the windowless host's editor
projection matched an editor tab to its file by path only, so tabs under an owned id got no row
(or lost their tab id, color and pin). Both now use the shared editorTabShowsFile matcher, which
is path equality with the switch off.
2026-10-17 23:35:10 -07:00
Jinwoo Hong 25fb579c12 fix(layout): refuse a layout change on a workspace a WSL move has paused (dark, layout-owner build only) (#30198)
* fix(layout): refuse a layout change on a workspace a WSL move has paused, and say why (dark, layout-owner build only)

* fix(layout): refuse without a toast; keep the move message in main
2026-10-17 23:26:38 -07:00
Jinwoo Hong 95d6018323 Main keeps editor tabs under owned ids and in-place re-owns in step (dark, layout-owner build only) (#30206)
A restored tab re-owned inside its own workspace now tells main through
renameEditorFiles with the file's new record; main skips it for paired servers.
Main's load, save, close and tab-host read an editor tab's file by its owned id
as well as its path, behind the layout-owner switch.
2026-10-17 23:04:15 -07:00
Jinwoo Hong 31f766515d Main records when a workspace's default tabs were applied (dark, layout-owner build only) (#30176)
* feat(layout): main records a workspace's default-tabs mark from the window (dark, layout-owner build only)

* fix(layout): record the default-tabs mark after the tab creates it covers (dark, layout-owner build only)

* test(e2e): strict default-tabs mark and exact tab count across a crash (dark, layout-owner build only)
2026-10-17 23:02:05 -07:00
Jinwoo Hong 483a17becc Reopening a file in its open tab sends the main process the changed file record (dark, layout-owner build only) (#30197)
* fix(layout): a reopen that rewrites an open tab's file record reaches main (dark, layout-owner build only)

* fix(layout): compare reopened editor records without a new renderer import
2026-10-17 22:58:06 -07:00
Jinwoo Hong b9b1b85261 test(layout-owner): an older session's moved editor tab gets its workspace's id on save and from the first frame (#30172)
Sessions saved before #29930 can hold a moved editor tab whose
worktree id still names the workspace it came from. Pin that the
saver writes the workspace's own id and that the window's tab carries
it after main's first snapshot. Tests only.
2026-10-17 22:09:10 -07:00
Jinwoo Hong bf97836ae0 Pane titles stay layout: the window stops writing tab names as pane titles, and main loads them as the window does (dark, layout-owner build only) (#30136)
* refactor(shared): move the pane title sanitizer to shared, unchanged

So main's layout Loader can apply the same rule the window applies on load.

* fix(layout): pane titles stay layout in a layout-owner build (dark)

The window stops writing a revealed terminal's tab name as its pane title,
and main's Loader drops a lone pane title that only repeats the tab's name,
as the window's load does. Both behind ORCA_LAYOUT_OWNER_IS_MAIN.

* fix(layout): the Loader compares a lone pane title with the tab bar's quick command label too (dark)

As the window's row hydration does when the row has no label.

* fix(layout): the Loader compares a lone pane title with the tab bar label, the live title (dark)
2026-10-17 20:59:57 -07:00
Jinwoo Hong c2021ebc47 fix(layout-owner): main records a quick command tab's label on create (dark, layout-owner build only) (#30112) 2026-10-17 16:11:05 -07:00
Jinwoo Hong d9a61432c8 Main's layout Loader keeps one pane per terminal on restore (dark, layout-owner build only) (#30110)
* fix(layout): main's Loader repairs a terminal bound in two panes of one tab (dark, layout-owner build only)

* fix(layout): a window save stores main's repaired pane tree, so the duplicate-terminal repair survives the save (dark)

* test(layout): the SSH duplicate-terminal case builds an SSH-hosted tab
2026-10-17 16:03:56 -07:00
Jinwoo Hong 609f3425ad Record simulator tab opens and closes in main (dark, layout-owner build only) (#30034)
* feat(layout): record simulator tab opens and closes in main (dark, layout-owner build only)

* chore(layout): note why sleep/wake params copy OnTab
2026-10-17 02:48:46 -07:00
Jinwoo Hong 4f5d464ca6 Open into the window's emptied pane beside a split branch in one layout command (dark, layout-owner build only) (#30039)
* Open into the window's emptied root beside a split branch in one layout command (dark, layout-owner build only)

A tab opened into a group the window kept empty reaches main with the
split that places it, now also when the group's neighbour is itself split
(named by the branch's first and last groups) or is another empty group
main lacks. Main makes the group with its tab beside that branch. Switch-off
builds send no such command.

* Comment the refusal of a branch main's tree does not hold (dark)
2026-10-17 02:15:39 -07:00
Jinwoo Hong fa962dd1cd Record chat tab opens and closes in main (dark, layout-owner build only) (#30019)
* Record chat tab opens, closes and host-listed chats in main (dark, layout-owner build only)

A chat tab the window opens, closes, or adds and removes from a host
listing of this machine's chats now reaches main as openAgentSessionTab,
closeTabs or setTabProps under the window's ids, so the window's save no
longer teaches main a chat tab. openAgentSessionTab takes the window's
place, creation time and new split group, and the same open again
changes nothing. Switch-off builds are unchanged.

* test(layout): a paired server's chat tabs send nothing to main (dark)

* fix(layout): keep a windowless host's chat-tab saves as they were; send synced chat opens in tab order (dark)

Only a window's openAgentSessionTab (main marks it drawn on layout.apply)
takes the new rules: a repeat changes nothing, a taken id is refused, and
a workspace with no group takes the window's. A windowless host's own
saves mint a fresh group and keep a chat whose id another tab holds, as
before. A host listing's new chats are recorded in tab order, so each
open's anchor already reached main.

* refactor(layout): one rule set for a chat tab open; the windowless caller names only a group it holds (dark)

Drops the per-sender drawn flag. The windowless save passes its
remembered active group only when the workspace still holds it, so it
mints a fresh group as before. Refusing a taken chat tab id is gated on
the build switch: shipped builds still save the duplicate id.
2026-10-17 00:57:12 -07:00
Jinwoo Hong a349bfa6cc Agree with main on first-group ids, tab order and the preview flag (dark, layout-owner build only) (#29987)
* Agree with main on a workspace's first group and the tab order, and stop writing a preview flag main never stores (dark, layout-owner build only)

Switch-on builds only; switch-off behaviour is unchanged.

Group ids: a window save made before main's frame with a workspace's first
tab carries none of that workspace's tab-bar records, so the Loader rebuilt
the tab in the group it names for the workspace while main had minted a
random one: every workspace's first save replaced main's group. Main now
names a workspace's first group as the window's root group and the Loader
do (`workspaceRootGroupId`), or takes the sender's when the window drew the
tab in a group main lacks (the window keeps its emptied group); the
window's `ensureGroup` names it the same way. The detector reads a lone
group with no stored tree as that group's leaf, as main's own save writes
it. Empty groups: the window never saves a group it holds empty, so a save
cannot bring back one main closed (locked by a test).

Tab order: a reorder the window makes itself (an anchored CLI or agent
create, a reopened tab's old slot) is recorded in main as `moveTab` steps
at the store boundary; callers that already asked main pass
`layoutOwnerHandled`. The reopened-editor record this replaces is deleted.
A paired client's move (`ui:moveSessionTab`) goes to main first through
the tab-bar drop path.

Preview: activating a tab removed its preview flag by writing
`isPreview: false` on every tab; main stores no flag, so every activation
changed the tab in main's model. The window now drops the flag instead.

* Record a reopened tab's place in main every time, leave a move into a tab's own group alone, and keep a preserved preview untouched (dark, layout-owner build only)

Review fixes, switch-on only:
- A reopened tab's place is recorded as one moveTab whenever its old
  position is known, as the deleted editor-only record did, now for
  every kind; the reorder it may make is not recorded twice.
- A move into the tab's own group (a paired client can send one) goes
  to today's no-op instead of a moveTab that reordered the tab.
- Activation that preserves a preview returns the tab unchanged.
Tests: switch-off first group stays minted; tabOrderMoves refuses a
duplicate order.

* Send a paired client's tab reorder to main as its whole order (dark, layout-owner build only)

A client reorder names the group's full order, but it went to main as
one moveTab of the named tab, so [a,b,c] -> [b,a,c] naming c moved
nothing anywhere. It now takes the store's reorder path, whose record
turns the whole order into main's moves (tabOrderMoves), the one rule
for every window-side reorder. Moves and splits still go main-first.
2026-10-16 23:25:10 -07:00
Jinwoo Hong 5b69634c0a Agree with main on main-made terminal tabs and sleeping-agent records (dark, layout-owner build only) (#29967)
* feat(layout): main-made terminal tabs drawn with main's facts; resume records reach main record-only (dark)

In layout-owner builds only:
- A terminal tab main made itself (drag-out, CLI or host terminal, live-surface
  adoption) is drawn with main's createdAt and creation fields from the frames,
  and the window sends no create for it.
- An editor tab main records takes the time the window drew it.
- Every change of the window's agent resume records (live and quit captures,
  dropped records) reaches main through record-only sleep and wake commands,
  which stop or start nothing.
- A terminal row's remount generation is declared the window's.

* fix(layout): real wake reaches main before the record drop; drag-out keeps a frame-drawn tab (dark)

In layout-owner builds only:
- A resumed pane's wake is sent before the window drops its record, so main's
  wake still finds the pane sleeping.
- A drag-out whose new tab main's frame already drew shows the pane in that
  tab instead of throwing "Terminal tab already exists".
- A main-made tab keeps the window's shell, cwd, host-runtime and launch agent
  where main's copy names none (a runtime create records a shell only when
  its launch named one).
- No record-only wake for a tab the window closed; main dropped its records
  with the tab.
- Tests for the resume-record follower.

* fix(layout): a dragged-out chat pane's new tab opens in chat view in main (dark)

Main's movePaneToNewTab now gives the new tab viewMode 'chat' when the moved
pane is its source tab's chat pane, as the window's own drag-out does. In a
layout-owner build the window adopts the row main's frame drew, so without
this the tab opened in terminal view.

* fix(layout): a bound pane with a resume record starts as a no-op; resume records only for held tabs (dark)

- startPane checks the pane's live terminal before its resume record: a
  running agent's live or quit record does not make its pane asleep. A pane
  with no terminal and a record is still refused.
- The window sends record-only sleeps, like record-only wakes, only for tabs
  it still holds; a capture from a stale agent status no longer names a tab
  main already closed.
2026-10-16 23:23:04 -07:00
Jinwoo Hong 37c314bcc9 Send a browser tab's window times to main with its open (dark, layout-owner build only) (#29988)
* fix(browser): keep slept workspace browser tabs off the reopen-closed list

Sleeping a workspace closed its browser tabs as if the user had closed them, so
Reopen Closed Tab could bring back a tab that was only slept. Sleep now closes
them with a 'sleep' reason that skips the reopen stack while still releasing
host pages, doc grants and webviews as before.

* feat(layout): send a browser tab's window timestamps with its open (dark)

A layout-owner build's openBrowserTab carried no createdAt, so main stamped the
tab-bar entry and the browser record with its own apply time and the window's
next save diverged on createdAt for both. The window now sends both times, as
createTerminalTab does; only switch-on builds send them.

* Revert "fix(browser): keep slept workspace browser tabs off the reopen-closed list"

This reverts commit 6798c3c701.

* test: drop the slept-tab close test that went with the held sleep fix
2026-10-16 23:15:43 -07:00
Jinwoo Hong f7a8e633af Send a terminal tab's own changes to main first, from every caller (dark, layout-owner build only) (#29904)
* feat(layout): route a terminal tab's own changes to main first (dark)

Rename, color, agent and vault titles, view mode and an ended launch
agent now reach main as layout commands from every caller, in
layout-owner builds only. Main's create also takes the window's
createdAt, and frames keep the window's pane mount generation.

* fix(layout): toggle from the mode sent to main, give each agent title its own answer (dark)

A second view toggle before main answers the first now flips from the
mode already sent, so the tab ends where a window-owned build would.
An agent-title batch applies each tab's title when main answers that
tab, and a paired server's tab at once. Drops the frame merge's
generation field: the window's own save already carries it. Reverts
unrelated formatting.

* fix(layout): clear a pending view mode only on its own send's answer (dark)

* chore: restore three files the commit hook reformatted outside this change

* fix(layout): send one agent title per tab while main has not answered (dark)

The window keeps a tab's first generated title, so a second prompt's
title sent before main answered the first would leave main on the
second and the window on the first.
2026-10-16 19:56:25 -07:00
Jinwoo Hong 9cc5d6b427 Keep an editor tab Orca moved to its file's workspace across restart; record editor renames in main (dark) (#29930)
* fix(editor): keep an editor tab Orca moved to its file's workspace across restart

The move saved the tab under the file's owned id and left the tab's
workspace pointing at the old one. Restore rebuilt a local file under
its bare path, so the tab named no open file and was dropped. Restore
now maps a saved owned id to the id it rebuilds, and the move gives
the tab its new workspace.

* refactor(editor): delete reorderFiles, which nothing calls

* chore: prune the renderer layout-writer baseline

* fix(layout-owner): keep diff, preview and conflict records when a frame redraws editor tabs

Switch-on builds only. A frame rebuilds the window's tabs through the
restore builders, which drop every diff, conflict and check tab (restore
has no record for them), and the store patch dropped every open file
the frame did not carry. Frames carry only edit-mode files, so the next
frame after opening a diff closed it in the window while main kept it,
and a markdown preview kept its tab but lost its record.

The rebuild now keeps a transient tab whose record the window holds,
and a held record goes only with the last tab showing it; a record no
tab shows (a conflict review's selected file) stays.

* feat(layout-owner): record renamed open files and closed conflict placeholders in main

Switch-on builds only; switch-off behaviour is unchanged.

A rename or move in the explorer rekeys open editor, preview and diff
tabs in the window and main learned it only from the window's save.
The window now records the rekey as a `renameEditorFiles` command,
under the ids it chose (bare path, owner-qualified id, diff id), one
per workspace, with each edit-mode file's new record. It records
rather than asks first: the rekey is atomic with drafts and the
autosave echo gate, and its answer gates the move coordinator. Main's
path-keyed `editorFilesRenamed` transition, which nothing produced and
which could not match owner-qualified or diff ids, becomes this command.

A git status poll that sees a deleted-side conflict resolved drops its
placeholder record but leaves the tab to the tab reconcile; frames then
brought main's edit record back. Main now closes that tab.

* test: type the rename command as a window layout command

* fix(editor): map a moved tab's owned id on restart only; main records a renamed file only for a tab it shows

* fix(editor): restore a moved tab's file under the owned id the tab names

Restart and frames now agree: neither rekeys the tab, and a frame that
redraws one workspace never points its tab at a same-path file in
another. A rename that changes nothing leaves no empty file list.

* test: a moved tab's owned id stays apart from a sibling's same-path file in any frame
2026-10-16 19:54:52 -07:00
Brennan Benson 64e544a442 feat(mobile): Resume in New CLI from a phone chat tab (#29783)
* feat(mobile): Resume in New CLI from a phone chat tab

A chat tab on the phone had only Close in its long-press menu, so a chat
that would not load left no way to reach its conversation. The phone now
offers Resume in New CLI, like the desktop tab menu: the chat's host
prepares the conversation and the phone opens a copy in a new terminal tab.

The resume and fork startup builders move to src/shared so the desktop and
the phone build the same command.

* test(mobile): type the fake terminal create instead of casting its calls

* fix(mobile): pass the host's fork source as is; match the quoted args in the test

* test(mobile): give the fake host client the untyped mock the other client fakes use

* style: format the shared resume builder and phone fork files

* Fix mobile chat CLI fork creation on owning host

* Fix mobile chat fork lint and split create owner helpers

* Keep chat fork beside phone anchor across tab groups

* Combine mobile terminal runtime type imports

* Repair mobile create CI contracts after chat fork extraction

* Classify chat fork preflight refusals and preserve mobile create recordings

* Keep route-owned create cleanup quiet after unmount

* Preserve typed runtime repair refusal in chat fork preflight

* Preserve checked store through mobile chat fork preflight

* Register mobile chat fork runtime fixture before test setup

* Remove unused mobile create recording value

* style(mobile): format the chat fork create files; move fork reply messages into the fork feedback module

Formatting pushed the create hook past its line limit, so the fork-only reply message, partial-success warning and host capability check now live beside the other fork messages. Same order and copy as before.

* Keep quick command mutation aligned with formatted request

* Keep mobile chat CLI forks in terminal view

* Pass mobile chat resolution its existing controller arguments

* Match the mobile create fixture mock layout

* Keep mobile chat CLI forks open when placement is unavailable

* Brace placement contract fixture checks

* test(mobile): keep the prompt controller mock under its line limit after the main merge

* Expose pane admission in the runtime test fixture type

* Complete the chat fork contract controller fixture
2026-10-16 18:10:24 -07:00