Commit Graph
1425 Commits
Author SHA1 Message Date
Brennan Benson 2fc2f92a38 test(e2e): follow intentional product changes in stale full-suite specs (#30897)
The daily full E2E run on main fails these specs every time because the
product moved on and PR CI only runs specs a PR touches:
- editor header menu renamed to File actions (#30129)
- terminal attention: an on-screen bell leaves no unread marker (#28637)
- terminal file drops carry their source host (#26830)
- single-choice questions auto-submit (#26288)
- phone terminal rows carry customTitle (#30037)
- gh/git preflight re-reads the login shell PATH (#25183)
- board drag starts after the target column is in view
- chat history anchor read after scroll settles
2026-10-20 12:41:00 -07:00
Brennan Benson ad1bcfe502 A chat's model, effort and Fast belong to the chat, not to a model (#30561)
* fix(native-chat): a chat's model, effort and Fast belong to the chat, not to a model

Structured chats kept their option values per model on the client, so a value
the host saved without a model (a resumed chat that never picked one, Codex
Fast saved on the conversation) had nowhere to live. The client either dropped
it or labelled the chat with the listing's default model to hold it.

- One flat record per chat holds model, effort, speed and Fast independently;
  a model pick changes only the model and the host reconciles the rest.
- Controls come from the model they apply to, separately from the values; with
  no model known, a saved Codex speed still shows and Standard turns it off.
- The options answer marks values a catalog default supplied
  (current.defaulted). A resumed chat never names or sends such a model as its
  own; a new chat still shows the default the host says it runs. An answer
  without the mark reads as before.
- Every send, including a new chat's first message, carries the chat's own
  model and effort and held picks, never a default read off the picker.
- A held launch pick supersedes only an earlier pick of the same option.

Tracked in STA-10947.

* fix(native-chat): a new chat's first send still pins the default model it shows

A new chat that never picked a model carries the default its picker names, as
before, so it keeps the model it actually ran after the CLI's default changes.
Only a resumed chat's defaulted model is never named or sent.

Tracked in STA-10947.

* fix(native-chat): narrow the host catalog before reading whether its listing names the default

The flag lives only on a listed catalog, so the check confirms the catalog is not the unknown
answer before reading it; the first-frame test reads the chat's flat model value.

* fix(native-chat): a reopened chat whose saved model was refused records the model it now runs

After a retired saved model is reset, the options answer no longer marks the model the child runs
as a default, so the next send records it instead of replaying the refused model on every reopen.

* fix(native-chat): a new chat names and pins the default it shows, and a model pick keeps only what the model offers

A chat this view started names the default model the host fills in, whichever of the model list and
the options answer lands first, and its first send carries the shown model and effort. A resumed
chat still names and sends only its own values. A model pick, held or live, keeps an effort, speed
or Fast only where the new model offers it, so a value the model cannot take is never shown, sent
or remembered.

* test(native-chat): give the no-Fast model fixture its required default flag

* fix(native-chat): an open chat's model pick leaves reconciling to the host

A live pick no longer drops values on the client; the host's reply, when it carries its full
value set, replaces the chat's own values as a read does. Only a pick held before launch, with no
host yet, keeps just what the picked model offers.

* fix(native-chat): a pick reply only adds the values it names

A pick's reply carries only the values the host set or was asked for, so it can't speak for the
rest: it no longer removes values it leaves out. Joined tests pin real reply shapes and a new
Claude chat in a fresh Claude home naming and pinning Opus High.

* fix(native-chat): a tier the drawn model doesn't offer shows as Standard

A Speed value another listed model offers but the named model doesn't is drawn as Standard, which
is what the host reconciles it to, while the chat's record keeps the host's value. Effort, a tier
no listed model offers, an unlisted model and a stand-in model's controls still show what the host
holds. After a lost pick reply the picker no longer shows the old tier as a raw extra choice.

* fix(native-chat): a never-sent chat names and pins its default after a reopen

The options answer no longer marks the default model and effort as stand-ins for a chat this host
started that has sent nothing, so a chat reopened after a restart or on the phone names them and
its first send pins them, as before. A chat taken from history, or one that has sent, keeps the mark.

* fix(native-chat): narrow the missing record as null and type the new-chat fixture's handle chain

* test(native-chat): the unloaded-turn context test's record store answers null for a missing record

* fix(native-chat): a chat's speed shows what its next turn runs

A Codex tier the drawn model lacks shows as Standard, which is how Codex runs it, except Flex, which
Codex passes regardless: established Flex keeps showing, and after a model pick the host hasn't
answered it shows no selection until the reply or a read. Each Codex turn sends the same effective
tier without rewriting the chat's saved one, and a new chat's create carries held picks as the
picker draws them, settling them so they aren't applied again after publish.

* fix(native-chat): one speed rule for picker, host and turns; never-sent needs proof; create keeps held picks

A Codex tier the model doesn't list shows and runs as Standard under one rule shared by the picker,
the host's live reconciliation, each turn and the at-rest answer, which keeps the saved tier for a
model that offers it. An older host's Flex shows as it reports it. A chat counts as never sent only
when no turn, rewind or untrustworthy journal says otherwise. A new chat's create carries held picks
as the picker draws them but keeps them as the user made them until create succeeds, so a refused
create and retry still has the user's own pick, and a newer pick still applies after publish.

* test(native-chat): type the resting journal mock and cut overlapping option tests

One typed resting-journal helper replaces six inline mocks and the broad object parameters the
low-evidence check rejects. Cuts tests the joined flows already pin: the resumed list-or-answer
ordering test, the restart-after-first-send view test, the held effort before a model pick, and
the model-pick and restart loop of the saved-Fast test.

* test(native-chat): cut option assertions the joined flows already pin

Drops the Codex Standard row (the joined tier test pins it), the dispatched-source and literal
effort-choice checks, and the resumed view test restart and model-pick steps; folds the pick table.

* fix(native-chat): an at-rest pick reply and answer name the speed the next start runs

The reply to a pick made at rest now carries the tier the chat model runs, through the same rule as
the at-rest answer, so an older app whose follow-up read fails shows what the next turn sends; the
saved record keeps the pick. A saved tier no listed model offers (a Flex from the user Codex config)
is the agent configured tier at rest too, as the live rule and the picker already read it. Restores
the pin that an accepted Codex reply naming Standard shows with no later read.

* fix(native-chat): a reopened chat with no saved model names its default at once

A reopened chat that never saved a model showed just "Model" until a send started its agent, while
main names the list default straight away. The picker now names the default the host marks for every
chat, as main does. Whether a send carries that default is a separate rule: a new chat sends it, so
its first send pins it; a reopened chat does not, so its next start still runs the agent's own.

* fix(native-chat): a chat with no saved model keeps the model its first turn ran

A chat that never saved a model followed whatever the agent's default was at each start, so a chat
resumed from history or a terminal could quietly switch models after a restart. When a message is
handed to the agent on such a chat, the host now saves the model that turn ran: the message's own
model when it carried one, otherwise the model the agent reports running. A default the host filled
in, no model at all, or a model the agent would refuse as a choice on later sends, is never saved. The write sets the model only while none is saved, inside
the record store's transaction, so a pick made meanwhile always wins and effort, speed and Fast are
untouched. One host step covers Claude, Codex, ACP agents and Pi. No new saved field.

* fix(native-chat): a chat with no saved choices keeps the model and effort its first turn ran

A Codex message that left the model or effort unset was filled from the model list's defaults, not
from what the running thread uses. Once the host saved the model a chat ran, the next Codex turn
carried that model alone and ran the list's default effort for it (a configured xhigh became
medium), and a message carrying only an effort ran the list's default model while the chat saved
the thread's. A Codex message now resolves an unset model or effort to the thread's own, and only a
switch to a different model takes that model's default effort, the same rule the other agents use.

When a chat with no saved model records what its first turn ran, it now records the effort, service
tier and Fast that turn ran too, in the same one-time store write: each only where the chat has none
saved, only from the message's own value or the agent's real report, never a filled-in default, so
a later change to the agent's configured effort no longer pairs the kept model with an effort it
never ran. No new saved field and no wire change.

A Codex follow-up sent while a chat's first turn runs is compared with what that turn actually runs,
so it steers instead of waiting for a new turn just because the host saved the model meanwhile.

Also fixes the delivery loop's optional recording hook type.

* fix(native-chat): key what a child ran by its provider child type

* fix(native-chat): record each option a turn ran where none is saved, and drop what a model pick can't run

A Codex message that only switched the tier set the thread's effort as a lasting override, so a later
switch to a model with no listed efforts was refused; the effort is again written only where the
model brought one or an earlier override needs replacing.

Recording what a turn ran now works per option: a chat that already saved its model still records
the effort it ran, and so on for each option it has none saved for. Tier and Fast come only from the
agent's own report, since a message steered into a running turn runs that turn's, and Fast is kept
only when the agent reports it actually routed on: an off state may only mean Fast is unavailable.

A model pick drops a saved effort, or Fast, that the newly picked model's listed row does not offer,
in the one host step both resting and live picks go through, so no later send carries a choice the
agent would refuse. The saved tier is kept, as before: it runs as the model's effective tier.

* fix(native-chat): save only options a turn really ran, and only ones the chat's model runs

A chat with a saved model recorded the effort its agent's settings named, not the one the turn
ran: with "effortLevel": "high" in Claude's settings and Haiku saved (no effort control), the chat
saved high and every later send was refused. A Codex thread effort its saved model doesn't list
did the same.

What a turn ran now comes from the agent's own dispatch: Claude reports the choices it applied and
the effort the CLI resolved for the model it applies, never the settings level alone; Codex reports
its final request over the thread's own model and effort, and a steer reports its running turn.
This evidence lives only in memory on the dispatch result; agents without it keep using their own
reports.

One admission rule now decides every saved option, inside the ordered record write: under a model
the saved catalog lists, an effort must be one it offers and Fast must be one it replays (Claude
needs positive support). A late record of a turn that ran on another model than the one the chat
now has saves none of that turn's options. A model pick drops inherited options its new model
doesn't run, an explicit pick or a new chat's option the model doesn't offer is refused with the
existing option refusal, and unknown catalogs or unlisted models pass as before. The saved tier
keeps its effective-tier rule, and nothing already saved is cleaned up later.

* fix(native-chat): an attach that founds a chat admits its options against the saved catalog too

* fix(native-chat): Claude and Codex always name what a turn ran, and picks are judged inside the write

The handover type was misnamed, which broke the typecheck. Claude and Codex now attach their
dispatch evidence even when it names nothing, so a Claude turn dispatched before its settings
readback never records the settings effort it did not run; the agent's own report still names the
model and stands in only for agents without dispatch evidence. Recording leaves the saved-model
and set-if-unset checks to the store's one admission rule. An explicit effort or Fast pick is now
checked inside the ordered write, with the same refusal, and only at rest: a running chat's own
menu can offer more than its saved catalog row (Pi's probe menu stops at high).

* test(native-chat): Codex admission tests accept the dispatch's run evidence

* fix(native-chat): a new chat's options are admitted against the folder list its picker showed

* fix(native-chat): a new chat's admission reads the list its picker read, resolved once by the create

* chore: re-run CI against main's formatting fix (#30918)

* chore: re-run CI against main's formatting fix (#30948)

* test(native-chat): explain the seed-options stub host cast

* fix(native-chat): attach keeps its two-argument call when there are no attach options

* Revert "fix(native-chat): attach keeps its two-argument call when there are no attach options"

This reverts commit e633d2065b.

* test(native-chat): create-path attach assertions check the params argument, not the arity
2026-10-20 11:55:34 -07:00
Jinwoo HongandE2E Test 893c4e24a4 fix(ssh): an old SSH host moves to its managed server while a relay shell named only on its tab still runs (#31093)
* fix(ssh): an old SSH host converts while a relay shell named only on its tab still runs

The conversion check counted legacy pane-key aliases as terminals waiting to
reconnect. An alias only routes hook events to a pane whose terminal the lease
check, the bridge or the terminal gate already accounts for, so it no longer
blocks the move.

* Tighten the alias census comment

* Register the tab-only relay spec: typed deployment lookup, relay fence baseline, PR route

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 14:18:04 -04:00
Brennan Benson 6c7a6ddd54 fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients (#29982)
* fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients

Orca cut Codex replies, every agent's reasoning, plans, prompts and all tool
output to a 16 KB head when it saved them, and threw the rest away. The
limit existed so a reconnecting client is not replayed a huge row, but it
was applied at save time, so every transport limit became permanent data
loss.

The journal now keeps what the agent said. One outbound form, applied to
every history page, live batch and catch-up, sends a client the same 16 KB
head of each tool output and diff it always got, and marks the item
`shortened`. A new capability-gated `agentSession.item` RPC returns the
whole item, and the desktop and phone show "Show full output" / "Show full
message" when the copy was cut. Items past half a page are shortened and
marked instead of replaced with "too large to show".

Two bounds stay, both named: a 4 MiB ceiling per text (head and tail),
applied where every provider's rows enter the write queue so one write can
never stall a chat, and a preview on Grok/OpenCode/OMP/Pi calls while they
run, whose closing row is built from the whole value the translator held.

* test(native-chat): an over-budget history item is shortened and marked, not replaced

* fix(native-chat): format, keep the sink and projection under the line limit, update method count and tests

* fix(chat): preserve complete writes and keep detail current

* test(chat): align final tail reservation with captured content

* Prototype source-batch journal ownership for Claude

* Pin the resolved iterator promise in ownership experiment

* Revert "Pin the resolved iterator promise in ownership experiment"

This reverts commit bdb49ab738.

* Revert "Prototype source-batch journal ownership for Claude"

This reverts commit e5ed387f0c.

* Preserve large received chat content while pacing input

* Verify received chat writes wait for journal persistence

* Close test journal databases before removing temporary roots

* Exercise item detail in the cross-version chat wire manifest

* Pin complete saved output and fitted client publication

* Exercise Pi exit retention with real journal write pressure

* Settle terminal web test unmount before DOM teardown

* Reuse existing readers and contract support in session tests

* Reuse measured chat item sizes in history pages

* fix(chat): derive page form type from reader contract

* fix(chat): open phone user messages while loading full text

* test(native-chat): reuse imperative context provider construction

* test(native-chat): format full-content disclosure fixture

* ci: preserve unit shard diagnostics after a timeout

* ci: retain unit shard output when tests time out

* ci: expose unit shard watchdog and output stages

* Revert "ci: expose unit shard watchdog and output stages"

This reverts commit 0b4991b144.

* Revert "ci: retain unit shard output when tests time out"

This reverts commit 87eec4c04f.

* Revert "ci: preserve unit shard diagnostics after a timeout"

This reverts commit 1888c656bb.

* test: verify complete Claude tool output without large failure diffs

* Simplify reply extraction in cross-version session test

* Restore full message recovery for grouped phone thoughts

* Target recovery button in grouped thought fixture

* test(native-chat): count agentSession.item alongside main's new structured method

* fix(chat): reuse strict session subscription schema

* test(chat): open Codex before checking received content
2026-10-20 11:13:48 -07:00
b8a5c8f910 A paired desktop draws a layout-owner server's workspaces from its layout stream (switch-on builds only) (#31181)
* review: shorten a test comment; the methods file header names layout.apply

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

* wip(h5b): a lost remote layout stream reopens with the local backoff

* wip(h5b): server frame id translation (pending the binding handle)

* wip(h5b): frame syncs take a source; the mirror gates frame-drawn server workspaces and indexes handles; ids translate with session.tabs handles

* wip(h5b): one layout stream per capable paired server, handing workspaces between frames and the mirror

* wip(h5b): tests for server frames; layout.getRemoteSnapshots returns only paired servers (L18)

* wip(h5b): re-gate a server workspace when its routing arrives after the snapshot; e2e hook and spec

* wip(h5b): late-owner test; e2e spec on the switch-on leg

* wip(h5b): server streams follow every paired capable server, not the focused one; a known-kind guard without a cast

* wip(h5b): server-frames tests read real paired-server state; no casts

* fix(h5b): a handed-back workspace is the mirror's alone; frozen frames draw only gated workspaces

* fix(h5b): a workspace rejoining the gate takes the full build

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 14:09:35 -04:00
Jinwoo HongandE2E Test 98e7dbecfe The window applies each layout update record by record instead of rebuilding from its whole session (dark, layout-owner build only) (#31152)
* 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)

* Name the pane tree the window compares by what it holds: the tree without its panes' bindings or live titles (dark, layout-owner build only)

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 11:28:45 -04:00
Jinwoo HongandE2E Test b1b4c785c0 test(e2e): the old-phone layout baseline expects a layout-owner build's file open to succeed (#31138)
Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 10:09:52 -04:00
Jinwoo HongandE2E Test bfc436be60 test(layout): a serve restart that lost a pane's binding shows the terminal ready at once in the layout-owner build (#31132)
In the layout-owner build, main's load binds the pane again from its tab row while the
terminal is alive, so the replacement host publishes the pane ready and never pending.
The serve-restart spec waited for a pending frame that this build correctly never sends.
It now expects pending only in the current build, and asserts no pending frame in the
layout-owner build. A unit test pins the precondition on a windowless host: main's own
writes leave no shutdown list, and a real store load restores the binding.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 09:53:52 -04:00
Jinwoo HongandE2E Test b887483e55 fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows (#31102)
* fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows

The phone built its project sections from a catalog the desktop window published through a
side channel (mobileRelay.projectSections). That copy changed on a timer after the event that told
the phone to re-read it, and a re-read asked for during a read in flight was dropped, so the phone
could keep an old grouping (a project under the wrong group, a group missing) indefinitely.

Main now publishes the catalog (repos with their group and order, project groups, projects and
host setups, every host's including paired servers') as a projectCatalog domain of the sidebar
feed, from its own store and its own reads of each paired server. The phone reads it from the same
install as the rows. mobileRelay.projectSections stays, answered from the same builder, for pages
that predate the domain; a page on a desktop without the domain still reads it, now re-reading
when an invalidation lands mid-read. The phone e2e list helper runs the page's real refresh
lifecycle instead of re-reading on every assertion, and covers a group move while a read is held.

* test(mobile): stand AppState in for the screen hooks' react-native mocks, and tolerate a world with no project rows in the parity catalog

* fix(sidebar): main files a server's copy of a grouped project under its group, and reads a server's catalog only on catalog events

- The window gives an ungrouped paired-server repo the group of an existing copy of the same project
  (applyInheritedProjectGroups). That function now lives in src/shared and main's catalog builder
  folds each server's repos through it, so the phone files the copy where the desktop does.
- A paired server's change watch now says whether the event can carry a catalog change. Only
  ready, a replay and reposChanged mark the catalog stale; worktreesChanged (pins, unread, sort
  stamps) reads rows only.
- The parity fixture now builds the server side from raw server answers, folded through the
  window's mergeFetchedReposForHost and through main's own server read and builder, with a server
  copy of a grouped project and a partial saved manual order. It fails without the inheritance.

* fix(sidebar): parse a paired server's catalog rows with a schema instead of reading keys reflectively

* fix(mobile): sync the feed ref after render and re-read queued project sections without an await loop

* fix(mobile): spell the AppState shape the refresh reads, so the e2e helper pulls in no React Native types

* test(sidebar): give the merged builder test's feed domains a project catalog

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 09:34:02 -04:00
Jinwoo HongandE2E Test 2d461d20be test(e2e): the old-phone layout baseline lists customTitle on terminal rows (#31127)
#30037 made the headless row builder emit customTitle on every terminal row; the Linux baseline
recorded before it (#27688) never listed the field, so the spec fails on main with either layout
switch.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 09:05:04 -04:00
Neil 49a2203b0e Remote Orca Servers: one list instead of six sections; SSH Hosts: bulk remove that stays removed (#31082)
* feat(settings): one-list Remote Orca Servers page and bulk-manageable SSH Hosts

Remote Orca Servers: replace the three mode cards, Advanced fold, duplicate
managed/SSH-access lists and troubleshooting accordion with a single list
(one row per server: status, one action, overflow menu), an always-visible
'New projects open on' row, and Share / Cloud VM cards that open dialogs.

SSH Hosts: group by source, filter, checkbox + shift-range selection, one
confirm for bulk remove (skipping hosts with a managed server or
workspaces), bulk Test. 'Sync ~/.ssh/config' no longer clears removal
tombstones; 'Restore removed hosts (n)' is a separate confirmed action.

* fix(settings): address review findings on servers and SSH hosts redesign

- SSH filter stays clearable when the list shrinks under the threshold
- Cmd/Ctrl-A only selects hosts when pressed in the list itself
- group legacy no-source hosts with main's import predicate (now shared)
- capture the shift-range anchor before queuing the selection update
- blank server names de-duplicate instead of colliding
- Share dialog Done refreshes link count and resets name editing
- Install / SSH-route dialogs reload SSH hosts when opened
- default-host row shows the selector when the default is a WSL host
- managed 'Not running' no longer masks connecting/revoked/update states
- servers without a dropped transport can be re-checked

* test(e2e): WSL host spec matches repo.rm ending a removed project's shells

repo.rm now stops the project's terminals on the host (#29964), so the
'orphan left by older builds' setup can no longer be built; use a shell
only the host knows of instead. Resolve the project path so macOS
/private/var matches what the host reports.

* fix(settings): carry 'Orca on <host>' naming and paired-twice warning into the new server list

Ports #31099 and #31100 into the redesigned rows, install dialog and SSH
host dialogs.

* test(e2e): keep the WSL spec's project path as created
2026-10-20 05:22:39 -07: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
NeilandClaude 0ee13f831d fix(ssh): say "Orca on <host>" instead of "managed server" in the UI (#31099)
* fix(ssh): say where Orca runs instead of "managed server" in the UI

Users read "managed server" as hosting run by someone else, so they declined
moving their SSH hosts. Every user-facing string now says "Orca on <host>" or
"this host's Orca server", in all six locales; Settings > "Orca on your SSH
hosts" and the setup line say the code and files stay where they are and only
Orca's list of projects, tabs and terminals moves to the host. CLI command names,
logs and identifiers are unchanged.

Fixes #30880

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

* fix(ssh): reassure only when something moves; name the host's label in its row

Review follow-ups: the reassurance shows only while Orca sets itself up or
moves a host, not on every reconnect; the converting line no longer repeats it;
the row names the SSH host's current label; the section description and the
Move anyway dialog stop repeating themselves; translate the Restart dialog's
buttons; drop "managed" from the remaining raw causes and the external
automation refusal.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 04:18:08 -07:00
Neil ba6599eabb fix(ssh): pair again over SSH when a managed server drops this desktop, instead of reconnecting forever (#30775)
* fix(ssh): say the host dropped this desktop's pairing, not that another Orca holds the port

* fix(ssh): pair again over SSH when our own managed server refuses this desktop's token

* fix(ssh): key re-pairs by the refused revision, cap attempts, retry on explicit connect

* fix(ssh): keep a newer pairing stored mid-repair, count only completed reads, and offer Connect, not an access link, for managed servers

* fix(ssh): judge a re-pair only against the pairing still stored; Check connection retries a managed server's pairing

* fix(ssh): a pane whose managed server refuses this desktop says why and offers Reconnect, instead of spinning

* test(e2e): live failure path for a refused managed pairing

* fix(ssh): a Connect that re-paired answers from the new pairing, not the retired one

* fix(ssh): only an explicit Connect lifts a managed re-pair's hold and cap; say SSH access can pair again

Startup restore reaches ssh:connect with automatic: true and went through the same
ensureTunnel that cleared the hold, so repeated automatic reconnects could bypass the
three-attempt cap. The reset now lives in connectTarget's explicit branch.

The Shared Server Access footnote now says revoking there doesn't stop someone with
SSH access to this computer from pairing again.

* ci(e2e): route the managed pairing-repair spec with the managed-host remote sources

* fix(ssh): the repair gate decides whether a managed tunnel builds, not the build

A refused shared-control open rebuilt the tunnel (SSH execs, forward, handshake, wake)
before the held failure or the attempt cap was consulted, so reopens on a blocked
server rebuilt without limit. buildWithPairingRepair now asks the gate first: a failure
stands for the refused pairing revision for 15 s, or through the window once the cap is
spent, and every caller gets it without reaching the host.

- A connect during an in-flight repair keeps its reset (per-environment reset epoch).
- An unpaired answer fails at once; it is not a moved port, so no re-read or wake.
- A refusal that only rebuilt the tunnel no longer reports "pairing changed".
- WSL and cloud VMs no longer say Orca tried to pair over SSH, and Check connection
  only re-pairs servers managed over SSH (runtimePairingRepairRoute).
- Translate the new strings; route the pane-banner files to the managed-SSH e2e lane.

* fix(ssh): fence a status re-pair follow-up to the same server, and hold a refused repair under its own revision

- getRuntimeEnvironmentStatus followed any pairing change mid-probe, past a caller's
  expectedEnvironmentPairingRevision fence. It now follows only for an unpinned caller and only
  a new pairing of the same SSH-managed server under its pinned host key.
- recordRefusedAfterRepair stood under whatever revision was stored now, so a newer pairing
  stored mid-refusal was blocked untried; it stands under the repaired revision.
- The WSL restart test expects the refused-pairing classification main's WSL path now reaches.

* fix(ssh): a re-pair hands its callers the new credentials, and a failed one judges only the pairing it tried

- ensureOrcadManagedTunnel returns the environment as stored once the tunnel stands. The
  terminal census and a deploy's already-current result use it, so after a re-pair they
  no longer authenticate with the refused token, which read as an unverifiable census and
  refused restarts, updates and stops.
- The shared-control refusal handler blocks the status owner only while the failure stands
  for the pairing stored now (managedOrcadPairingFailureStands). A newer pairing stored
  meanwhile keeps its owner and retries.

* fix(ssh): migrations commit with the pairing the tunnel stands on, and a Connect mid-handshake stays reset

- Conversion binds the environment ensureOrcadManagedTunnel returns, so the destination, the
  server project reads and the result use the re-paired credentials (the migration mark is kept).
- A delta move gets its destination only from connectDestination, which opens the tunnel and
  builds the destination from what it returns; a pre-ensure destination can no longer exist. A
  tunnel that cannot open keeps the delta journaled and the host marked for the next move.
- buildWithPairingRepair takes the reset epoch when it starts, so a refusal after the re-pair
  cannot reinstate a failure that a Connect pressed during the handshake cleared.

* refactor(ssh): one module owns a shared control socket's environment policy, keeping request-connections under its line limit
2026-10-19 22:45:25 -07:00
Neil e278bc9a17 feat(orcad-migration): unsaved drafts stay on this desktop and are renamed to the server on commit (#30894)
* feat(orcad-migration): unsaved drafts stay on this desktop and are renamed to the server on commit

* fix(orcad-migration): refuse clashing drafts from the rename's own collection; stricter live spec

* fix(orcad-migration): stamp renamed drafts with the server, rename at startup after a cut-off commit, keep draft refusals

* fix(orcad-migration): retain only after a startup rename is durable; startup survives an unreadable journal

* fix(orcad-migration): a cut-off commit is renamed and retained at startup, so the rename runs once

* fix(orcad-migration): one retain per migration at a time; no rollback that could drop newer edits

* test(e2e): check the visible draft in whichever editor a large file opens in

* fix(orcad-migration): startup retains only migrations of hosts this profile owns

* ci(e2e): route the editor-drafts migration spec to the sources it covers
2026-10-19 22:44:43 -07:00
Jinwoo HongandE2E Test 23d68112df Restored terminals named only on their tab reattach after a restart (dark, layout-owner build only) (#30986)
* fix(layout): restored terminals named only on their tab reattach after a restart (dark, layout-owner build only)

In a layout-owner build, a terminal recorded only on its tab (an older session, or an SSH tab
whose relay session was never visited) started a new shell after a restart and orphaned the old
one, and an unvisited SSH tab lost its relay session id on the next save.

- Main's Loader binds a one-pane tab's own terminal to its unbound pane in a workspace that was
  live at shutdown, as the window's reconnect does, so main's frames and saves hold it.
- The window's reconnect writes the restored row terminal again: ptyId is a field the frame merge
  leaves to the window, so main's frames never set it on a restored row.

* fix(layout): main binds a restored tab's own terminal once at load, not on every Loader run (dark, layout-owner build only)

The Loader re-derived the binding on each load from fields the window rewrites live, so an exit
main recorded could be undone. A switch-gated Store load repair now binds a one-pane tab's own
terminal to its unbound pane, as the window's reconnect does, and main's saves keep it. It skips
a terminal another mounted pane or a tab-bar tab holds, and a paired server's partition.

* fix(layout): an SSH workspace's restored tab binds only its relay session id; repair runs after the load purge (dark, layout-owner build only)

* test(layout): the reconnect gate test expects the row ptyId write; cover the repair's purge order and SSH repo predicate

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 00:41:41 -04:00
Jinwoo HongandE2E Test 920d1e9118 fix(status-bar): keep this computer's providers visible as "Not configured on <host>" under a remote workspace (#30858)
* wip(status-bar): remote empty usage state names the host and opens its accounts

* fix(status-bar): keep this computer's providers visible as "Not set up on <host>" under a remote workspace

* test(e2e): paired client keeps a this-computer-only provider as a host-named placeholder

* fix(status-bar): place usage placeholders only on the host's own 'unavailable' answer and open Settings on the owning host

* fix(status-bar): label the not-set-up placeholder chip "Not set up" instead of "--"

* fix(status-bar): placeholder only on a not-set-up reason; compact chip keeps "--"

* fix(status-bar): word the placeholder "Not configured on remote host"

* fix(status-bar): name the host in placeholder chips and keep Claude as 'No usage from <host>'

Only the host name truncates, so the phrase stays readable in every locale.
Claude set up here shows a placeholder when the host reports no OAuth login,
which reads the same as API-key billing, so it never says 'not configured'.

* test(status-bar): cover the Claude 'No usage from <host>' chip end to end

Pins USER on both apps so the runner's real Claude Keychain login cannot leak into
the host or client, seeds a refresh-token-only login on the client, and checks the
chip, its tooltip, the popover row, and that the chip goes away without Claude here.

* refactor(status-bar): share the owner placeholder snapshot and drop a dead Claude check

* fix(status-bar): build the owner placeholder tooltip in the current language

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 00:15:37 -04:00
0ee0290cc3 feat(accounts): save MiniMax and GLM Coding Plan keys on the Settings host (#30862)
* feat(accounts): save MiniMax and GLM Coding Plan keys on the Settings host

The usage poller on an SSH, `orca serve` or paired server reads these keys from
that machine's ~/.orca, but Settings could only write them on this computer, so
those hosts never showed the meters. New accounts.* RPC methods (advertised as
accounts.usage-credentials.v1) save, clear and report the keys on the host and
refresh its usage. Settings routes the MiniMax and GLM sections, and their
endpoint/site settings, to the host picked in the page; an older host is asked
to update instead of the key being saved locally.

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

* fix(accounts): a host whose desktop owns its accounts refuses usage keys instead of storing them unread

A WSL host's server has no usage poller, so a key saved there was reported
saved yet never read. Every usage-credential method now throws the same
desktop-owns-these-accounts message as the other account methods, and the
host stops advertising accounts.usage-credentials.v1.

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

* fix(accounts): a paired client never overwrites a usage key the other Orca app on that machine sealed

The desktop app (safeStorage) and orcad (its own host key) share ~/.orca but
cannot open each other's sealed files. A remote save or forget of a MiniMax or
GLM key that this host cannot read is now refused, and the status reports it so
Settings locks that key and says to change it in the other app. Blank keys now
fail parameter validation.

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

* fix(accounts): only blame the other Orca app for a key it really sealed, and always allow forgetting

Any read failure was treated as "saved by another Orca app", so a corrupt file,
a permission error or a host that cannot decrypt at all locked the key with a
false message and refused Forget, the only recovery. The host now reads the file
itself: only a well-formed encrypted envelope that a working store cannot open
counts as sealed elsewhere and blocks Replace; anything else is reported as
unreadable and stays editable. Forget is always allowed, and the check logs
nothing per status poll.

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

* test(e2e): fail clearly when the orcad slot is missing instead of passing null as the program

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

* fix(settings): name the host in remote key descriptions, give the MiniMax API key its own failure toast, and treat malformed sealed files as unreadable

---------

Co-authored-by: Jinwoo-H <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 23:46:28 -04:00
Neil 9a79de59a5 feat(native-chat): apply per-agent Auto permission defaults (#30853)
* feat(native-chat): sync per-agent permission defaults with explicit host support

* test: include native permission capability in host settings mock
2026-10-19 19:58:00 -07:00
Neil 912c13036b test(e2e): measure typing-time renderer stalls with long tasks, not interval drift (#30971)
In the hidden E2E renderer the 16ms interval probe measured timer
scheduling: a CI trace shows a 268ms interval gap while every keystroke
echoed within 45ms. Long-task timing reports how long the main thread was
actually blocked, which is what the 250ms and 3.5s budgets are meant to
bound. The report field keeps its maxTimerDrift name so the perf report
pipeline still reads and budgets it.
2026-10-19 19:41:41 -07:00
Jinwoo Hong 3ce80124c8 fix(notifications): a server with no window pushes phones when a terminal agent finishes or needs input (#30602)
* fix(notifications): a host with no window pushes phones when a terminal agent finishes or needs input

* test: name the cast rationale in the host-delivered notification test

* fix(notifications): wait out a finish's quiet window and match the window's request by its news id

* fix(notifications): announce a lead agent's finish while background work runs, not its all-clear

* fix(notifications): announce a lead agent's finish at once and keep it through the all-clear

* fix(notifications): the host pushes only while no window runs, with the window's rules; a session boundary is no finish

* chore: drop formatter churn in files this change does not touch

* test(e2e): record the window's own notification requests in main

* fix(notifications): a desktop whose window has not shown stays silent; seed restored panes as the window does

* fix(notifications): seed a restored pane fully, and read a listed worktree's own repo
2026-10-19 22:37:43 -04:00
Neil 929dededc5 test(e2e): give the ACK-backpressured typing scenario the under-load timer-drift budget (#30968)
The main-pressure scenario holds background PTYs behind the ACK gate on
purpose, but it passed the unloaded 250ms drift budget while every other
under-load scenario uses 3.5s. Two unrelated PRs failed it today at
252ms. The 75ms median key-latency check still guards responsiveness.
2026-10-19 19:32:13 -07:00
NeilandClaude 11b925d5a1 fix(ssh): a legacy relay terminal stays with the relay that runs it when another relay lists the same id (#30870)
* fix(ssh): a legacy relay pane stays with the relay that runs it when another lists the same id

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): keep an exit reported before a pane's owner is known, and re-list a contested pane

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): a legacy relay id's owner survives reconnects, is adopted only once every relay answered, and a contested id is never served

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): pin a legacy relay id's owner only on a listing that accounts for every relay

Co-authored-by: Claude <noreply@anthropic.com>

* test(ssh): the cross-version bridge test passes the target's owner ledger

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): an epoch-scoped relay id keeps its sole lister as owner on an incomplete listing

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 19:13:05 -07:00
Neil 8d0501622a feat(agents): add Auto to the global agent permission switch (#30677)
* feat(agents): configure Auto terminal permissions per supported agent

* feat(agents): restore global permission switch with Auto, per-agent exceptions

Replace the Set defaults menu and inline per-row dropdowns with a global
Manual | Auto | Yolo switch. Auto reveals an Other agents control beneath it,
mixed states badge exceptions, and each row's expanded panel owns its
per-agent permission choice.

* test(e2e): fail loudly when the store is unavailable in the Auto permissions spec
2026-10-19 18:15:07 -07:00
Neil da0db9fc73 fix(ssh): orca open-url on a managed SSH host opens the page on the desktop through the browser bridge (#30849) 2026-10-19 16:33:20 -07:00
e9fa0cf95c fix: a tab close by the handle a split returned finds the new pane (dark, layout-owner build only) (#30786)
* fix(renderer): list a pane main holds before the window mounts it, so a tab close by a split's handle finds it (dark, layout-owner build only)

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

* test(e2e): the split-close spec polls through the host's "can't answer yet" terminal list

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

* fix(runtime): a pane the window lists before main binds its terminal keeps main's terminal (dark, layout-owner build only)

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

* fix(runtime): a tab close by a main-side split's handle stands on main's own record of the pane (dark, layout-owner build only)

Replaces the window-side listing and the binding fill: the window's list lags main in three
ways (built before main wrote the pane, from it before the binding, or before the window mounts
it), and only main's own layout answers all three.

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

* fix(runtime): main's relisting for a close tells no clients (dark, layout-owner build only)

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

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 15:41:18 -04:00
Neil eedca5ee1c fix(persistence): worktree meta and lineage writes take the row's host, not the id (R4-2b) (#30811)
* fix(persistence): worktree meta and lineage writes take the row's host, not the id (R4-2b)

* fix(persistence): review fixes for host-targeted worktree writes

* test: SAFETY notes on partial runtime store mocks

* test: drop imports duplicated by the rebase onto #30797
2026-10-19 11:39:54 -07:00
Neil 86ef00ad55 fix(projects): a project move names the row it moves; a paired server's twin by its SSH target (#30794)
* fix(projects): a project move names the row it moves; a paired server's twin by its SSH target

* fix(sidebar): pass the header's row without an assertion; e2e helpers and focus baseline follow the row-shaped move

* fix(sidebar): a header drop re-reads the dragged row and drops nothing if its group changed mid-drag

* fix(sidebar): read live repos through a stable store reader, not a ref written during render
2026-10-19 11:28:29 -07:00
Brennan Benson 53191250e8 Accept messages while any agent starts, and bound each startup step (#28046)
* Finish structured agent readiness before delivering held work

* Publish the Codex process before protocol startup

* Publish ACP agent processes before session startup

* Publish Pi at spawn and settle signed-out starts once

* Adapt permission fixtures to startup lifecycle

* Keep model discovery for chats before their first start

* Use one host readiness promise for provider operations

* Await provider opening in the execution host

* Await Claude initialization in host-owned open

* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Await Codex protocol open under host ownership

* Await ACP protocol open in the host

* Derive ACP opening from its protocol session facts

* Exercise unanswered ACP options with an explicit silent route

* Await Pi open and fail signed-out construction

* Read Pi catalog facts from awaited opening

* Keep the Pi fixture exit observer

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Capture message choices per accepted send and preserve picker state

* Apply accepted Claude message choices at prompt handover

* Apply captured message choices at provider dispatch

* Bind native chat choices to accepted message dispatch

* Reset Codex turn choices for each captured message

* Resolve Codex message defaults from native facts

* Align message choice and Pi default fixtures with provider dispatch

* Align option fixtures with saved message choices

* Match Codex message choices to the active turn origin

* fix: let pending commands reach handover refusal

* Abort pre-prompt message choice writes with host-owned wait

* Verify message choices through provider input boundaries

* Keep legacy mobile test hosts explicit about message choices

* Keep startup waits cancellable and preserve operation order

* Derive Codex startup exits from host phase

* Open Codex protocol in background-task test fixtures

* Keep ACP startup ownership until handshake completes

* Run ACP opening catalog fixtures with Node SQLite

* Verify saved ACP picks without live option writes

* Keep ACP catalog bookkeeping from failing an options read

* Restore signed-out restart and fail Pi startup faults

* Run Pi opening catalog fixtures with Node SQLite

* Preserve message choices across cold clients and kept cards

* Remove the unused provider option restore ledger

* Clarify launch choices and legacy option hydration

* Remove remaining restore-ledger forwarding and assertions

* Remove unused ACP and Pi restore bookkeeping

* Keep live choices and delivery order while agents start

* Drop retired startup revision from Codex opening

* Keep live-option cancellation until message choices and clarify catalog reads

* Hold ACP option replies in live-write cancellation regressions

* Remove retired Pi startup revision and match saved-pick behavior

* Check running choices and retained rewind messages after delivery integration

* Apply startup picks and let parked messages release operations [skip ci]

* Remove unused Codex opening report field [skip ci]

* Keep ACP restoration facts local to the provider [skip ci]

* Apply Pi startup picks after readiness and remove unused open facts [skip ci]

* Cover goal changes during the held Codex message wait [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Release scripted Codex startup before awaiting queued picks

* Check the saved picker selection in per-message choice fixtures

* Exercise current choices and eager client capabilities in runtime fixtures

* Reuse runtime fixture reads and type the fake connection directly

* Align startup fixtures and preserve observed Claude exits

* Honor queued send authorization in Claude runtime fixtures

* Open Codex protocol sessions before Stop fixture dispatch

* Wait for the opened Codex child before runtime notifications

* Keep repeated permission picks usable and align Codex fixtures

* Validate pending picks against the live option state

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Keep Codex startup flow contracts and remove overlapping adapter tests

* Restore the committed dependency lockfile after test tooling

* Keep startup recovery flows and remove duplicate Pi handshake cases

* Cover both Pi startup choices in the delivery flow

* Match the Pi prompt entry recorded by the scripted wire

* Honor the before-input callback in the startup reply fixture

* Keep message choice flows and remove overlapping selector tests

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Match startup picker fixture to saved message choices

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Keep the Codex journal translator within its line limit

* Match ACP startup fixtures to saved picks and late Stop behavior

* Check startup effort after saved picker choices are acknowledged

* Complete Stop target types and keep the effort regression concise

* Use requested picker values in the effort regression acknowledgement

* Require owned provider spawn and one host-awaited open (from #28055)

* Restore the live checks the startup cleanup's deleted tests pinned

One assertion each, in the surviving suites: a stale-generation exit is
ignored, a failed create lets its sink go, a readback before open is saved,
an idle stop that fails is retried, a no-pick Pi start teaches its default,
an older client's pre-spawn failure and packed answer, an undrivable chat
stays readable, the resting slash menu stays on its own host, the main agent
reads apart from subagents, restore publishes status, naming runs off the
host's status feed, and a throwing logger never reaches the operation.

Also corrects the reliability gate note that claimed the send-idempotency
suite replaced the refusal-retry tests.

* Read a rewind in doubt outside the chat's serialize and apply it in the commit

The started settle and the attach's ready-child path read the provider's
history first, then apply what it proves in a short commit that re-checks
the rewind it read.

* Accept messages while the agent starts: run the start outside the chat's serialize

The chat's serialize was both where a send was admitted and where its agent
started, so every slow step of a start (closing-child join, lease
reconcile, recovery probes, resolvers, history read, spawn, sink drain,
provider dispatch) held every later send.

A start now registers in one short step before its first wait: its abort
controller (which Stop, close and quit already reach outside the lock),
its startup clock (the 10-minute ceiling now counts from here) and quit's
drain. Every wait after runs outside the lock, raced with that signal, and
every write is a commit that first re-checks the start is still the
session's. A start that loses the check is late: its spawn is closed
through the post-acquisition failure path and nothing is handed to it.

- The delivery loop's prepare returns a start handle; the loop awaits the
  start outside, then re-prepares. Handover writes the pending row and
  begins the dispatch abort handle in one commit, calls the provider
  outside after re-checking the child, and resolves in a second commit.
- Ready operations claim a start instead of running one in their step.
- A client attach runs the same steps under one serialize, as before.
- An exit reported between spawn and publish is held for the publish
  commit, which ends the child it indexes.
- An unproven close still fails only what was queued when its start was
  asked for.

* Test that a send is accepted at every wait an agent start makes

One whole-flow test holds the start at each wait in turn (closing-child
join, reconcile probe, recovery re-probe, resolver, spawn, sink drain,
open, started recovery, pending row, provider call): a second send is
accepted at once, a Stop is recorded without waiting and nothing reaches
the provider, and what was sent goes to the published child once, in
order. Also quit before spawn, concurrent sends, a lease left recovering,
and an idle sweep racing a send.

* Bound each startup step on its own and drop the startup silence rule

A start no longer ends after 60 s without output: output is not protocol
progress. Each protocol step now has its own bound where its handshake
lives, and one 10-minute ceiling from registration stays as the outer bound.

- Codex: initialize 15 s and thread start/resume on the connection's 30 s
  default again, instead of the startup ceiling.
- ACP: initialize 60 s, authenticate 120 s, session new/load/resume/fork
  300 s; turns stay unbounded and option writes keep their caller's bound.
- Claude: the initialize answer has a 60 s bound; past it the open fails
  with the ordinary startup-failure words and the host ends the child.
- The onOutput plumbing that only fed the silence rule is removed.

* Count Pi ready once it reports its state

Pi's start no longer waits for its model listing and slash commands.
The open returns after get_state and the unknown-provider check; the
listing then arrives as an options report (with the catalog listing the
start resolved) and the commands as a later publish. An empty model list
no longer fails the start as signed out: a signed-out Pi runs the unknown
provider, which still fails it, and a prompt it cannot run is refused
with the sign-in words by the existing prompt path.

get_state keeps the connection's 30 s request bound instead of the
startup ceiling.

* Test that model and effort picked while a chat is created ride its first message

A slow host's create reply arrives after the user picked a model and effort. The picker's view
layers the launch's held picks over its seed and the host's saved selection, and every send
carries what the picker shows, so the first message carries the picks whether the host's status
names its selection or not, and while the last pick is still on its way to the host.

* Start a phone's new empty chat on its first message

The phone's new chat is an agent.launch with no prompt, which always created the chat with its
agent started, while desktop chats start on the first message. The phone now advertises the
create-without-start capability, and a launch with no prompt from a caller advertising it creates
the chat at rest, keyed in the ledger by its create intent as a client's create is. Launches with
a prompt, the CLI (which advertises nothing), orchestration workers and ensure still start at once;
an older host ignores the capability and stays eager.

* Take main's lockfile; the branch changes no dependencies

* Restore main's exact text in files the merge only reformatted

The pre-commit formatter rewrote 37 of main's files staged by the merge, though this branch never changed them, pushing two past the line limit. Committed without the hook because the hook would reformat main's text again; CI lints the result.

* Fit the startup tests to the owned-spawn, host-awaited-open contract

Test doubles gain the mandatory open and an acquisition generation, the
removed started event gives way to the rig's proveStart, and two literal
types are tightened for CI's typecheck. A Stop during a held start writes
no host note on this branch: the taken-back send draws its own
stopped-before-start row, which the startup-attempt test now asserts.

* Fit the remaining CI checks to the startup limits and current releases

- Run agent-launch-at-rest.test.ts in the SQLite Node runtime project.
- Pin the send-options cross-version case to v1.4.228, the first published release with agent-session.send-options.v1; v1.4.226 was never pushed.
- Load the baseline's isStructuredTab from src/shared (moved in v1.4.228), falling back to the renderer path for older baselines.
- End the never-proven start with a short startup ceiling: the silence limit it relied on is gone, and the ceiling counts from registration.

* Read the option stamp from the chat's one host-execution selection

* fix: preserve startup authority and accepted agent choices

* Retain dispatch authority and reconcile accepted options

* fix(native-chat): preserve accepted choices and cancellation authority

* Restore established client selection flow

* Preserve provider output observation and typed startup fixtures

* fix: preserve initial Claude launch choices and effort readback

* test: complete Claude catalog session fixture

* Preserve incoming main files after merge formatting

* Preserve compact handover through startup merge

* Type instant startup fixtures before their open is supplied

* Keep browser sign-in under its existing expiry owner

* Finish the main merge: retired-model display, start rewind cut, new tests

A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.

* Type the start's rewind record as the store returns it

* Type the Sleep test's held open as the provider open it replaces

* Sleep counts a chat stopped once the start it aborted leaves it at rest

A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.

* Keep Sleep's start-abort bookkeeping beside the rest of Sleep's stop
2026-10-19 10:53:46 -07:00
Neil d4813638d2 fix(orcad): orca open-url opens the page on the paired desktop (#30782)
* wip: orcad client-hosted browser pages

* wip: tests and renderer placement

* wip: engine-free client tab queries; keep paired server intent

* wip: one logical tab list for orcad engine tabs and desktop-hosted pages

* wip: one selection rule for engine tabs beside desktop-hosted pages

* refactor: one owner per mode for orcad tab selection; hide reopen-on-server without a server browser

* fix: an engine tab create or switch takes workspace selection from desktop-hosted pages

* refactor: one logical tab list and one selection rule for orcad engine tabs and desktop-hosted pages

* fix: keep the engine's per-workspace current tab beside desktop-hosted pages

* fix: an unscoped current tab matches unscoped automation routing

* fix: retained desktop-hosted pages stay listable and closable after their desktop leaves
2026-10-19 10:04:09 -07:00
Jinwoo HongandE2E Test 1f555c095a test(e2e): two chat specs follow the pan-and-zoom preview and the renamed New tab chat row (#30755)
Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 13:00:14 -04:00
Jinwoo Hong b358328b7f test(e2e): pane-close specs close panes the way the user does and check main's copy (#30745)
The pane-close consistency spec closed panes with a bare PaneManager.closePane, the
drawing-layer call that never asks main. In a layout-owner build that close reached main
only because killing the pane's terminal made main retire the pane; right after a reveal
the terminal has not attached yet, so main kept the pane. Every user close path already
sends the close to main first.

The helpers now close through the pane close request the menu and shortcut use, and every
scenario also checks that main's stored panes match the window's.
2026-10-19 10:32:01 -04:00
Neil 9a46695f9f test(e2e): mount-authority spec polls through the host's "can't answer yet" terminal list (#30743)
* test(e2e): mount-authority polls retry the host's "can't answer yet" terminal list

First-mount adoption briefly makes terminal.list refuse with
terminal_surface_ownership_unavailable. Thrown out of an expect.poll
callback, that ended the poll instead of retrying it, so the spec failed
whenever a slow runner's probe landed in that window.

* test(e2e): say the refused read returns undefined
2026-10-19 07:29:03 -07:00
Jinwoo HongandClaude 36913fd4f5 test(e2e): a phone lists the desktop's managed SSH workspace as the desktop sidebar does (switch on) (#30718)
* test(e2e): a phone lists the desktop's managed SSH workspace as the desktop sidebar does (switch on)

* fix(sidebar): key folder workspace rows by host in the shared builder so the phone matches the desktop

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

* test(sidebar): pin the SSH folder row key encoding

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

* test(e2e): route the SSH phone feed list spec from its sources

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 09:19:39 -04:00
Kelvin Amoaba 162e07cf97 fix(browser): refuse agent clicks while the user is mid-IME composition (#30358) 2026-10-19 11:45:29 +00:00
Neil 148944c020 refactor: de-barrel remote-runtime-client, runtime-terminal-stream and runtime-terminal-inspection (D2, D3) (#30684)
* refactor: de-barrel remote-runtime-client and runtime-terminal-stream (D2)

importers name the defining modules; the two vi.mock factories that named a re-export move to
the defining module (pty-id spy) or drop the dead key (RemoteRuntimeClientError)

* refactor(renderer): de-barrel runtime-terminal-inspection (D3)

importers name runtime-terminal-input-recording, runtime-terminal-verified-input and
shared/remote-runtime-pty-id; 22 vi.mock factories move the re-exported keys to those modules
(pty-id keeps its other exports via importOriginal)

* test: drop the duplicated verified-input mock (Opus P3)
2026-10-19 03:58:26 -07:00
Jinwoo Hong 9def13dde2 ci: run the layout-owner switch-on specs as an advisory PR check (dark, layout-owner build only) (#30582)
* ci: run the layout-owner switch-on specs as an advisory PR job

Build the e2e app with ORCA_LAYOUT_OWNER=main through the existing e2e.yml
(new layout_owner input, read only by the app and orcad builds), run the
layout oracle, its positive control and the layout-owner acceptance specs
with the save-divergence log on, and upload a summary by kind and spec.
Not in the PR aggregate's needs.

* ci: summarize divergence logs without a test report, and drop file-path ids from kinds

* ci: switch-on job adds the stream-loss spec and the managed-SSH Docker lane, drops diff-word-wrap (fails switch off), and names each looped test in the summary

* test(e2e): editor-model-host-isolation opens both files through the store's open action, so a layout-owner build's main holds the tabs

* ci: the divergence summary skips a cut-short test report instead of failing

* ci: re-run PR checks on a merge ref that includes the main typecheck repair (#30680)
2026-10-19 06:41:26 -04:00
Jinwoo HongandClaude 78bad24d33 test(e2e): nine sidebar specs create workspaces and links through main so they pass with the switch on (#30667)
* test(e2e): create reveal-spec workspaces through main so they pass with the sidebar switch on

The agent-send picker and host-section reveal specs injected fake repos and
workspaces into the window store. With ORCA_SIDEBAR_INPUTS=main the sidebar
lists main's rows, so those workspaces never appeared. The SSH host is now an
unreachable target seeded into the stopped profile, and the paired-server host
is a real headless orca serve host; pins, lineage and notes go through main.

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

* test(e2e): type the seeded hosts as execution host ids

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

* test(e2e): poll the reveal specs' page setup instead of awaiting it across CDP

An evaluate that awaits a preload promise sometimes fails with "Resulting
promise was garbage collected" in switch-on runs. evaluatePolled runs the work
in the page, polls its outcome, and still fails on a rejected or failed write.

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

* test(e2e): parent reorder spec creates its workspaces and links through main

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

* test(e2e): lineage specs create their workspaces and links through main

seedLineageScenario names and orders the two seeded workspaces and links them
with assignWorktreeParent. Extra families and surrounding rows come from a
fresh repo with real worktrees; pins and stale instances are main writes.

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

* test(e2e): default-branch and downward-drag specs create their workspaces through main

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

* test(e2e): reveal and active-delete specs create workspaces through main

The 60-row reveal, the folder-workspace reveal and both active-delete tests
now use real workspaces and folder workspaces added through main, ordered with
metadata writes. The deletes go through the real removal path, so the row
removal sampler waits by time rather than a frame count.

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

* test(e2e): address review on the switch-on spec follow-up

writeWorkspaceMeta now fails unless main lists the written values; the sleep
test's parent is a real worktree instead of a store-faked one; the legacy
inline-lineage case stays window-only and is skipped with the switch on;
the parent-reorder collapse key is built from the row's host.

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

* test(e2e): read-back reports an unlisted workspace and compares values structurally

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

* test(e2e): wait for the added repo before showing its worktrees

updateRepo returns false without writing when the store does not list the repo
yet. On CI the add's own refresh superseded the helper's fetch, so the visibility
write was skipped and every discovered worktree stayed hidden. The helper now
waits until the store lists the repo and fails if the write is refused.

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

* test(e2e): name the repo wait in its failure message

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

* test(e2e): size the setup poll budget to the number of main writes

evaluatePolled waited a fixed 10 s, sized for one write. Linking 53 children takes
about 8 s locally and 36 folder workspaces about 4.5 s, so CI ran past it ("page
work never settled"). Bulk setup now passes a budget of 10 s plus 500 ms per write.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 06:29:17 -04:00
Jinwoo Hong 0e1d0ad4a8 A CLI split that times out leaves no pane (dark, layout-owner build only) (#30563)
* fix(layout): a pane main closed stays closed when its terminal starts late (dark, layout-owner build only)

* fix(layout): main takes back a CLI split pane whose terminal does not start within 10 s (dark, layout-owner build only)

* fix(layout): the timeout take-back keeps a pane whose terminal started or is starting (dark, layout-owner build only)

* fix(layout): one take-back call keeps the layout-writer count

* fix(layout): a split take-back is decided in the write queue, and a spawn for a taken-back pane starts nothing (dark, layout-owner build only)

* test(layout): a spawn for a taken-back pane is refused before the provider spawns

* test: typed store fake in the closed-pane spawn test, no casts
2026-10-19 05:21:06 -04:00
Jinwoo Hong 9294b0ae9c fix(sidebar): the window raises the unread flag exactly when its host does not (dark switch) (#30638)
* fix(sidebar): the window raises the unread flag exactly when its host does not

With ORCA_SIDEBAR_INPUTS=main the host raises the workspace unread flag. A host
now advertises workspace-unread.host-raises.v1 in its status; a switch-on window
raises the flag itself only for workspaces whose paired server lacks it, waiting
for that server's status first. The desktop advertises the same capability to the
hosts it pairs with, and a host leaves raising to an older desktop (one that hosts
client pages without the capability) while it is connected.

* fix(sidebar): every desktop socket advertises the unread capability; window answers from cached status

The browser host lease and network tunnel build their capabilities on the shared
remote base, not the Electron list, so a current desktop's lease looked like an
older desktop and silenced the host. The capability now lives in the shared base.
The window reads the cached status first and probes only before the first one.

* test(sidebar): pin the cached-status path and both older desktop socket shapes

* test(e2e): narrow the server row's host before Mark as read
2026-10-19 05:14:00 -04:00
Jinwoo HongandClaude 0c3807e73b test(e2e): reveal specs create workspaces through main so they pass with the sidebar switch on (#30642)
* test(e2e): create reveal-spec workspaces through main so they pass with the sidebar switch on

The agent-send picker and host-section reveal specs injected fake repos and
workspaces into the window store. With ORCA_SIDEBAR_INPUTS=main the sidebar
lists main's rows, so those workspaces never appeared. The SSH host is now an
unreachable target seeded into the stopped profile, and the paired-server host
is a real headless orca serve host; pins, lineage and notes go through main.

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

* test(e2e): type the seeded hosts as execution host ids

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

* test(e2e): poll the reveal specs' page setup instead of awaiting it across CDP

An evaluate that awaits a preload promise sometimes fails with "Resulting
promise was garbage collected" in switch-on runs. evaluatePolled runs the work
in the page, polls its outcome, and still fails on a rejected or failed write.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 05:10:40 -04:00
NeilandClaude d600658b84 fix(ssh): closing a bridged relay tab stops its shell, even across a restart (#30587)
* fix(ssh): stop a closed bridged relay tab's shell from every close route, across restarts

The renderer's tab-close kill tombstoned a bridged lease before the bridge's
own close ran, so the old relay's shell kept running. Route it through the
bridge like the runtime kill. Persist a pending bridged stop on the lease's
pendingKill so a restart before reconnecting still sends it, and list such
leases for the stop without serving them as panes.

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

* fix(ssh): end a closed bridged tab's lease when its stop can't be proven

Clearing the stop left the lease bridgeable, so a process that took the
recycled relay id was served under the closed tab. Retire it as any closed
tab's lease is.

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

* fix(ssh): keep a closed bridged tab closed across restarts even when its stop can't be fenced

An unfenced legacy stop now gets one try and ends the lease, a repeated close
keeps the first record's age, and the Docker bridge spec covers a close while
disconnected followed by a restart.

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

* fix(ssh): re-list after abandoning a bridged stop so the closed pane drops

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

* fix(ssh): route CLI and worktree stops of a bridged tab through the bridge, and keep a repeated close fenced to the first identity

* fix(ssh): refuse sleeping a bridged pane, settle bridged stop-and-wait on the relay's exit proof, and never attach a pane closed mid-attach

* fix(ssh): settle a bridged stop-and-wait on the exit proof alone, for every overlapping waiter

* fix(ssh): keep an unfenced bridged stop's pane tracked for its trailing exit report, never serving it

* fix(ssh): keep awaiting a bridged stop's exit after a failed reply, never name a retired pane to the relay CLI, and require a real empty listing in e2e

* fix(ssh): tell the runtime a bridged terminal exited once its stop-and-wait is proven

* fix(ssh): keep an unfenced bridged stop's exit observable after its reply fails

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 01:59:23 -07:00
Neil 7ce58ec47e refactor(terminal): one PTY attribution for teardown, close and identity; delete the deprecated owner resolver (#30626)
* refactor(terminal): one PTY attribution for teardown, close and identity; delete the deprecated resolver pair

- strict ssh: route: a proven direct listing beats a same-id server proxy; other contradictions refuse
- terminal spawn's git branch takes the strict route when a host is named (rivals refuse)
- resolveTerminalPtyOwner + isUnattributedTwinWorktree replace resolveRuntimeOwnerForWorktree in
  shutdown, pane and agent hibernation, and PTY identity; close logs unroutable runtime ids
- delete getRuntimeEnvironmentIdForWorktree/resolveRuntimeOwnerForWorktree (legacy branch moved
  into the adapter), the alias golden test, the importer ratchet, its baseline and pr.yml step
- hasHostEvidence reads the owner indexes instead of scanning repos and groups

* fix(terminal): close and identity read spawn's attribution for unscoped remote ids (Opus P2)

- an unattributed twin's unscoped remote: id is skipped and logged on close, never closed locally
- setup terminals: identity and close follow spawn's runtime; raw-id teardown stays local
- drop the last stale getRuntimeEnvironmentIdForWorktree mock (tests/e2e unit suite)

* refactor(terminal): one ownership read in resolveTerminalPtyOwner

* test(terminal): pin the close-path twin skip and the two-proxy direct listing (Opus P3)

* test(terminal): keep the close-provider owner fixture's full union type (Codex P1)
2026-10-19 01:50:57 -07:00
NeilandClaude 36ed236394 fix(persistence): repo writes take the row's identity, never a bare id (#30616)
* fix(persistence): repo writes take the row's identity, never a bare id

updateRepo(target, updates) now takes Pick<Repo,'id'|'connectionId'|'executionHostId'>
(RepoHostTarget) and resolves the host from it, so a caller that holds the row can no
longer drop its host and write a same-id sibling on another host. The id-only callers
(project host setup controller and IPC handler, folder-to-Git upgrade watch, both
clone paths, registration adoption, fork backfill) pass the row they resolved.

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

* test: migrate the remaining Store.updateRepo callers to row targets

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 00:33:50 -07:00
Brennan Benson 7dcb7d4439 Sleep stops a workspace's native chat agents (#28445)
* fix(sidebar): Sleep stops a workspace's native chat agents

Sleeping a workspace stopped terminals and browsers but never its structured (native)
chats, so their claude/codex process kept running, and Sleep was disabled for a
workspace whose only content was a chat.

Sleep now stops each chat's agent through a new agentSession.sleep (the host's close,
keeping the tab), routed to the host recorded on the chat's tab; an older host without
the capability fails Sleep with a clear message. The Sleep menu counts a chat.

* Fix native chat Sleep ownership and delayed teardown

* Stop native chat sessions when sleeping their workspace

* Fix sleep teardown wake and host-qualified receipt races

* Verify mobile Sleep sends the selected desktop host

* Keep worktree Sleep RPC within method file limit

* Use sleep intent as attempt identity

* Gate workspace sleep on structured client support

* fix(native-chat): preserve sleep stop receipts and send timing

* fix(native-chat): keep superseded workspace sleeps quiet

* fix(native-chat): ignore superseded failed sleep stops

* Fix sleep lifetime test resolver types

* Accept workspace Sleep for already ended chats

* Sleep native chats through the shared no-toast sleep core

Main split runSleepWorktrees into sleepWorktreesReportingFailures (no toast) and
a toasting wrapper. Move the native chat stop, host routing and typed failures
into the shared core so the WSL move's Sleep request also stops chats, and
report every workspace not verifiably slept (an unconfirmed chat stop included)
in failedWorktreeIds. Fold one ACP adapter field onto a line so main's file plus
this PR's ownership check stays within the file length limit.

* Import isStructuredTab from its shared module

* fix(native-chat): leave chat rows to their host through Sleep

Sleep's worktree status drop deleted the chat's row, which the host feed then
rewrote, so the outcome depended on which write landed last. Open chat tabs
own their rows (same rule as terminal-tab close), and a chat only makes a
workspace sleepable while its host holds the agent's process.

* fix(native-chat): judge the Sleep receipt by facts, not lane occupancy

A running chat's stop queues the provider's own requested-close end report
onto the chat's lane, so the lane-occupancy check read every proven stop as
superseded and dropped its receipt. Later work now counts only once it
exists: a moved fence, a child, delivery, child work, a pending submission,
a pending create or attach, or an open dispatch.

* test(native-chat): report a fake close's end only for an acquired child

* Preserve canonical workspace identity when sleeping

* Propagate optional sleep host through file command contract

* Move the deliberate-sleep PTY tests to main's spec file

Main grouped the PTY connection suites (#29193) and moved this file to a .spec.ts registered from pty-connection-spawn-and-startup.test.ts. Taking main's copy here keeps the following main merge free of a modify/delete conflict; this PR's test changes are reapplied after the merge.

* Reapply Sleep's teardown waits to the moved PTY spec; keep acquisition under its line limit

The deliberate-sleep spec moved in #29193; this carries the PR's changes into it: a pane mounted after wake waits for issued teardown, an unqualified pane waits for every same-id host's teardown, and a queued startup waits too.

The merge left claude-structured-session-acquisition.ts over 300 lines. resolveClaudeAcquisitionError, which already builds the exit-unproven error, now decides whether to release the acquisition.

* test: keep the Sleep receipt checks on the closed handle and kept record

* fix(sidebar): a single-workspace Sleep failure says workspace, not workspaces

* fix(sidebar): Sleep stops a workspace's chats on every host that has a copy

* test: drop redundant casts from the Sleep flow test state

* fix(sidebar): stop a workspace's chats after its terminals, leaving main's teardown owner check as is

* fix: admit workspace chat stops before teardown waits

* fix: retain browser teardown caller sequencing

* fix: retire failed acquisitions after observed root exit
2026-10-19 00:33:01 -07:00
Brennan Benson 503d919e64 feat(native-chat): + menu shows chat, disabled chat and CLI rows (#29074)
* feat(native-chat): choose which agents open as a chat

Settings → Chat gets a Providers list. Each agent the host can open as a chat
has a switch; off opens that agent in a terminal instead. Only choices the user
makes are saved. Unchosen: Claude and Codex on, others on when their CLI is
installed on this computer.

The renderer's route and the host's launch-mode decision both read one rule, so
the tab menu, shortcuts, CLI, orchestration and relayed phone launches agree.

* fix: keep chat provider settings with the other chat settings types

* test: route input carries this computer's detected agents

* feat(native-chat): the + menu shows chat, disabled chat and CLI rows

With Chat UI on, each agent whose chat provider is on gets a chat row, disabled
with the reason where the workspace's machine can't run that chat (not
installed, SSH, WSL, not on this host). Every other installed agent gets a row
marked (CLI) that opens its terminal. With Chat UI off, or on an older server
that opens no chats, the menu is unchanged.

* fix: new tab menu rows use a launch variant instead of restyling the menu item

* fix: name every route blocker the menu rows map

* fix: menu row subscriptions render on the server too

* feat(native-chat): open a chat agent's CLI from beside its + menu row

A chat row whose CLI is installed on the workspace's machine gets a › beside
it. Its side menu offers '<Agent> (CLI)', which opens that agent's terminal for
this launch only; nothing is saved and the row itself still opens a chat.

* fix: keep new tab agent surfaces authoritative

* fix: handle unknown host support reasons

* fix(chat): resolve provider choices from their owning computer

* fix: include chat launch feedback translations

* test(chat): use checked native inventory fixtures

* chore: retrigger PR checks

* test: pin pending chat row behavior

* chore: retrigger PR checks after cancellation

* Fix launch preparation lifetime and execution host startup

* fix: keep agent menu choices bound to their launch

* Simplify agent menu support reads and preserve explicit surfaces

* Keep focused menu behavior coverage within its budget

* Keep menu host fixtures typed as execution hosts

* Assert notes payload on the current launch action

* Combine agent session launch type imports

* fix(chat): keep unchosen providers on without discovery

* fix(chat): use workspace installation for menu defaults

* test(chat): type provider routing fixtures against store contracts

* fix(tab-bar): keep + menu chat rows clickable while the host answers

Each menu open discarded the host's answers, so every open showed the chat
rows disabled with 'Checking chat support' until the host replied. Answers
now outlive the menu and refresh in the background, rows use this client's
facts until the host answers, and a slow host no longer reads as a refusal.

* test(chat): align shared launch fixture with private settings reader

* fix(tab-bar): keep known + menu refusals and never treat a silent host as one

Closing the menu dropped the workspace's agent list, which changed which agents
the menu asks about and threw away the host's earlier answers, so a reopen showed
a refused chat as clickable. A support read that could not answer was stored as
"No chat on this host", disabling chats the client knows are supported.

The menu now keeps the stored agent list while closed (only fetching waits for
it to open) and only a real host answer relabels a row. The workspace's
detection target is resolved where main already resolves it (QuickLaunchButton
and the tab-bar runtime model) and passed in, so no new focus read is added.
Tab-bar test mocks now stub the QuickLaunchButton exports the menu imports.

* fix: sync runtime English catalog for agent menu

* test(chat): give the structured-session RPC fixture the private settings reader

The host now reads saved chat choices through requireStore().getSettings(); the
shared RPC fixture only had getClientSettings, so the launch-mode reader saw no
settings and create support refused Claude. The fixture now follows each test's
client settings.

* test(chat): pin create support's options argument and kept decline reason

* fix(native-chat): + menu chat rows wait disabled for the host and share its answer across tab groups

A chat the host refuses showed clickable for up to the 10s host read, then flipped to disabled,
once per tab group. Rows now stay quietly disabled until their host answers (or the read times
out), and every tab group on the same workspace host reads the same answers.

* fix(native-chat): + menu rows also wait before the host's chat list loads, and on every reopen of a host that could not answer

Asking the host no longer waits for its chat list, so Claude and Codex no longer show clickable,
then disabled, while it loads. A read that times out releases rows for that open only, so the
next open waits again instead of flipping on a late refusal. Agents not installed skip the read.

* fix(native-chat): typed + menu search puts a launchable row ahead of a disabled chat

In a WSL workspace, typing an agent's name and pressing Enter highlighted its disabled chat row,
so nothing launched where main opened the CLI. Search now ranks rows that can launch first.

* fix: preserve provider choice and cached native inventory

* feat(native-chat): hosts publish each workspace's chat availability as one stream

A new negotiated stream, agentSession.subscribeAvailability, advertised as
agent-session.structured.availability.v1. The host keeps one in-memory resource per
workspace, shared by every reader, that asks the existing createSupport check for every
registered agent in that workspace and publishes only complete generations. It answers
again when a launch setting changes, after an explicit agent refresh, and (for answers
that could not be checked) when a workspace appears; the last reader leaving retires it.
A check that could not answer is unknown, never a refusal. agentSession.agents stays
static and unchanged.

* feat(native-chat): + menus read a publishing host's workspace availability

The renderer holds the workspace's availability stream while its tab bar is mounted
(not while a menu is open), shared by every tab group, with runtime and contact fences.
It exposes a readiness model, { ready: false } | { ready: true; byAgent }, from
useHostStructuredAgentAvailability and the menu hook. On a host advertising the
capability the menu reads its answers from that model and makes no per-agent
createSupport reads; an older host keeps the per-agent reads, answer cache and deadline
unchanged. Row building and presentation are unchanged.

* fix(native-chat): workspace availability checks only what the shown workspace needs

Only the shown worktree (or an open menu) holds its host's availability stream, and only with Chat
UI on. Display checks no longer force a fresh login shell for an agent that isn't installed, so
Claude and Codex answer without waiting on it. A change to the Windows runtime default or a project's
WSL choice re-checks, and a workspace still on its first check answers again when one appears.

* fix: keep main's bytes for the create-support file the merge reformatted

The pre-commit formatter rewrapped main's unformatted lines during the merge and pushed the file past its line limit.

* test: type the + menu rows hook mocks without assertions

* test: match the availability stream's types in three tests

* test(cross-version): declare the workspace availability stream in the agent-session surface

* feat(native-chat): + menu holds a placeholder until its agents are known, and learns them while the workspace is shown

* feat(native-chat): + menu reads one agent health snapshot per machine

The + menu no longer checks chat availability per workspace. Each Orca
runtime keeps one agent health snapshot: every agent's location rule on
this machine plus a typed observation of its installed release, probed
in the server's own home. It is taken on first use and replaced only on
a launch setting change, an explicit agent Refresh (after a fresh shell
capture), or a restart, and kept without readers.

- The version probe reports supported, unsupported or inconclusive; a
  launch still refuses an inconclusive run, while the snapshot reads it
  as unknown rather than a refusal.
- agentSession.subscribeAvailability takes no params and publishes that
  snapshot; a replay that closes the transport releases its reader.
- The renderer keeps the snapshot per host, runtime and pairing, through
  lost contact and across workspace switches on the same host.
- The menu hook drops the per-open createSupport display reads, their
  deadline and answer cache; older hosts show client facts and the
  click's admission, unchanged, states any refusal.

* fix: a failed shell recapture still publishes the agent health snapshot

* fix(native-chat): capture fresh shell facts for every health snapshot

* fix(tab-menu): share settled agent actions across menu surfaces

* fix(tab-menu): preserve loading row line height

* feat(native-chat): the whole chat row opens its CLI submenu

Hovering a chat row in the + menu, or pressing ArrowRight on it, now opens
the "<Agent> (CLI)" submenu to the right instead of needing a separate
chevron button. Clicking the row, or Enter/Space, still starts the chat
and closes the menu; hosts pass closeMenu since a submenu trigger never
closes the menu on its own.

* fix(tab-menu): line the CLI submenu up with its chat row

* feat(native-chat): a chat row's terminal button opens its CLI

Clicking a chat row in the + menu starts a chat. Hovering the row (or
pressing ArrowRight on it) reveals a terminal button at its right edge that
opens the agent in a terminal instead. This replaces the submenu, whose
chevron made the row look like it only opened a submenu. The button's click
bubbles into the row, so the menu closes as for any item and the hosts no
longer need a closeMenu callback.

* fix(native-chat): the chat row's terminal button handles drags, arrows and screen readers

A press on + dragged onto a chat row's terminal button and released there
now opens the CLI: the row reads whether the pointer or focus is on its
button instead of the click target, which a drag-release never sets.
Arrow, Home and End keys pressed on the focused button move between rows
as they do on the row. The row is announced as "Start a <Agent> chat"
instead of including the button's label.

* fix(native-chat): chat rows read "Start a chat with <Agent>"

"Start a OpenCode chat" was ungrammatical for agents whose name starts with a vowel.
2026-10-19 00:27:17 -07:00
Neil 767b55b1b6 fix(terminal): client pane names reach a desktop host's window, and open panes follow the host's (#30496)
* fix(terminal): client pane names reach a desktop host's window, and open panes follow the host's

- A host with its own desktop window ignored client pane-layout writes, so a
  client's pane rename never reached the window. updateMobileSessionPaneLayout now
  passes the client's pane names to the window one pane at a time through the
  set-pane-title path, and still leaves the window's geometry and the tab's name
  alone. A single pane named after its own tab is dropped, not forwarded.
- Open panes on a client only read host pane names when they mounted. They now
  follow the host's published titlesByLeafId live. A local rename is held until
  the host echoes it, so a snapshot taken before the host saw it cannot undo it.

No wire change: hosts already publish titlesByLeafId, and old peers keep their
current behaviour.

* fix(terminal): hold a client's pane rename over host snapshots until echoed, and fail a dropped window delivery

* test(e2e): read the created host tab id without a type assertion

* fix(terminal): only an explicit client rename reaches a desktop host's window, and local rename holds are bounded

- A client push now names the panes it renamed (optional `renamedLeafIds`), and
  the host forwards only those. A resent title map, which older clients and every
  divider drag send, can no longer revert or clear a name set on the host.
  Older hosts strip the field. Older clients send none, so their renames stay
  local as they do today.
- A held local rename now gives way to a newer host name, a failed push, or a
  15s expiry, and expired holds are pruned. A host that never echoes (an older
  desktop host, or a single-pane name dropped as the tab's own) can no longer
  hide host titles forever.

* fix(terminal): a failed rename push retries with its rename, and an earlier echo cannot undo a newer rename

* fix(terminal): a pane rename bypasses the unchanged-layout push dedupe
2026-10-18 23:03:00 -07:00
Jinwoo Hong 7395d30dc7 The host raises the shared unread dot whatever each client's Filters say (dark) (#30581)
* fix(sidebar): the host raises the shared unread dot regardless of Filters (dark)

* fix(sidebar): tighten the shared-dot comment and tests after review
2026-10-19 01:58:52 -04:00
Jinwoo Hong e8d7da029c An e2e test cuts the window's layout feed mid-session and checks it recovers and survives a restart (dark, layout-owner build only) (#30574)
* test(layout): an e2e ends the window's layout stream mid-session and checks the reopen and a relaunch (dark, layout-owner build only)

* test(layout): the stream-loss e2e covers main's change during the gap deterministically and reuses the shared pane-tree and frame readers (dark, layout-owner build only)

* test(layout): the stream-loss e2e names how long main's create took when it misses the reopen window (dark, layout-owner build only)
2026-10-19 01:45:27 -04:00
Jinwoo Hong 3051b7b671 One background-process read of "Notify About Hidden Workspaces" for phone pushes and the host unread flag (#30571)
* refactor(notifications): one host read of Notify About Hidden Workspaces, used by phone pushes and the host unread flag

* fix(notifications): folder chat phone pushes read the folder's name; skip the hidden check with no Filters switch on
2026-10-19 01:30:52 -04:00