Commit Graph
2357 Commits
Author SHA1 Message Date
Jinwoo HongandClaude bd0f326218 feat(sidebar): one lifecycle line per workspace in main, with in-progress hints (switch-gated) (#31553)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)

The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.

- Main stops counting a closed tab's terminal once it was asked to stop
  and no pane shows it; a failed or refused stop takes the request back,
  and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
  so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
  read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
  feed stands in while a fresh read is out; it highlights its own
  last-opened workspace and a fold never scrolls; projects file under
  their project group with the desktop's indent; the collapse spinner
  shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.

* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises

- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.

* feat(sidebar): main publishes cached snapshots and announces its own stores; detected, view and live-content domains (inert)

* fix(sidebar): keep links confirmed only by a new git run, no rescan on metadata writes, no phone wait on detected checkouts

* fix(sidebar): rescan the workspace rows when the Windows runtime default changes

* feat(sidebar): one lifecycle queue per workspace in main, with in-progress hints (switch-gated)

Create, delete, terminal.closeAll, terminal.sleep and worktree.sleep now run through one
first-in, first-out queue per host-qualified workspace: strict order, no merging, delete
cancels what has not started and waits for what has, an unconfirmed outcome pauses until
the host's own answer. Hints publish in a new inProgress feed domain (no reader yet); the
creations domain is its projection. A finished create or delete keeps its hint until a scan
started after it applies, in the same revision. Adds the paired-server metadata serializer
(saving domain, no caller yet). Replaces the creation registry.

* fix(sidebar): a create never queues behind the previous occupant's delete

* fix(sidebar): resume a paused workspace by re-observing, never replaying

- A command issued while paused waits behind the pause; when the owner answers again main
  re-reads terminal liveness and the listing, then carries on. A forced delete runs past it.
- Commands keep their arrival order across a workspace lookup; deletes by name key by the
  resolved id, so they wait for that workspace's running sleep.
- A second delete with different options is refused as already in progress.
- Close and delete read lost contact as sleep does.
- A create always frees its own queue entry, even after a retry took over its id.
- Drops the unused opId, and the paired-server write serializer and its feed section,
  which land with their first caller.

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

* fix: restore main's final P2 test fixes a merge reverted

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 19:34:14 -04:00
Neil 926decd2f1 refactor(jira): group attachment modules with concrete owner imports (#31683) 2026-10-21 16:19:40 -07:00
Jinwoo Hong 7df3ff76d5 fix(sidebar): keep each host's activity, PR lane and retention apart for a same-id workspace (dark) (#31517)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)

The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.

- Main stops counting a closed tab's terminal once it was asked to stop
  and no pane shows it; a failed or refused stop takes the request back,
  and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
  so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
  read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
  feed stands in while a fresh read is out; it highlights its own
  last-opened workspace and a fold never scrolls; projects file under
  their project group with the desktop's indent; the collapse spinner
  shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.

* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises

- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.

* fix(sidebar): key activity, review, retention and repo lookups by host, not bare id

The feed published activity and PR facts by bare workspace id, so a same-id
workspace on two hosts (a paired server and this desktop, or two servers)
shared one host's facts. Activity and review are now keyed by row identity,
retention sweeps per listed row, and the builder resolves each row's repo by
its own host's registration. Bumps the feed semantics version to 3.

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103

* test(e2e): read a review fact by its row key

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103

* test(mobile): key fixture rows by row identity

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103

* fix(sidebar): count and label rows by their own repo registration; keep activity keyed through a failed listing

Host header counts and mixed-host labels read the row's own registration, as
the host filter and row now do, so one render cannot file a row under one host
and count it under another. A failed listing keys activity by the last rows.

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103

* fix(sidebar): a slower older listing never overwrites a newer one's rows

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103

* test(sidebar): read the quiet-desktop PR fact by its row key

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103

* refactor(repos): the renderer re-exports the shared getRepoHostIdentity

Claude-Session: e1fdb3d9-6985-4693-84aa-9a9fad732103
2026-10-21 18:55:25 -04:00
NeilandTauri-EPO 2d9da847cb fix(runtime): keep desktop take-back from stale phone input
Invalidate unfinished phone input claims after desktop takeback or disconnect. Recheck presence after delayed layout and preserve fresh phone input.

Closes #23199.
Adapted from #23200.

Co-authored-by: Tauri-EPO <enrico.pin@gmail.com>
2026-10-21 14:30:57 -07:00
Neil 87ca5dad43 refactor(repo-host): one helper for the hosts registering a repo id (#31634)
Four twin checks (sort stamps, sidebar unread, sort-order notify, worktree-meta owner)
each rebuilt new Set(repos.filter(id).map(getRepoExecutionHostId)). Name it once as
getRepoRegistrationHostIds in shared/repo-host-identity.
2026-10-21 13:31:46 -07:00
Neil 8e89202f60 fix(runtime): resolve a repo's SSH host instead of reading raw connectionId (#31630)
Seven runtime decisions read repo.connectionId, so a row naming its SSH host only as
executionHostId 'ssh:<target>' read as local: the missing-worktree sweep killed local PTYs
unfenced, the drift probe ran local git on a remote path, relay recovery skipped its mobile
tabs, and agent launch picked this machine's platform. Route them through
getRepoRoutingConnectionId, drop a dead includeLocalRegistry branch, and shrink the
repo-host-read ratchet from 91 to 81. resolveRepoForConnection keeps the raw read: it
matches the raw id a client sends.
2026-10-21 13:30:24 -07:00
Neil 553a704403 refactor(execution-host): one resolver for the SSH target a repo row may dial (#31623)
getStoredRepoSshConnectionId and seven inline
getSshTargetIdForExecutionHost(getRepoExecutionHostId(repo)) spellings answered the same
question. Name it once in shared (getRepoDialableSshTargetId), build
getRepoRoutingConnectionId on it, and drop the getRepoHostedReviewExecutionHostId re-export
alias in favour of a direct import of getStoredRepoExecutionHostId.
2026-10-21 13:29:42 -07:00
Neil cd8b289f2f chore(runtime): delete exports nothing reads (#31639)
flushDurableStateOrThrowAsync and six exported types (AgentSessionSpawnTokenScanEvidence,
FreshExplicitAgentStatus, LiveTerminalPty, RemoteStartReceipt, WorkerStartMode(Reason),
WorktreeTeardownStopPty) had no reader anywhere in src, mobile, config or tests.
2026-10-21 13:18:20 -07:00
Jinwoo Hong 1db95e308a fix(terminal): stamp ORCA_WORKSPACE_ID on every terminal main creates, not only folder ones (#31608)
The window stamps it (equal to ORCA_WORKTREE_ID) for every workspace kind and agent
wrappers forward it; terminals started from the phone, the CLI or a server's own create
in a git worktree lacked it.
2026-10-21 15:53:36 -04:00
Neil dafed6b345 fix(orcad): hold terminal admission through a server stop; fence rollback and restart (#31418)
* fix(orcad): hold terminal admission from a stop's start until exit; fence rollback and restart counts

Once orcad begins any stop (signal, managed stop request, idle exit), its terminal
admission stays closed until the process exits, past an update census's 60s fence
or a deferring desktop's reopen. Rollback and restart now take their count through
the same fenced recount the update uses, reopen admission when they leave the
server serving, and stop nothing once that count may have outlived its fence.

* fix(orcad): check the rollback's stop deadline after its transaction write

* fix(orcad): rollback and restart recount under the host lock; a reopen never wakes a server

Only the lock holder closes or reopens a server's terminal admission, so a
rollback or restart that loses the lock can no longer clear another update's
fence. A reopen skips the serving check, so it never starts or recovers a
stopped server, and a stop that begins before the runtime binds still holds.

* fix(orcad): reopen admission when a rollback's stop was withdrawn

* fix(orcad): keep admission closed after a stop that may still be on its way

* refactor(orcad): route update, rollback and restart through one fenced stop

runFencedStop is the single count, close, stop, reopen-on-abort sequence. The
update's activation, the rollback and the restart each supply only their own
refusal, journal step and stop; the reopen and deadline rules live in one place.
2026-10-21 11:57:39 -07:00
Jinwoo Hong 2b1da76d70 fix(mobile): a windowless server warns about unwakeable agents only when the workspace really has slept agents (#31607)
* fix(mobile): warn about unwakeable agents on a windowless server only when the workspace really has slept agents

A running ('live') or quit agent's resume record made a windowless server tell the phone
'Open Orca on the host to wake sleeping agents.' Count only real sleep records, using the
shared isPaneSleepRecord predicate.

* refactor: move the headless wake check into a shared hasSleptAgentToWake predicate

* docs: say a live or quit record is resumed by its own pane

* test: build the headless wake report store from the shared partitioned fixture, without casts

* test: make a host-less session read miss the record, so the SSH partition test proves the named-host read
2026-10-21 14:55:06 -04:00
Kelvin Amoaba 52f6960751 fix(mobile): closing the tab a phone shows returns it to the tab it viewed before (#31365) 2026-10-21 18:28:38 +00:00
Kelvin Amoaba 22154de422 feat(mobile): tab strip shows terminal and chat agent state, reveals the next tab, renames on double-tap (#31372) 2026-10-21 18:27:30 +00:00
Kelvin Amoaba cdb032e83f fix(mobile): New Tab drawer reopens with its agents and refreshes them when desktop does (#31367) 2026-10-21 18:26:42 +00:00
Kelvin Amoaba ba8f237ade fix(mobile): phone terminal and chat tab names match the desktop (#31384) 2026-10-21 18:23:12 +00:00
Kelvin Amoaba dde7bf8533 fix(runtime): renaming a terminal no longer hides its running agent (#31428) 2026-10-21 18:22:37 +00:00
Brennan Benson 8174208436 fix(terminal): every start in a deleted folder opens at the workspace root and says so (#30747)
* fix(terminal): start every terminal in the workspace root when its saved folder is gone, and say so

A terminal whose start folder was deleted opened at the workspace root only
when the window started it; the CLI, phone restore, orchestration and other
host starts failed with the raw "Working directory ... does not exist" error,
because the renderer had to opt in with a cwdFallback flag.

The execution host now makes that decision for every start that may create a
process (anything but a known live pane), with no caller flag. The fallback is
reported only when a process was proven started at the requested folder, so a
reattach or a crash restore in its recovered folder never claims "workspace
root", and only then does the saved start folder heal. A recovered folder that
was itself deleted yields to the request's resolved folder.

The notice reaches each caller through surfaces it already has: the window's
terminal line (now also written when a host-created tab attaches, and on a
restored reattach), the agent.launch warning (also for callers that show the
tab themselves), and a toast when a phone tap restores the tab.

* fix: recover terminal startup folders without healing unreadable paths

* fix: preserve saved start folder when revealing fallback terminals

* Reuse saved-tab lookup in cwd persistence test

* Import mobile tab activation result from its contract

* Check startup fallback response without a test cast

* Update expectation to catch up with accepted saved-folder metadata

* Preserve inconclusive WSL directory evidence when saving startup folders

* fix: project restored terminal ids for handle consumers

* test: return void from launch parity cleanup

* test: retain saved folder in shared spawn expectation
2026-10-21 10:21:36 -07:00
Jinwoo Hong 0eb2403ef8 Main closes, renames, sleeps and starts new tabs' terminals itself, with or without a window (dark, layout-owner build only) (#31571)
* fix(terminal): a CLI or phone close through main keeps a pinned tab, as the window-era close did

* feat(terminal): main applies phone closes, renames, pane titles, sleeps and agent-launch tabs itself, with or without a window (layout-owner build)

* fix(terminal): a tab close through main also ends the tab's dormant SSH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build)

* feat(terminal): main starts a new tab's first terminal under the window's ids, and the pane attaches to it (layout-owner build)

* test(terminal): tests that counted the window's create calls or publications see main's start of a new tab

* fix(terminal): an account-switch restart binds only into a pane main's layout holds, with no window tree (layout-owner build, L197)

* test(terminal): the new-tab start tests hold no type assertions; the start's ledger row is L252

* fix(terminal): a new tab's pane attaches to any terminal main binds it to, and a failed start keeps the tab's startup

* test(terminal): a Claude account-switch restart's tests draw the pane main holds

* fix(terminal): a launch pane main takes back still tells the window why, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build)

* fix(terminal): a new tab another owner's terminal won attaches to it, and a failed start's startup never runs twice

* fix(terminal): main's pane close reads its stop result as a type only, so no runtime module loads before its base class

* fix(terminal): a phone's close through main drops the closed rows from what phones list at once, not at the window's next publish (layout-owner build)

* chore(terminal): mark the phone-close row drop interim (ledger L260)

* test(worktree-create): a new local tab's startup counts as run when main starts it (layout-owner build)

* test(worktree-create): record main's starts without a type assertion

* fix(terminal): a tab closed before main declines its start gets no startup back (layout-owner build)

* fix(terminal): the window's new tab starts in main when its own window asks over IPC, which labels itself a client (layout-owner build)

* fix(terminal): a refused close of a pinned tab leaves its running agent launch alone (layout-owner build)
2026-10-21 13:12:19 -04:00
Jinwoo Hong 84a9f995da Sidebar P2: main publishes without waiting on git, and every source announces its changes (inert) (#31513)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)

The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.

- Main stops counting a closed tab's terminal once it was asked to stop
  and no pane shows it; a failed or refused stop takes the request back,
  and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
  so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
  read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
  feed stands in while a fresh read is out; it highlights its own
  last-opened workspace and a fold never scrolls; projects file under
  their project group with the desktop's indent; the collapse spinner
  shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.

* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises

- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.

* feat(sidebar): main publishes cached snapshots and announces its own stores; detected, view and live-content domains (inert)

* fix(sidebar): keep links confirmed only by a new git run, no rescan on metadata writes, no phone wait on detected checkouts

* fix(sidebar): rescan the workspace rows when the Windows runtime default changes

* test(sidebar): gate the 1000-workspace publish cost on its median

* test(sidebar): name the git-run token type in the identity test
2026-10-21 13:07:39 -04: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 92bbb380a6 Sidebar: land the switch-on fixes from the flip branch on main, switch still off (#31466)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)

The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.

- Main stops counting a closed tab's terminal once it was asked to stop
  and no pane shows it; a failed or refused stop takes the request back,
  and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
  so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
  read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
  feed stands in while a fresh read is out; it highlights its own
  last-opened workspace and a fold never scrolls; projects file under
  their project group with the desktop's indent; the collapse spinner
  shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.

* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises

- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.
2026-10-21 12:24:27 -04:00
Jinwoo Hong de7a2d24f9 Phones get main's tab order, groups and names after every layout change (dark, layout-owner build only) (#31529)
* fix(session-tabs): a windowless layout-owner server lists and pushes main's tab groups and order

With the layout-owner switch on and no window (orca serve, orcad), session.tabs rows came from
the snapshot hydrated from the saved session; main's layout was merged in only when a window
published. A routed reorder or Move to Split reached main's layout but not session.tabs.list or
the phone. Main now merges its layout into the rows it holds on each layout change and before
each list, with the same merge a window publication gets.

* refactor(session-tabs): keep the windowless merge with main's publisher, under the file's line cap

* WIP

* fix(session-tabs): every stored tab row follows main's layout, with or without a window

With the layout-owner switch on, the one writer of the rows phones read now merges main's
layout into every write for a workspace main owns, and main re-merges the held rows after
each layout commit on any host. A CLI rename, create or close reaches phones without a
window publish, and a row main's layout dropped cannot come back from an older snapshot.
A write that main's layout wholly overrides stores nothing, so list calls on settled rows
bump no version and push nothing. A pane's own title now counts as a manual name.

This commit also untracks a local vitest config and a scratch test that the previous
work-in-progress commit picked up by mistake.

* test(session-tabs): a rebuild from the saved session and a CLI rename keep main's groups and names on phones

* fix(session-tabs): the row writer only reorders and regroups; layout commits add main's new rows

With the layout-owner switch on, the one writer of the rows phones read now takes only main's
tab order and groups. It never adds or drops a row and never replaces a terminal id, so a
freshly started terminal keeps its id and rows the exit filters removed stay removed.

After each layout commit, on any host, the held rows take main's new rows, names and order,
keep any row main's layout lacks (closes are the close path's job), and pass any added
terminal through the same exit filters the window path uses. A main binding that runs nowhere
no longer replaces a running terminal's id. Both read main's last projection, so neither
adds a session read per write or per workspace.

A CLI close still leaves its row until the dropped-leaf hook lands; its test is marked as an
expected failure until then.

* test(session-tabs): the row writer's order and a commit merge's keep rules, unit by unit

* fix(session-tabs): a kept row keeps its group, and an unchanged commit pushes nothing

A layout commit's merge placed a row it kept (a tab main closed, until the dropped-leaf hook
removes it) by its row id, but groups list a terminal by its tab, so the row fell into the
active group. Both the writer's reorder and the merge now place rows through one shared
group-placement step keyed by the tab.

A commit now pushes only when the writer stored something. The writer skips a write only
when it keeps the stored occupant, publication and retirement proofs. A terminal row a commit
rebinds also passes the exit filters, a main binding the host saw exit no longer replaces a
running terminal, and a binding main made before its terminal connected reaches the rows when
it connects.

* test(session-tabs): a kept row's place and the no-push commit are separate tests

* fix(session-tabs): a rebuild with a new publication stamp is still skipped; simpler commit merge

The writer's skip compared publication stamps, but a rebuild from the saved session mints a
new stamp from the clock each time, so every rebuild minted a version and pushed. It now
stores only for a different workspace instance or new retirement proofs; an absent one keeps
what is stored.

The commit merge again runs the exit filters only for added terminal rows (they admit any row
the held rows list, so a rebind changed nothing), and a terminal connecting now schedules the
merge itself rather than a separate check, since an unchanged merge stores and pushes nothing.

* refactor(session-tabs): a commit merge pushes after every store again; name the window-path exit test

No reachable case was found where a commit merge's rows differ from the held rows while the
writer keeps the stored ones, so the extra "only when stored" check had nothing to guard and
goes. The exit-rule test now says it covers the window's publication path too.

* fix(session-tabs): a window reload's new publication stamp is stored, not skipped

The writer skipped a write that main's order wholly undid without comparing publication stamps,
so a window reload that republished the same rows under a new stamp kept the old one. The
reconciler then saw its accepted stamp missing and asked the window to republish on every sync,
and a host-built stamp could stay on window rows. The skip now treats stamps as unchanged only
when both are host-built, so repeated rebuilds from the saved session still mint nothing.

The windowed test of a row main's layout lacks also replays the window's older snapshot.
2026-10-21 10:53:40 -04:00
Jinwoo Hong ad269f471c A paired desktop shows a server's browser tab without opening a second page (dark, layout-owner build only) (#31509)
* fix(browser): a paired desktop shows a server's browser tab without opening a second page (switch-on builds only)

With the layout-owner switch on, a server browser tab drawn from the server's layout
reaches the desktop without a saved page handle. Showing it made the desktop create a
new page on the server, so the server went from one page to two.

Before creating, the desktop now asks the server for its browser pages and adopts the
one already named by the tab's page or browser tab id. It creates a page only when the
server has none, when the lookup fails, or when the server gives no list. A pane that
moved on during the lookup adopts nothing. Switch-off builds keep creating directly.

* test(browser): explain the fake runtime call's cast in the page session test

* fix(browser): adopt a server's browser page by the id its layout already names, not by listing pages (switch-on only)

This replaces the tab-list lookup. A tab list misses saved pages a restarted server has
not started, wakes the active saved page as a side effect, and can match a page another
client hosts.

The server's layout already records the page id on the browser tab. Desktop hydration
ignored it whenever the page record had not landed yet, and made up a new id on every
frame build. With the switch on, hydration now uses the tab's recorded page id, so the
desktop row is named by the server's page from the first frame and stays named by it.

A frame-drawn row still has no saved page handle: hydration has none for server-hosted
pages, and the server environment is known only to the pane. So, with the switch on, the
pane probes its row id with browser.tabShow. tabShow also starts a saved page the server
has not opened since a restart. On success the pane stores the handle and streams that
page. On not-found, or any other error, it creates a page as before and closes nothing.
A pane that moved on during the probe adopts nothing.

Why not reorder the server to save the page record before opening the tab: it would only
fix the id, not the missing handle, so the probe is needed either way. The page record
write also depends on the tab main writes asynchronously, so the order cannot be made
certain from the server alone.

* test(browser): explain the lifecycle harness's fake runtime call cast

* fix(browser): create a server browser page only when the server lacks it, and prefer the tab's active page id

The switch-on probe created a page after any tabShow failure, so a timeout or a brief
connection error could duplicate a page the server still runs. Now only a page-missing
answer creates one. Any other error is raised, as the stored-handle path already does, and
the pane shows its stopped state with Reconnect.

Hydration now names a tab's page by its recorded active page before its first page id,
which matches the page the server starts for a saved tab.
2026-10-21 08:32:17 -04:00
NeilandClaude 5dd5294294 perf(orcad): keep the server bundle ASCII so V8 stores it at half the size (#31495)
* perf(orcad): keep the server bundle ASCII so V8 stores it one byte per character

129 non-ASCII characters in regex literals (and three in a String.raw plugin
template) made V8 hold the whole ~11 MB orcad-server.js source as a two-byte
string. Write them as \uXXXX escapes and fail the build if any return.

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

* fix(orcad): keep license comments ASCII and name the module the guard trips on

esbuild gathers legal comments at the end of the bundle; escape that comment so
a dependency's license text cannot fail the ASCII guard, while licenses still
ship with the code. The guard now reports the offset and, from an unminified
rebuild, the module the character came from. A dark-switch build is covered.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 04:06:49 -07:00
Neil abce679f88 feat(accounts): add Claude and Codex accounts on a remote server from a paired client (#31461)
* feat(accounts): add Claude and Codex accounts to a remote server from a paired client

The server runs the login (Claude's pasted-code sign-in, Codex's device
code) and keeps the credentials; the client only opens the page and
relays the code. New capability-negotiated accounts.hostSignIn* methods;
older servers degrade to an update message.

Fixes #20009

* fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin

Review fixes: a cancel the login refused still reports the added account;
the client pins each flow to its pairing revision; Claude's stdin EPIPE no
longer crashes the host; both sides accept only the provider's sign-in
host; pasted codes reject whitespace; a host-side supersede says so; Codex
cancels its own login handle; opt-in real-CLI test.

* fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only

A Cancel clicked before the host answers the start is applied once it does;
the client keeps polling through a short outage until the host would have
ended the flow; Claude's link is read from stdout only; a timeout after the
code was sent keeps its own message.

* test(accounts): name the stdin sender and allow its stream write in the persistence ratchet

* fix(accounts): register each Add Account attempt before its first await

One attempt per click owns the pairing pin, the cancel flag and the host's
flow, so a Cancel during the capability check or the start is never lost;
polls retry only lost answers; the stdout forwarder is removed on cleanup.

* refactor(accounts): drive the client sign-in as one explicit state machine per server and provider

checking -> starting -> following -> settled, with Cancel as a flag the run
loop delivers once the host names its flow and re-asks after any answer
until the host takes it. A second Add joins the running attempt; a Cancel
the server could not be told about is surfaced; retry policy tested.

* fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends

* fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow

The host picker can move while a sign-in runs: Cancel now targets the
running server sign-in (or the desktop's own login when none runs), and
the pasted-code form is keyed by server and flow.

* fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning

* fix(accounts): the server sign-in prompt owns its Cancel

Cancel for a server sign-in now lives in the prompt bound to that server,
which also reappears with its Cancel when Settings reopens mid-sign-in; the
header Cancel is only for this desktop's own login.

* fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status

* fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs

* fix(accounts): the header Cancel follows the running addition, wherever Settings now points

Each Add Account records where it is signing in. Cancel stops that
sign-in, on a server or this desktop, and is hidden only where that
server's own prompt already offers Cancel.
2026-10-21 03:16:45 -07:00
Neil c149395c20 fix(remote-terminal): repaint an idle full-screen program when a reconnect can't image its pane (#31482)
* fix(remote-terminal): repaint an idle program when a reconnect can't image its pane

After an SSH reconnect the pane is repainted from the host's screen
snapshot. When the host cannot give one (too large for the replay limit,
no buffer, unanswered, overflowed), the pane only printed a loss banner and
an idle full-screen program that writes diffs kept its pre-disconnect frame
until the user resized.

The two places a remote pane declares it has no host image now ask the
program to repaint through one choke point: a one-row viewport claim and
back, which every TUI honours (Node's tty ignores a same-size SIGWINCH).
It respects a phone or another desktop holding the size, skips hidden
panes, and puts the row back even if the pane unmounts mid-nudge.

Fixes #26887

* test(e2e): idle full-screen program frame after a managed SSH outage

* test(e2e): wait for the pane before typing in the idle-frame spec

* fix(remote-terminal): pick the redraw height on the host's clamped grid

* fix(remote-terminal): wait for the host's grid before putting the redraw row back

* test: give transport mocks a connection status for the redraw gate

* test(e2e): keep the orcad idle-frame proxy typed for the e2e ratchet

* fix(remote-terminal): let a new PTY's redraw supersede a swapped-out nudge

* test: keep new transport-mock lines free of type assertions

* ci(e2e): route the managed-SSH idle-frame spec to its sources
2026-10-21 02:48:21 -07: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
Neil a58789d0c1 fix(mobile): keep a Codex chat addressable under Codex's own idle title on headless hosts (#31460)
On a headless host the mobile status builder treated any title without an
agent marker as the shell reclaiming the pane. Codex's idle title is its
project name, and since OSC 7501 program status gives the pane a PTY state,
a Codex turn with no tool call ended with the hook's transcript address
withheld, so the phone showed "Can't find this conversation yet".

Only a shell or management title now retires the hook identity, matching the
renderer projection. Refs #31136.
2026-10-21 01:22:04 -07:00
Neil 7f21cc91b8 fix(codex): Codex copy from a remote pane reaches the viewer clipboard (#31453)
* fix(codex): copy from a paired client's or WSL Codex reaches the viewer's clipboard

* test: type the Claude launch fixture
2026-10-21 00:58:03 -07:00
Neil c7f722bbdf fix(pairing): offer only addresses the runtime listener answers (#31447)
The runtime WebSocket listener widens to 0.0.0.0, which never accepts
IPv6, yet the Connection address picker and the default pairing address
offered global IPv6 interfaces, minting links nothing answers. Filter the
interface list by the family of the host the listener binds when serving
the network.

Reported-by: @fongyuan-2011
2026-10-21 00:11:21 -07:00
Brennan Benson 92732bb0f9 fix(native-chat): bring back Working-only-while-held (#30134) without shutting down Claude chat workers (#30577)
* fix(native-chat): a Claude chat worker keeps running after Claude acknowledges its task

Claude's adapter dropped an echoed send from the sends it holds before
reporting the send's answer to the host, and emitting the echo's turn
can publish at once. That publish read the still-pending send as held
by nobody, so it read as "couldn't confirm": orchestration worker-start
then closed the new Claude worker, and every other answer wait and
client saw a one-frame doubt.

A frame's settled sends now stay held until their answers are reported,
when the host's handover window takes them over; a Stop's rejected
sends are released and reported one at a time, as Pi and ACP already do.

Tracked in STA-11160.

* test(native-chat): assert the settled send's dispatch state through the send result

* Reapply "fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send (#30134)" (#30572)

This reverts commit 3ef5a46061.

* fix(native-chat): a Claude send stays held until its settlement reaches the host, on the reapplied held-sends read

* fix(native-chat): a Claude send stays Working, not in doubt, while a Stop is still ending its CLI

* test(pi): drop the removed idle-release dependency from the adapter fixture
2026-10-20 23:46:45 -07:00
Jinwoo Hong cfdc347bd8 Layout-owner build: main shows live terminals that lost their tab, at the same moments the window did (dark) (#31430)
* feat(layout): main places a swept workspace's live terminals; the window sweep only reads (switch-on builds only)

The window's activation sweep (first visit, an empty or sleeping activation, a phone wake) now
asks main, through its existing terminal.list call, to place that workspace's unshown live
terminals before listing them. With the switch on the sweep writes nothing to the window store and
counts a pane main names as shown, so it no longer seeds a fresh shell beside a terminal main just
placed. Main's "still binding" check also covers a registration in flight.

* fix(layout): a sweep's placement pass holds back only its own workspace's first surface (switch-on builds only)

* test(layout): switch-on activation-gate and census-call tests read main's listing, not window binds
2026-10-21 02:33:56 -04:00
Brennan Benson 505d6719c0 Refuse a workspace parent that would close a loop, even when the worktree cache is cold (#29340)
* fix(worktree): refuse a parent loop even when the worktree cache is cold

The parent cycle walk read each workspace's current instance from the
resolved-worktree cache, which the reparent itself invalidates, so it only
knew the two endpoints and accepted A->B->C->A. Read saved worktree
metadata instead.

* fix(worktree): fall back to locator metadata and tolerate loops saved earlier

* fix(worktree): skip another host's locator metadata in the parent loop walk

* fix(worktree): read loop-check identities from a read-only, ownership-scoped view

The per-host metadata accessor migrates a hostless legacy row into the
queried host, so the loop check could adopt another host's worktree and
refuse a valid parent. Read one read-only host snapshot instead, counting
a row only when its project is registered on that host (the resolved
list's ownership rule).

* fix: import host-qualified worktree meta helpers once

* test: match main's host-targeted lineage write contract
2026-10-20 23:23:02 -07:00
Brennan Benson fc8439e05f fix(notifications): a pane with hooks gets its finished banner from its hook turn only (#30620)
* fix(notifications): tie a completion banner to the hook turn it announces, not a 10 s window

A title-only working->idle edge (Claude's /btw side question fires no hook) raised a
generic "Task complete" banner because the pane's hook row was older than 10 s. On a
hook-reporting pane the hook row now decides: a row not yet announced supplies the
banner content however old it is; an already-announced row means no turn ended, so
no banner. Hookless agents keep the generic title fallback.

* fix(notifications): a pane with hooks gets its completion banner from its hook turn only

The title lanes (mounted pane and parked watcher) announced a finished task on every
working->idle title edge, even on panes whose agent reports turns through hooks. A title
spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra
banner that could only be filled with the workspace name and tab title.

On a pane the hook lane is tracking, with a settled Done row from the same agent, the title
lane now defers before it claims the turn, so the hook lane's banner (with the last message)
is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless
agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner.
This replaces the previous commit's announcement memory in the dispatcher.

* fix(notifications): hook-owned turn ends settle pane bookkeeping and announce re-entered Stops

- A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner.
- A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start.
- A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop.
- Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off.
- The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age.
- The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher.

* fix(notifications): the host marks each real turn end, so a repeated status can't raise a second banner

The renderer told a Stop re-entered after a blocking Stop hook from a
restatement by its final message. The message also changes when the
listener's cache resets (a session reset, then an idle reminder), so a
restated finished turn raised a second, empty "finished" banner.

The execution host now classifies each provider's own turn-end event by
name and stamps an optional `turnEndedAt` on the status row. Idle
reminders, session events, replays, child re-emits and result retries
restate it; a turn-end event on an already finished row is a new end only
when the agent says it re-entered (`stop_hook_active`). Local hosts and
SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it
at admission and sends it on its envelope. The field is persisted,
forwarded over IPC and the paired-server session projection, and the
renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the
existing `turnCompletedAt` precedence). `stateStartedAt` and every other
clock are unchanged.

Paired servers that predate the stamp advertise no
`agent-status.turn-ended-at.v1` capability; their panes keep the
terminal-title completion fallback exactly as before.

* test(notifications): type the old-server pane setup in the completion flow test

* fix(notifications): a blocking Stop's final banner reaches the user; /compact and quitting an idle agent stay quiet

Real-agent QA on the hook-only completion design found three gaps:

- A Stop re-entered after a blocking Stop hook could be dropped twice
  over: inside 1 s of the first banner by the replay guard (a new host
  turn-end stamp is now a real end), and by main's 5 s per-workspace
  desktop cooldown. A banner with the same notification id and changed
  text now replaces the one already shown instead of being dropped;
  every other cooldown rule is unchanged.
- On a mounted pane, a hook-owned idle edge (Codex /compact) no longer
  claimed a completion, so the burst's BEL went out as a Bell banner. The
  deferred idle edge now absorbs that BEL like a claimed completion did.
- Quitting an idle agent after a title spin raised a generic process-exit
  banner. On a pane whose hook lane owns turn ends and whose last turn is
  done, an exit ends no turn; a crash mid-turn still gets its banner.

* test(runtime): the projection reference keys on the host turn-end stamp too

* test(relay): restore the Copilot retry test while CI isolates a runner error

* test: key the hot-path projection reference on the turn-end stamp; relay retry restates the end

* fix(notifications): host turn identity per agent session; keep later blocking-Stop banners past main's cooldown

- The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an
  agent session is in: Codex by its own turn id, Command Code by its transcript prompt position,
  every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs
  from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn
  get their banner; duplicates, replays, tool reopens and result retries restate it. The record
  is saved with the status row (validated on restore) and never published.
- Main's desktop and mobile notification cooldowns are back to main's code exactly.
- A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands
  inside main's cooldown is held, latest only, and sent once after that cooldown through main's
  unchanged gates; the renderer learns main's answer to anchor it.
- One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that
  stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other
  pane, and an unbound PTY, keeps main's behavior.
- A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements
  ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn.

* test(notifications): run the whole-pane harness through main's shared desktop cooldown, not main's service module

* fix(notifications): decide hook ownership from how the host identified each turn end

The execution host now labels every classified turn end with one optional
disposition beside its end stamp, by the same rule for every agent:
identified (an unused prompt opening, a new native turn id, or a verified
re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate
only when the agent supplies both a turn id and a verified continuation
flag; anything else is ambiguous and never mints. Replays and retries
restate the decided answer, and local and relay hosts carry an end only
within a done segment.

The renderer replaces the Claude/Codex pane gate with one evidence
decision: identified ends are hook-owned, ambiguous ends keep the title
lane's role, and rows without a disposition (older hosts, unclassified
agents) keep the previous behavior exactly. A title working edge on a
hook-owned pane keeps its turn bookkeeping and only the hook receipt; an
owned idle edge settles after the existing max wait, or announces through
the title path if the end turned out ambiguous. Idle and working title
edges resolve the same owner (the title's agent, else the foreground one).
The held later banner for identified continuations is unchanged.

* test(notifications): give the whole-pane harness's host event its local connection id

* fix(notifications): a settled cancellation ends the open turn, so its late Stop folds as on main

A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner.

* fix(notifications): a settled cancellation ends the open turn on every lane, so a relay's late cancel end restates none

The desktop restates a WSL relay's end as decided. The relay never learns a
cancel the desktop inferred for a non-Claude agent, so Grok's own late
stop_cancelled reached the desktop as an identified end and gave a second
banner. One predicate now decides both lanes: after a settled cancellation
that carried no end, a restated end keeps the shown one, and a child event
carrying the cancellation also ends the open turn. The relay's own cancel
row is the main agent's, never a child's.

* fix(notifications): a relayed done segment shown without its own end keeps showing none

The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late
plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the
row's turn-end fact instead, which the folded row satisfies again.
2026-10-20 22:59:57 -07:00
Brennan Benson ae9bc8d120 A slept native-chat workspace reads asleep and hides (#28457)
* fix(sidebar): a slept chat workspace reads asleep and hides

* fix(sidebar): a wake during Sleep's teardown keeps the workspace awake

* test(sidebar): give the activation-race mock store the chat tab map Sleep reads

* feat(native-chat): the host marks the chats an explicit Sleep stopped

The fact "the user's Sleep put this chat to rest" now lives on the host
that runs the chat, on its session record, instead of in the window.

- New runtime capability agent-session.workspace-sleep-mark.v1 and an
  optional markAsleep on agentSession.sleepWorkspace (strict params, so
  only hosts advertising the capability are sent it).
- The Sleep close decides each chat's receipt inside the same per-session
  step that stopped it; with markAsleep it writes sleptAt in that step's
  record write, so a chat is marked exactly when it is in the receipt.
- The next send the host hands over clears sleptAt, whichever client sent
  it; the idle sweep and plain stops never mark.
- sleptAt rides the status summary (optional field), its equality, and
  the retained summary of a closed chat, so it survives restart through
  the persisted tab index.

* refactor(sidebar): read a slept chat from its host's mark, not a window record

The window no longer keeps its own saved record of which chats Sleep
stopped (explicitlySleptChatsByWorkspace). That record had to mirror every
host event that should end it (sends, replaced conversations, renames,
profile and WSL moves, removal, wakes during teardown), and each missed
mirror was a bug. The host now owns the fact (previous commit).

- Delete the record, its schema, store slice, workspace-session field
  registrations, moves, renames, purges, migrations and hydration, and the
  renderer clearing hooks. Those files are back to main.
- Opening a workspace no longer wakes its slept chats (product ruling):
  drop the chat Sleep-attempt tokens and their set-active-worktree hook. A
  slept chat wakes only when a send reaches its host, from any client.
- The status bridge mirrors each chat tab's published sleptAt into a
  store map keyed by the chat's pane key, dropped when its projection
  unmounts and never saved; a chat whose host runs it again is not slept.
- The asleep derivation, hide filter, palette, board and context menu read
  that map. Explicit Sleep sends markAsleep only to the local host and to
  hosts last probed with the new capability; a project move never does.

* fix(native-chat): preserve host-owned Sleep across fallback and legacy tabs

* fix(native-chat): clear Sleep when a new send is admitted

* chore: restore incoming documentation after merge hook formatting

* fix(native-chat): let admitted sends supersede pending Sleep receipts

* fix(sidebar): wait for host Sleep state before showing restored chats

* fix(sidebar): retain host Sleep answers when chat projections remount

* fix(native-chat): clear Sleep when queued drafts become sends

* test(sidebar): cover unknown and known chat Sleep state on the board

* test(vault): expect both host status capability checks

* test(native-chat): consolidate Sleep behavior coverage

* fix(native-chat): commit Sleep wake with send admission

* test: pin visibility while chat host is unidentified

* test: consolidate workspace sleep regression flows

* test: keep status feed regression within renderer boundary
2026-10-20 22:42:56 -07:00
Neilandbbingz e2196403c3 fix(terminal): realign mobile snapshot residual metadata after query stripping (#31348)
Align plain mobile/legacy residual metadata after snapshot-boundary query stripping; preserve genuinely transformed span metadata.

Fixes #9682. Based on the report and suggested correction by @bbingz.

Co-authored-by: bbingz <zzb@gxsmjx.com>
2026-10-20 21:41:02 -07:00
Jinwoo Hong 87fe8424eb fix(runtime): a server with no window answers its first terminal create, not 'handle stale' (switch-on builds only) (#31391)
* fix(runtime): a server's new terminal keeps answering under its own handle after main's pane adopts it (switch-on builds only)

* fix(runtime): keep handle branching on the leaf path; only callers with no leaf fallback read the adopted own handle (switch-on builds only)

* fix(runtime): with no window, a terminal's own handle keeps naming it after main's pane adopts it, as before main listed panes (switch-on builds only)
2026-10-21 00:34:08 -04:00
NeilandWooseong Kim 8bec7323ef fix(mobile): preserve Claude chat after renaming its pane (#31358)
fix(mobile): retain Claude chat when its pane is renamed
Distinguish a manual title override from a later shell OSC title, keeping transcript identity and live hook selection while preserving exit retirement.

test: cover restored mobile chat after manual shell-like rename

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-20 21:32:31 -07:00
Neil d1ed73ba9b fix(orcad): refuse new terminals while an update stops the server (R32) (#29834)
* fix(orcad): refuse new terminals between an update's last census and the stop

The last census before an SSH or WSL update stops the server now closes the
server's terminal admission first. Every spawn lease (acquireWorktreeTerminalSpawn,
which both spawn paths hold across the provider spawn) passes the fence, the
census waits for leases taken before the close, and an update that leaves the
incumbent serving reopens it. A reattach to a session the daemon still runs is
still admitted.

* fix(orcad): fence-exempt reattaches spawn attach-only; reopen admission when no stop was sent

* fix(orcad): stop nothing once the recount's admission fence may have lapsed
2026-10-20 21:23:41 -07:00
Jinwoo Hong ce71535bbf Layout groundwork (switched off): main starts every split and new pane, the window only attaches (#31258)
* 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

* 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(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.

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

* 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

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

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

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

* 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
2026-10-21 00:14:06 -04:00
Jinwoo Hong 046f29b9e5 fix(runtime): a phone's Markdown save keeps the route it started on (#31376)
forSave captures the workspace's route (and its pairing pin) once; every read, stat,
status.get and write of that save uses it, so a workspace that moves between local and
a paired server mid-save never sends the write to the other host unpinned.
2026-10-21 00:14:01 -04:00
Jinwoo HongandE2E Test b99552cabc fix(runtime): a phone reads and saves Markdown tabs on a headless server (#31101)
* fix(runtime): a phone reads and saves Markdown tabs on a headless server

With no window to relay through, markdown.readTab and markdown.saveTab answered
renderer_unavailable. Main now reads the tab's file (or its saved draft) from disk and
saves with a version compare plus the disk write precondition, in the shapes the
window's editor relay returns. With a window attached the relay is unchanged.

* Headless Markdown save: keep the draft's disk baseline, honour read-only tabs, re-find the record to clear

* Use the disk baseline only for a draft

* ci: run the phone Markdown spec's orcad case in the orca serve mode-switch job

* Headless Markdown: answer from disk only; a window's saved draft stays its view state

Clearing a saved draft wrote workspace layout from main, which no layout command carries.
Main now never shows or edits a draft; the desktop's restore check reports a disk change.

* Phone Markdown: one path through main and the disk, window or not

A paired server's workspace is read and saved on that server with files.read /
files.writeIfUnchanged, so it answers from its own disk and keeps the precondition.
The window relay (ui:mobileMarkdownRequest and the renderer bridge) is removed.

* e2e: a phone saves a Markdown tab the desktop holds unsaved edits in

The phone reads the disk with the tab marked dirty; its save lands on disk, and the
desktop keeps its draft and shows the changed-on-disk banner.

* Group the Markdown disk host's imports

* Drop an unneeded store cast from the dirty-readers test

* Phone Markdown: refuse a save on a server too old to check the disk; cover the untitled tab

A server before #30605 has no files.writeIfUnchanged, so the save is refused with the
existing update-required message rather than written unchecked.

* Phone Markdown: refuse a file an SSH host holds outside the workspace; pin the untitled answer

* Phone Markdown: untitled tabs stay read-only; older servers save like the desktop editor; a closed-source preview still reads

- The desktop lists an untitled document's tab with isUntitled (optional, additive); main answers
  it read-only with unsupported_untitled and refuses its save before touching the disk.
- A server without the files.write.disk-precondition capability gets the desktop editor's guarded
  save: compare from here, then a plain files.write, or a conflict. Every call of a save is pinned
  to the pairing it probed.
- A preview whose source tab was closed has no saved record; it reads its own file, read-only.

* e2e: read the untitled tab's answer without a cast

* Phone Markdown: pin a save's pairing before its first read

A save captures the server's pairing before it reads, and every call of the save, the read
included, is pinned to it, so a re-pair between the read and the write is refused.

* Test: with no window, a phone saves an existing untitled.md and never creates a deleted one

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 23:08:47 -04:00
Neil fece03b5ac fix(orcad): voice dictation on headless orca serve hosts (#31217)
* fix(orcad): voice dictation on headless orca serve hosts

Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.

The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.

The parity test now requires both roots to install speech.

Reported-by: delorean2020 (@delorean2020)

* fix(orcad): retry Node download drops, stage the recognizer on Windows, review fixes

- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
  interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
  in the profile, so a loaded addon never locks the app's files against
  an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
  helper per test file instead of repeated casts; comments.

* fix(orcad): cloud speech is ready only with a key this host can decrypt; prune superseded recognizer copies

Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).

* fix(orcad): never prune staged recognizers before the profile lock; copy linked packages as real files

Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.

* fix(orcad): remove a partial recognizer copy when staging fails (Codex round 4)

* fix(speech): never hand out another store's ciphertext as the OpenAI key (Codex round 5)

* refactor(orcad): no recognizer copy on Windows; Windows orcad keeps cloud dictation only

Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.

* fix(speech): name the fix when an old server refuses a mic tap; always set the recognizer env; retry ENOTFOUND (final Opus review)

* fix(managed-server): probe install completeness relative to the slot dir

CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.

* fix(mobile): pin host.platform's read to a platform type, independent of which zod copy resolves

With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.

* fix(mobile): keep the host.platform read line intact for its RPC mutant; narrow in a second step

Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.

* refactor(cli): app-executable resolution in its own module; drop the platform guard now zod is aligned

launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.

* refactor(cli): import app-executable helpers from their module; no re-exports from launch.ts
2026-10-20 19:54:55 -07:00
Jinwoo Hong 379ad0c5cd test(terminal-surfaces): pin why main's pane republish is deferred to a microtask (#31333)
The deferral keeps a terminal's connected flip, noted inside syncWindowGraph's
record update, from re-entering syncWindowGraph and recursing. Layout-owner
(switch-on) builds only.
2026-10-20 22:33:42 -04:00
Neil 018c6e0c42 fix(mobile): create and edit notes on headless hosts (#31291)
Create, read, and save Markdown notes on headless hosts through authorized file operations and persisted editor tabs.

Addresses the Markdown-note reproduction in #22186, reported by @shota46. Thanks to @Jinwoo-H (#31101), @brennanb2025 (#25436), and @Meapri (#23357) for related community implementations and regression coverage. Those PRs retain broader/distinct scope.

Validation: focused tests, paired-headless E2E, iPhone simulator proof, and all CI checks passed.
2026-10-20 19:30:42 -07: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
Brennan Benson 8e5992663b A terminal pane that loses its reconnect to another connection attaches to the winner instead of showing terminal_pane_owner_changed (#30448)
* fix(terminal): a pane that loses its attach to another connection attaches to the winner

When a terminal pane reconnected to a saved process that had exited, and
another writer re-bound the pane meanwhile (most often the window's own
session save), main threw terminal_pane_owner_changed. The window showed it
as a raw red banner over a pane that was otherwise working, and nothing
cleared it.

Main now re-resolves the pane's owner once and attaches to it, with fresh
absence evidence even when only the saved binding changed. Retiring a dead
id's binding is fenced on its incarnation so it can never clear a newer
process that reused the id. A second loss is still reported; the window
asks once more for a local pane (never a fresh launch) and otherwise shows
the existing reconnect banner with Retry instead of the raw code.

Tracked in STA-4139.

* refactor(terminal): move the stable-pane attach step into its own file (max-lines)

* fix(terminal): fence an absent owner's retirement on an explicit incarnation, not the binding's own field

* Complete bounded pane ownership recovery through host publication

* Apply repository style rules to pane recovery changes

* fix: preserve terminal adoption type contracts

* fix: complete terminal recovery ownership and acceptance contracts

* fix: keep terminal registration evidence with its candidate

* fix: satisfy terminal recovery lint gates

* fix: consolidate terminal recovery imports

* test: explain bounded terminal fixture assertions

* test: handle optional pane binding maps in assertions

* fix: scope remote pane ownership refresh to recovery epoch

* fix: reject remote pane replies after recovery epoch ends

* Fix static checks for pane recovery

* Reuse session partition assignment in binding retirement

* fix: acquire returned terminal receipt before rejecting early exit

* Release superseded terminal spawn preparation before retry

* test: type preparation cleanup callbacks as functions

* test: initialize telemetry classifier in preparation collision cases

* test: identify preparation resources in reentry assertions

* fix(terminal): let a recovered pane replace the binding its host proved dead

Reopening an agent terminal whose process had exited showed "Orca couldn't
verify this terminal's owner", and the next relaunch lost the tab. The pane
operation proved the old PTY absent, but a window session save wrote that dead
binding back before the fresh commit, and the commit treated it as a rival
owner. The refused candidate was then shut down without an intentional-stop
mark, so its exit retired the whole tab.

The operation now carries its absence proof into the fresh launch, and both the
pre-launch owner check and the commit accept a replay of that dead binding. A
different or unproven owner is still contention. Discarded candidates are
marked as replaced, so their exit keeps the tab.

* test(terminal): type the discard test's runtime mocks

* fix: expose reconnect for retained pane recovery

* test: expect replaced stop for discarded fresh terminals

* fix(terminal): type the absent owner's retirement fence after the main merge

* test(terminal): follow main's Reconnect handler in the pane binding ownership test
2026-10-20 19:09:03 -07:00
Kelvin Amoaba 1c76c71409 fix(mobile): list session tabs in the desktop tab bar order (#31269) 2026-10-21 01:59:24 +00:00
Brennan Benson 5c666d2974 fix(native-chat): retry a failed chat name, show the failure on the tab's hover card, and add Regenerate name (#30646)
* fix(native-chat): name a chat from its first message at once; the generated name replaces it

A native chat's one stored name was written only by a best-effort, one-shot
generation, so any failure left every surface on the 'Codex Chat' placeholder.
The first prompt now names the chat when its first turn starts, and the
generated name replaces it only while the field still holds that seed.

* fix(native-chat): the chat names setting says a chat shows its first message until it is named

* fix(native-chat): retry a failed first chat name twice before keeping the first message

A failed name generation was final for the chat's lifetime. The first name now
gets two retries, after 2 and 4 seconds, and stops early once the
first-message name was replaced.

* fix(native-chat): show a failed chat name on the tab hover card, with Regenerate name

A chat whose name generation failed after every retry gave no sign of it, and
nothing could run it again. The host now keeps the last failure in memory and
publishes it with the chat's status while the chat still has the name it failed
to replace; the tab hover card shows it with the error dialog, and a new
agentSession.regenerateName (behind a capability) makes one more try.

* fix: derive chat naming progress from host jobs

* fix(native-chat): merge duplicate import CI caught

* fix(native-chat): declare agentSession.regenerateName in the cross-version surface manifest

* fix(native-chat): stub naming-failure hook in SortableTab rename shortcut test
2026-10-20 18:34:16 -07:00