* refactor(renderer): read workspace records through typed selectors
* refactor(renderer): keep only the selectors with readers; type the host owner lookup
* feat(accounts): add Claude and Codex accounts to a remote server from a paired client
The server runs the login (Claude's pasted-code sign-in, Codex's device
code) and keeps the credentials; the client only opens the page and
relays the code. New capability-negotiated accounts.hostSignIn* methods;
older servers degrade to an update message.
Fixes#20009
* fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin
Review fixes: a cancel the login refused still reports the added account;
the client pins each flow to its pairing revision; Claude's stdin EPIPE no
longer crashes the host; both sides accept only the provider's sign-in
host; pasted codes reject whitespace; a host-side supersede says so; Codex
cancels its own login handle; opt-in real-CLI test.
* fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only
A Cancel clicked before the host answers the start is applied once it does;
the client keeps polling through a short outage until the host would have
ended the flow; Claude's link is read from stdout only; a timeout after the
code was sent keeps its own message.
* test(accounts): name the stdin sender and allow its stream write in the persistence ratchet
* fix(accounts): register each Add Account attempt before its first await
One attempt per click owns the pairing pin, the cancel flag and the host's
flow, so a Cancel during the capability check or the start is never lost;
polls retry only lost answers; the stdout forwarder is removed on cleanup.
* refactor(accounts): drive the client sign-in as one explicit state machine per server and provider
checking -> starting -> following -> settled, with Cancel as a flag the run
loop delivers once the host names its flow and re-asks after any answer
until the host takes it. A second Add joins the running attempt; a Cancel
the server could not be told about is surfaced; retry policy tested.
* fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends
* fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow
The host picker can move while a sign-in runs: Cancel now targets the
running server sign-in (or the desktop's own login when none runs), and
the pasted-code form is keyed by server and flow.
* fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning
* fix(accounts): the server sign-in prompt owns its Cancel
Cancel for a server sign-in now lives in the prompt bound to that server,
which also reappears with its Cancel when Settings reopens mid-sign-in; the
header Cancel is only for this desktop's own login.
* fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status
* fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs
* fix(accounts): the header Cancel follows the running addition, wherever Settings now points
Each Add Account records where it is signing in. Cancel stops that
sign-in, on a server or this desktop, and is hidden only where that
server's own prompt already offers Cancel.
* fix(remote-terminal): repaint an idle program when a reconnect can't image its pane
After an SSH reconnect the pane is repainted from the host's screen
snapshot. When the host cannot give one (too large for the replay limit,
no buffer, unanswered, overflowed), the pane only printed a loss banner and
an idle full-screen program that writes diffs kept its pre-disconnect frame
until the user resized.
The two places a remote pane declares it has no host image now ask the
program to repaint through one choke point: a one-row viewport claim and
back, which every TUI honours (Node's tty ignores a same-size SIGWINCH).
It respects a phone or another desktop holding the size, skips hidden
panes, and puts the row back even if the pane unmounts mid-nudge.
Fixes#26887
* test(e2e): idle full-screen program frame after a managed SSH outage
* test(e2e): wait for the pane before typing in the idle-frame spec
* fix(remote-terminal): pick the redraw height on the host's clamped grid
* fix(remote-terminal): wait for the host's grid before putting the redraw row back
* test: give transport mocks a connection status for the redraw gate
* test(e2e): keep the orcad idle-frame proxy typed for the e2e ratchet
* fix(remote-terminal): let a new PTY's redraw supersede a swapped-out nudge
* test: keep new transport-mock lines free of type assertions
* ci(e2e): route the managed-SSH idle-frame spec to its sources
* fix(layout): a running agent's resume record is not a sleep in switch-on builds
The shared layout load put every pane resume record into leaf.sleeping, so main's
terminal index read a live or quit capture as a slept pane and terminal.list
reported running agents as disconnected and unverifiable. Records of slept panes
(isPaneSleepRecord) now load to leaf.sleeping and running agents' records to
leaf.resume; both save to the same sleepingAgentSessionsByPaneKey entry, so the
disk format is unchanged. The disk-field census names both holders, and a model
rule refuses a pane holding both.
* fix(layout): a waking start wakes only slept panes; a recordOnly wake drops either record
* fix(layout): main's shutdown reconnect facts read a running agent's resume record as awake
Adopt the nine Japanese wording corrections from community PR #29248. Clarify smartphone control, manual restoration, and mobile data connections.
Closes#29247.
Co-authored-by: sojiro-o <53679703+sojiro-o@users.noreply.github.com>
On a headless host the mobile status builder treated any title without an
agent marker as the shell reclaiming the pane. Codex's idle title is its
project name, and since OSC 7501 program status gives the pane a PTY state,
a Codex turn with no tool call ended with the hook's transcript address
withheld, so the phone showed "Can't find this conversation yet".
Only a shell or management title now retires the hook identity, matching the
renderer projection. Refs #31136.
* fix(native-chat): typing fast after focus moves keeps keys in order
Typing a burst right after focus left the composer (Move Tab to Split, a
paired chat, a click on the transcript) could scramble the first letters:
"Reply exactly" came out as "Rply exactly...e".
The first key was routed into the composer programmatically: focus moved
at once, but the caret was set a frame later from a position computed
before the key. The next keys landed natively, then that late caret write
pulled the caret back. Quote "Add to chat" and picker completion had the
same deferred caret write.
Each programmatic write now puts the text and caret into the editor in one
synchronous step, read from the editor's live value, then focuses it. No
write happens while an IME composition owns the field.
* fix(native-chat): retain saved chips after synchronous composer edits
* fix(ssh): the host's 'no desktop is sharing its browser' error names the browser routing setting too
The desktop withholds the browser bridge from a managed SSH host whose pages browse from the
desktop's own network, but the host's CLI was told only to turn on the per-host CLI control
switch, even when it already was on (#31421).
* fix(ssh): name the per-host routing opt-out too, and guard the labels against UI copy drift
The runtime WebSocket listener widens to 0.0.0.0, which never accepts
IPv6, yet the Connection address picker and the default pairing address
offered global IPv6 interfaces, minting links nothing answers. Filter the
interface list by the family of the host the listener binds when serving
the network.
Reported-by: @fongyuan-2011
* fix(plugins): a development plugin reloads an edit saved while its watcher was starting
Orca scans a development plugin folder, then starts watching it. The watcher goes live
about a second later and reports only changes made after that, so a save in between was
never picked up and the plugin kept running the old files. Once a development root's
subscription goes live, schedule one catch-up refresh through the existing debounce.
* test(plugins): count a replaced dev root's catch-up refresh regardless of timing
* fix(native-chat): a Claude chat worker keeps running after Claude acknowledges its task
Claude's adapter dropped an echoed send from the sends it holds before
reporting the send's answer to the host, and emitting the echo's turn
can publish at once. That publish read the still-pending send as held
by nobody, so it read as "couldn't confirm": orchestration worker-start
then closed the new Claude worker, and every other answer wait and
client saw a one-frame doubt.
A frame's settled sends now stay held until their answers are reported,
when the host's handover window takes them over; a Stop's rejected
sends are released and reported one at a time, as Pi and ACP already do.
Tracked in STA-11160.
* test(native-chat): assert the settled send's dispatch state through the send result
* Reapply "fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send (#30134)" (#30572)
This reverts commit 3ef5a46061.
* fix(native-chat): a Claude send stays held until its settlement reaches the host, on the reapplied held-sends read
* fix(native-chat): a Claude send stays Working, not in doubt, while a Stop is still ending its CLI
* test(pi): drop the removed idle-release dependency from the adapter fixture
* feat(layout): main places a swept workspace's live terminals; the window sweep only reads (switch-on builds only)
The window's activation sweep (first visit, an empty or sleeping activation, a phone wake) now
asks main, through its existing terminal.list call, to place that workspace's unshown live
terminals before listing them. With the switch on the sweep writes nothing to the window store and
counts a pane main names as shown, so it no longer seeds a fresh shell beside a terminal main just
placed. Main's "still binding" check also covers a registration in flight.
* fix(layout): a sweep's placement pass holds back only its own workspace's first surface (switch-on builds only)
* test(layout): switch-on activation-gate and census-call tests read main's listing, not window binds
* fix(ssh): draw every host list through one bounded, searchable host picker
A generated ~/.ssh/config can import 13k+ SSH hosts, and several surfaces drew a
row per host: Settings > General workspace directory 'Apply to', Notifications >
Machines, the sidebar and Agents Hosts submenu, the composer run-target picker,
the Add Project host selector and remote step, the skill-install machine pickers,
Settings > Servers SSH host picker, repository 'Add to another host' and the
automation host field and filter.
They now share components/host-picker: boundHostList (pinned hosts plus a ranked,
capped match list), HostCombobox for single choice (a Command in a Popover that
mounts nothing until opened, replacing Selects that mount every item), and
useBoundedHostRows with HostListSearchField and HostListOverflowNote for
checklists. A host-list-render ratchet stops new JSX from mapping straight over a
host collection.
* fix(ssh): host lists keep room for search results, menu search is keyboard-reachable
- boundHostList gives pinned hosts and other matches separate limits, and narrows
pinned hosts by the query, so a large selection never hides what the user types.
- In a dropdown, the host search is a menu item: arrow keys reach it, focus moves
into the input, and arrows leave it again.
- The Add Project host picker hides its setup actions while searching, so Enter
picks the matching host.
* fix(ssh): re-picking the current host in a host picker is not a change
The Selects these pickers replaced suppressed same-value changes; the skill
install form clears its workspace and target on a machine change.
* chore(ci): let the shared ratchet runner pick up the host-list render ratchet
main now discovers every check-*-ratchet.mjs through run-ratchets.mjs, so the
separate workflow step would run it twice.
* fix(ssh): host lists rank pinned matches and keep input order; label and scroll fixes
- boundHostList ranks pinned and other hosts by the same match order before
cutting each to its limit, then keeps input order among equal ranks, so a
muted or checked exact match is always reachable and clicking a row never
moves it.
- The Servers SSH host picker is named by its form label again.
- The sidebar Hosts submenu scrolls its host rows instead of running off screen.
- The composer's overflow note sits outside the listbox.
* fix(worktree): refuse a parent loop even when the worktree cache is cold
The parent cycle walk read each workspace's current instance from the
resolved-worktree cache, which the reparent itself invalidates, so it only
knew the two endpoints and accepted A->B->C->A. Read saved worktree
metadata instead.
* fix(worktree): fall back to locator metadata and tolerate loops saved earlier
* fix(worktree): skip another host's locator metadata in the parent loop walk
* fix(worktree): read loop-check identities from a read-only, ownership-scoped view
The per-host metadata accessor migrates a hostless legacy row into the
queried host, so the loop check could adopt another host's worktree and
refuse a valid parent. Read one read-only host snapshot instead, counting
a row only when its project is registered on that host (the resolved
list's ownership rule).
* fix: import host-qualified worktree meta helpers once
* test: match main's host-targeted lineage write contract
* fix(shortcuts): app shortcuts run before the terminal, whatever order they mounted in
App shortcut owners and terminal panes both listened on window capture,
so React's effect order decided who saw a chord first. After a worktree
switch the new pane registered before the tab shortcut listener and
turned a user-bound Cmd+Right into Ctrl+E; clicking a tab flipped the
order back. A keyboard router installed before React renders now runs
every app owner, in registration order, ahead of any pane listener; a
key no owner claims still reaches the terminal's own handling. Held tab
chords are claimed on every repeat but switch once.
Tracked in STA-11205 (same bug as STA-4602).
* test(shortcuts): type the router mock with key event names
* fix(shortcuts): install the keyboard router in the web client before it renders
* test(shortcuts): floating panel specs read the routed key listeners from the window stub
* fix(notifications): tie a completion banner to the hook turn it announces, not a 10 s window
A title-only working->idle edge (Claude's /btw side question fires no hook) raised a
generic "Task complete" banner because the pane's hook row was older than 10 s. On a
hook-reporting pane the hook row now decides: a row not yet announced supplies the
banner content however old it is; an already-announced row means no turn ended, so
no banner. Hookless agents keep the generic title fallback.
* fix(notifications): a pane with hooks gets its completion banner from its hook turn only
The title lanes (mounted pane and parked watcher) announced a finished task on every
working->idle title edge, even on panes whose agent reports turns through hooks. A title
spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra
banner that could only be filled with the workspace name and tab title.
On a pane the hook lane is tracking, with a settled Done row from the same agent, the title
lane now defers before it claims the turn, so the hook lane's banner (with the last message)
is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless
agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner.
This replaces the previous commit's announcement memory in the dispatcher.
* fix(notifications): hook-owned turn ends settle pane bookkeeping and announce re-entered Stops
- A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner.
- A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start.
- A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop.
- Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off.
- The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age.
- The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher.
* fix(notifications): the host marks each real turn end, so a repeated status can't raise a second banner
The renderer told a Stop re-entered after a blocking Stop hook from a
restatement by its final message. The message also changes when the
listener's cache resets (a session reset, then an idle reminder), so a
restated finished turn raised a second, empty "finished" banner.
The execution host now classifies each provider's own turn-end event by
name and stamps an optional `turnEndedAt` on the status row. Idle
reminders, session events, replays, child re-emits and result retries
restate it; a turn-end event on an already finished row is a new end only
when the agent says it re-entered (`stop_hook_active`). Local hosts and
SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it
at admission and sends it on its envelope. The field is persisted,
forwarded over IPC and the paired-server session projection, and the
renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the
existing `turnCompletedAt` precedence). `stateStartedAt` and every other
clock are unchanged.
Paired servers that predate the stamp advertise no
`agent-status.turn-ended-at.v1` capability; their panes keep the
terminal-title completion fallback exactly as before.
* test(notifications): type the old-server pane setup in the completion flow test
* fix(notifications): a blocking Stop's final banner reaches the user; /compact and quitting an idle agent stay quiet
Real-agent QA on the hook-only completion design found three gaps:
- A Stop re-entered after a blocking Stop hook could be dropped twice
over: inside 1 s of the first banner by the replay guard (a new host
turn-end stamp is now a real end), and by main's 5 s per-workspace
desktop cooldown. A banner with the same notification id and changed
text now replaces the one already shown instead of being dropped;
every other cooldown rule is unchanged.
- On a mounted pane, a hook-owned idle edge (Codex /compact) no longer
claimed a completion, so the burst's BEL went out as a Bell banner. The
deferred idle edge now absorbs that BEL like a claimed completion did.
- Quitting an idle agent after a title spin raised a generic process-exit
banner. On a pane whose hook lane owns turn ends and whose last turn is
done, an exit ends no turn; a crash mid-turn still gets its banner.
* test(runtime): the projection reference keys on the host turn-end stamp too
* test(relay): restore the Copilot retry test while CI isolates a runner error
* test: key the hot-path projection reference on the turn-end stamp; relay retry restates the end
* fix(notifications): host turn identity per agent session; keep later blocking-Stop banners past main's cooldown
- The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an
agent session is in: Codex by its own turn id, Command Code by its transcript prompt position,
every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs
from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn
get their banner; duplicates, replays, tool reopens and result retries restate it. The record
is saved with the status row (validated on restore) and never published.
- Main's desktop and mobile notification cooldowns are back to main's code exactly.
- A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands
inside main's cooldown is held, latest only, and sent once after that cooldown through main's
unchanged gates; the renderer learns main's answer to anchor it.
- One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that
stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other
pane, and an unbound PTY, keeps main's behavior.
- A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements
ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn.
* test(notifications): run the whole-pane harness through main's shared desktop cooldown, not main's service module
* fix(notifications): decide hook ownership from how the host identified each turn end
The execution host now labels every classified turn end with one optional
disposition beside its end stamp, by the same rule for every agent:
identified (an unused prompt opening, a new native turn id, or a verified
re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate
only when the agent supplies both a turn id and a verified continuation
flag; anything else is ambiguous and never mints. Replays and retries
restate the decided answer, and local and relay hosts carry an end only
within a done segment.
The renderer replaces the Claude/Codex pane gate with one evidence
decision: identified ends are hook-owned, ambiguous ends keep the title
lane's role, and rows without a disposition (older hosts, unclassified
agents) keep the previous behavior exactly. A title working edge on a
hook-owned pane keeps its turn bookkeeping and only the hook receipt; an
owned idle edge settles after the existing max wait, or announces through
the title path if the end turned out ambiguous. Idle and working title
edges resolve the same owner (the title's agent, else the foreground one).
The held later banner for identified continuations is unchanged.
* test(notifications): give the whole-pane harness's host event its local connection id
* fix(notifications): a settled cancellation ends the open turn, so its late Stop folds as on main
A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner.
* fix(notifications): a settled cancellation ends the open turn on every lane, so a relay's late cancel end restates none
The desktop restates a WSL relay's end as decided. The relay never learns a
cancel the desktop inferred for a non-Claude agent, so Grok's own late
stop_cancelled reached the desktop as an identified end and gave a second
banner. One predicate now decides both lanes: after a settled cancellation
that carried no end, a restated end keeps the shown one, and a child event
carrying the cancellation also ends the open turn. The relay's own cancel
row is the main agent's, never a child's.
* fix(notifications): a relayed done segment shown without its own end keeps showing none
The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late
plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the
row's turn-end fact instead, which the folded row satisfies again.
* fix(sidebar): a slept chat workspace reads asleep and hides
* fix(sidebar): a wake during Sleep's teardown keeps the workspace awake
* test(sidebar): give the activation-race mock store the chat tab map Sleep reads
* feat(native-chat): the host marks the chats an explicit Sleep stopped
The fact "the user's Sleep put this chat to rest" now lives on the host
that runs the chat, on its session record, instead of in the window.
- New runtime capability agent-session.workspace-sleep-mark.v1 and an
optional markAsleep on agentSession.sleepWorkspace (strict params, so
only hosts advertising the capability are sent it).
- The Sleep close decides each chat's receipt inside the same per-session
step that stopped it; with markAsleep it writes sleptAt in that step's
record write, so a chat is marked exactly when it is in the receipt.
- The next send the host hands over clears sleptAt, whichever client sent
it; the idle sweep and plain stops never mark.
- sleptAt rides the status summary (optional field), its equality, and
the retained summary of a closed chat, so it survives restart through
the persisted tab index.
* refactor(sidebar): read a slept chat from its host's mark, not a window record
The window no longer keeps its own saved record of which chats Sleep
stopped (explicitlySleptChatsByWorkspace). That record had to mirror every
host event that should end it (sends, replaced conversations, renames,
profile and WSL moves, removal, wakes during teardown), and each missed
mirror was a bug. The host now owns the fact (previous commit).
- Delete the record, its schema, store slice, workspace-session field
registrations, moves, renames, purges, migrations and hydration, and the
renderer clearing hooks. Those files are back to main.
- Opening a workspace no longer wakes its slept chats (product ruling):
drop the chat Sleep-attempt tokens and their set-active-worktree hook. A
slept chat wakes only when a send reaches its host, from any client.
- The status bridge mirrors each chat tab's published sleptAt into a
store map keyed by the chat's pane key, dropped when its projection
unmounts and never saved; a chat whose host runs it again is not slept.
- The asleep derivation, hide filter, palette, board and context menu read
that map. Explicit Sleep sends markAsleep only to the local host and to
hosts last probed with the new capability; a project move never does.
* fix(native-chat): preserve host-owned Sleep across fallback and legacy tabs
* fix(native-chat): clear Sleep when a new send is admitted
* chore: restore incoming documentation after merge hook formatting
* fix(native-chat): let admitted sends supersede pending Sleep receipts
* fix(sidebar): wait for host Sleep state before showing restored chats
* fix(sidebar): retain host Sleep answers when chat projections remount
* fix(native-chat): clear Sleep when queued drafts become sends
* test(sidebar): cover unknown and known chat Sleep state on the board
* test(vault): expect both host status capability checks
* test(native-chat): consolidate Sleep behavior coverage
* fix(native-chat): commit Sleep wake with send admission
* test: pin visibility while chat host is unidentified
* test: consolidate workspace sleep regression flows
* test: keep status feed regression within renderer boundary
* Editor: git Discard pauses that file's autosave in every workspace; read-only markdown/CSV never pushes over edits (phase 1b-3)
- A Source Control discard holds autosave (and waits for in-flight saves) for every tab of that
file in every workspace, keyed by its model save key, until the discard finishes or fails; the
quiesce now returns the release, called in a finally. Watcher notices don't release it. A 120 s
backstop only shows the changed-on-disk banner on dirty tabs and never reopens autosave. This
closes the race where the compare fallback on older hosts reads disk just before git restores it.
- A read-only tab renders raw Monaco source whatever its type, so it counts as a code view: a clean
read-only .md/.csv tab can no longer push disk text over a writable tab's unsaved edits.
Only removes or delays writes. One autosave timer per model moves to 1b-4.
Co-Authored-By: Claude <noreply@anthropic.com>
* Editor discard hold review: release on a failed quiesce, cover unstaged diffs, re-show an overdue banner (phase 1b-3)
- A quiesce that throws (e.g. an unresolved CSV cell) releases its hold and rejects, so the discard
reports the error instead of hanging with autosave off; a bulk discard frees the holds the other
paths took.
- Holds key on the physical file (host + path), so an unstaged diff tab of the file in another
workspace is paused and drained too; save-queue keys are unchanged.
- Past the backstop, a dirty held tab whose banner was dismissed gets it back, so the pause is
never silent. Taking a hold no longer re-syncs; the save-time check already refuses.
Co-Authored-By: Claude <noreply@anthropic.com>
* Editor discard hold: key the physical file with the 1b-1 path rules, not the client's URI rules (phase 1b-3)
The hold key went through the Monaco model URI, which normalizes paths by the client platform, so
on a Windows client `/repo/a\b.ts` and `/repo/a/b.ts` on a POSIX host shared a key. It now uses
the owner key plus `editorBufferOwnerPathKey` (new, in editor-buffer-identity), which applies the
1b-1 rules: POSIX backslashes kept, separators folded only for proven-Windows paths, UNC server
case, and WSL aliases on the local Windows client. Same on the tab and the discard-path side.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(terminal): hidden tabs reconnect to sessions that survived a restart
pty:hasPty answered from the provider's attached-this-run set, so right
after a restart every surviving session read as exited and #29459's
hidden-agent hold kept live hidden tabs disconnected until shown. On an
attached-set miss, ask the host through the provider's existing liveness
probe; providers without one (the in-process provider, its own sole
owner) keep their answer.
* test: remove liveness fixture type assertion
* fix(terminal): a folder on a dead network mount fails the terminal start instead of hanging it
Bound the local folder checks a terminal start runs (folder-workspace admission and the
worktree-folder cwd fallback) with a 5s deadline, and make the fallback check async so it
can no longer block the main process. A timeout refuses the start with the existing
folder_workspace_path_unavailable code.
* refactor(terminal): build the folder deadline on the existing working-directory validation helper
Drop src/main/local-folder-stat-deadline.ts. statLocalFolderWithDeadline now lives in
working-directory-validation.ts and reuses its abandon-on-signal waiter and UNC route lane,
so a dead SMB share caps how many folder checks can hold fs threads.
* fix(terminal): keep only the non-blocking saved-folder check; drop the folder-check time limit
Per owner decision, a hung folder waits as before. Folder status and working-directory
validation return to main; the saved-folder fallback check stays async so main never blocks.
* fix(ssh): the Remote Hosts menu lists a capped set of idle SSH hosts
* fix(ssh): the SSH Hosts settings list renders hosts a page at a time
* fix(ssh): window the SSH Hosts settings list and keep project hosts in the capped menu
Settings > SSH Hosts now windows its rows against the Settings scroller with the
shared VirtualizedList instead of paging 100 at a time, so every host stays
reachable by scrolling or the filter while only the rows near the viewport mount.
The Remote Hosts menu cap rises to 25 idle hosts, fills it with hosts that own
projects first, and the overflow row says to search the rest in Settings.
* fix(ssh): search the Remote Hosts menu's capped idle hosts in place
Web clients reach SSH hosts through this menu but have no SSH Hosts settings
pane, so a link to Settings stranded every host past the cap. The menu now shows
a search box whenever idle hosts are hidden; it narrows them by name, and the
overflow line says how many are left. Also gives the two existing status-bar
store mocks the repos field the menu now reads.
* fix(ssh): typing in the Remote Hosts menu goes to its host search
The search box is not a menu item, so arrow keys can't reach it; letters typed
while the menu has focus now land in the search instead of Radix typeahead.
* fix(ssh): the Remote Hosts search keeps focus and drives its results by keyboard
The host rows are not menu items, so the menu's arrow keys never reach them. The
search box is now a combobox over the connectable results: it keeps focus, arrow
keys move the active result (announced through aria-activedescendant), and Enter
connects it through the same path as the row's Connect button.
* fix(ssh): Remote Hosts search only reaches hosts that match, and keeps the active one in view
Every not-connected row now has to match the search, so Enter can't connect a
failed host the user didn't type. Arrow keys scroll the active result into view.
Also drops restyles on the search Input and fixes the singular overflow default.
* refactor(ssh): find hidden Remote Hosts in a searchable picker, not a search box inside the menu
A search box inside the dropdown kept fighting the menu over focus, Tab, Enter
repeat and IME input. The overflow row is now a menu item that opens a host
picker (Command in a Popover, as the style guide prescribes for single choice
with search). It ranks exact names first so any host is reachable by typing its
name, mounts at most 50 results, and says how many more match. The menu keeps
config order when nothing is capped.
* fix(ssh): closing the host search from the keyboard returns focus to the status button
The picker is anchored, not triggered, so Radix had nowhere to restore focus and
it fell to the page. A keyboard close now focuses the Remote Hosts button; a click
elsewhere keeps focus where the user clicked. Also drops the padding restyle on
PopoverContent.
* fix(ssh): the host search marks the keyboard-selected host with the palette highlight
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Align plain mobile/legacy residual metadata after snapshot-boundary query stripping; preserve genuinely transformed span metadata.
Fixes#9682. Based on the report and suggested correction by @bbingz.
Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix(runtime): a server's new terminal keeps answering under its own handle after main's pane adopts it (switch-on builds only)
* fix(runtime): keep handle branching on the leaf path; only callers with no leaf fallback read the adopted own handle (switch-on builds only)
* fix(runtime): with no window, a terminal's own handle keeps naming it after main's pane adopts it, as before main listed panes (switch-on builds only)
fix(mobile): retain Claude chat when its pane is renamed
Distinguish a manual title override from a later shell OSC title, keeping transcript identity and live hook selection while preserving exit retirement.
test: cover restored mobile chat after manual shell-like rename
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
* test(claude): real-claude suite passes on any signed-in machine and current model aliases
Model ids come from the CLI's own model list instead of dated ids, tests that need a SessionStart hook or an effort bring their own project settings, and the supervised-stop test launches and reads its transcript under one Claude home, as acquisition does.
* test(claude): one real-claude flow for the model, effort and permission pickers and the context meter
Drives the live CLI through the production send path (model list, set model, apply effort, read settings), checks the turn's context report, and relaunches into a new permission mode the way Orca changes it. CI only meets these requests through stand-ins.
* test(claude): accept the ran options a send now reports
Since #30561 a Claude send resolves with the options it ran under; the stop test only needs the send admitted, and the options flow checks the ran options are exactly the per-message pick.
* test(claude): an alias pick's dispatch report may omit the model the CLI resolved
* fix(orcad): refuse new terminals between an update's last census and the stop
The last census before an SSH or WSL update stops the server now closes the
server's terminal admission first. Every spawn lease (acquireWorktreeTerminalSpawn,
which both spawn paths hold across the provider spawn) passes the fence, the
census waits for leases taken before the close, and an update that leaves the
incumbent serving reopens it. A reattach to a session the daemon still runs is
still admitted.
* fix(orcad): fence-exempt reattaches spawn attach-only; reopen admission when no stop was sent
* fix(orcad): stop nothing once the recount's admission fence may have lapsed
* fix(orcad-migration): a sidebar view preference never blocks moving an SSH host to its server
* fix(orcad-migration): keep repo filters on the desktop, keep later acknowledgements; e2e repro
* feat(terminal): the window's store shows main's pane start status (layout-owner build)
A status-only frame changes one store entry and no pane tree, so the pane's view stays mounted.
* refactor(terminal): keep the frame sync within its line budget
* feat(terminal): every pane main starts shows 'starting' from before it is written (layout-owner build)
* feat(terminal): a pane main is starting waits and attaches; a failed start shows why (layout-owner build)
Fixes owner-visible row 15: a failed agent or setup start no longer opens a plain shell.
* test(terminal): iterate store listeners directly
* fix(terminal): an exit that reports no code keeps its pane, as lost contact (layout-owner build)
* test(terminal): a pane main started attaches to main's terminal at its own grid
* test(terminal): detach census tests assert main's move and frame with the switch on
* test(terminal): exited-pane tests assert main's close with the switch on
These 8 have failed switch-on since #31130 moved the exit close to main.
* feat(terminal): main writes and starts every split of a pane it holds; the window attaches (layout-owner build)
The window's split, a startup split with its command, and a CLI split all go through main's
terminal.split; main's frame mounts the pane, with or without a window intent. Deletes the
window's split wait and withdraw, the split appear intents, and the queued-split skip and cancel.
The e2e helper uses the real split. A startup split leaves focus on its source pane.
* fix(terminal): the attach-only wait leaves sleeping, resuming and held panes to their restore path
* fix(terminal): a split pane attaches to the terminal main bound it to; a taken pane id is refused
Also restores the proof that a split closed while it starts gets one answer and leaves no
pane or start status behind.
* fix(terminal): a window split main cannot start still shows its pane, failed, with why
* fix(terminal): check a taken pane id first, keep the start's own error, clear a gone pane's status
Three test stores gain the pane-start field the window's store always has.
* test(terminal): the admission stub answers whether its pane is still written; a closed pane's failed start clears
* refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module
* test(terminal): a pane main wrote and the user closed mid-start drops its failed status
* fix(terminal): a split main starts is listed to phones and session.tabs without a window's report
* fix(terminal): a reconnect shows its pane starting and why it failed; a lost owner race shows no failure; a split no one asked to see keeps the phones' tab
* fix(terminal): a kept create another owner won shows no failure; the window attaches to the winner
forSave captures the workspace's route (and its pairing pin) once; every read, stat,
status.get and write of that save uses it, so a workspace that moves between local and
a paired server mid-save never sends the write to the other host unpinned.
Scoped Session History built its candidate list by spreading each
discovery's files into push(), which throws RangeError once one root holds
more files than V8 accepts as call arguments. Append item by item instead,
and do the same for the bucket-layout candidates (unbounded on unlimited
scans) and the search index's unchecked-paths hand-off.
Tracked in STA-11092.
* fix(terminal): start an agent's terminal at its first real size instead of after a 12-frame wait
The startup grid settle waited for the grid to change and then hold for 2 frames;
a grid that was right from the start always paid the 12-frame maximum (~90 ms at
120 Hz, ~200 ms at 60 Hz, seconds when frames are throttled, since it also cleared
the 250 ms fallback timer). Its only case, the setup split, was retired in #29819.
Command panes now connect like plain panes: on the first frame with a real
measurement, or the 250 ms fallback timer.
* fix(terminal): bound startup by completed fit and deadline
* test(terminal): brace startup contract control flow
* fix(terminal): release pending startup when window hides
* fix(runtime): a phone reads and saves Markdown tabs on a headless server
With no window to relay through, markdown.readTab and markdown.saveTab answered
renderer_unavailable. Main now reads the tab's file (or its saved draft) from disk and
saves with a version compare plus the disk write precondition, in the shapes the
window's editor relay returns. With a window attached the relay is unchanged.
* Headless Markdown save: keep the draft's disk baseline, honour read-only tabs, re-find the record to clear
* Use the disk baseline only for a draft
* ci: run the phone Markdown spec's orcad case in the orca serve mode-switch job
* Headless Markdown: answer from disk only; a window's saved draft stays its view state
Clearing a saved draft wrote workspace layout from main, which no layout command carries.
Main now never shows or edits a draft; the desktop's restore check reports a disk change.
* Phone Markdown: one path through main and the disk, window or not
A paired server's workspace is read and saved on that server with files.read /
files.writeIfUnchanged, so it answers from its own disk and keeps the precondition.
The window relay (ui:mobileMarkdownRequest and the renderer bridge) is removed.
* e2e: a phone saves a Markdown tab the desktop holds unsaved edits in
The phone reads the disk with the tab marked dirty; its save lands on disk, and the
desktop keeps its draft and shows the changed-on-disk banner.
* Group the Markdown disk host's imports
* Drop an unneeded store cast from the dirty-readers test
* Phone Markdown: refuse a save on a server too old to check the disk; cover the untitled tab
A server before #30605 has no files.writeIfUnchanged, so the save is refused with the
existing update-required message rather than written unchecked.
* Phone Markdown: refuse a file an SSH host holds outside the workspace; pin the untitled answer
* Phone Markdown: untitled tabs stay read-only; older servers save like the desktop editor; a closed-source preview still reads
- The desktop lists an untitled document's tab with isUntitled (optional, additive); main answers
it read-only with unsupported_untitled and refuses its save before touching the disk.
- A server without the files.write.disk-precondition capability gets the desktop editor's guarded
save: compare from here, then a plain files.write, or a conflict. Every call of a save is pinned
to the pairing it probed.
- A preview whose source tab was closed has no saved record; it reads its own file, read-only.
* e2e: read the untitled tab's answer without a cast
* Phone Markdown: pin a save's pairing before its first read
A save captures the server's pairing before it reads, and every call of the save, the read
included, is pinned to it, so a re-pair between the read and the write is refused.
* Test: with no window, a phone saves an existing untitled.md and never creates a deleted one
---------
Co-authored-by: E2E Test <e2e@test.local>
* test(layout): downgrade probe for the layout-owner session format, and the touched-records patch's guard
Downgrade probe (ledger L8): a checked-in golden of the session main writes in a layout-owner
build (layout commands through the Store plus a window save), matched byte for byte; a
switch-off Store hands it on unchanged and a switch-off window restores its tabs, panes, groups,
editor file and browser tab. Regenerate with ORCA_UPDATE_LAYOUT_OWNER_GOLDEN=1.
L104: for each writer of the touched-records patch (layout.apply, the cross-partition move, the
window save), the patch equals a full save on every partition the load leaves alone, and differs
only on records the load normalizes otherwise. Two concrete legacy cases are pinned, and a
switch-off window restores each differently after a full save, so the patch stays.
* test(layout): type the guard's partition ids and locate the golden without import.meta
* test(layout): pin every legacy fixture's patch difference, assert restored panes and group selection, keep the golden deterministic
* fix(orcad): voice dictation on headless orca serve hosts
Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.
The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.
The parity test now requires both roots to install speech.
Reported-by: delorean2020 (@delorean2020)
* fix(orcad): retry Node download drops, stage the recognizer on Windows, review fixes
- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
in the profile, so a loaded addon never locks the app's files against
an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
helper per test file instead of repeated casts; comments.
* fix(orcad): cloud speech is ready only with a key this host can decrypt; prune superseded recognizer copies
Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).
* fix(orcad): never prune staged recognizers before the profile lock; copy linked packages as real files
Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.
* fix(orcad): remove a partial recognizer copy when staging fails (Codex round 4)
* fix(speech): never hand out another store's ciphertext as the OpenAI key (Codex round 5)
* refactor(orcad): no recognizer copy on Windows; Windows orcad keeps cloud dictation only
Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.
* fix(speech): name the fix when an old server refuses a mic tap; always set the recognizer env; retry ENOTFOUND (final Opus review)
* fix(managed-server): probe install completeness relative to the slot dir
CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.
* fix(mobile): pin host.platform's read to a platform type, independent of which zod copy resolves
With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.
* fix(mobile): keep the host.platform read line intact for its RPC mutant; narrow in a second step
Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.
* refactor(cli): app-executable resolution in its own module; drop the platform guard now zod is aligned
launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.
* refactor(cli): import app-executable helpers from their module; no re-exports from launch.ts
* fix(chat): separate applied Claude choices from provider reports
* fix(chat): preserve Max effort without settings readback
* test(chat): retain Max effort confirmation contract
* test(chat): a literal model change no longer pre-flights the catalog
* fix(chat): record observed execution independently of accepted choices
* fix(chat): fence model reports at prompt input before recording
* test(chat): inspect Fast evidence without unsafe outcome access
* test(chat): honor input preparation in turn fixtures
* test(claude): accept dispatch metadata after stopping a turn
* fix: retire deferred chat options at input preparation
* fix(github): repo cache eviction drops every host scope's entries, keeping a twin's own
Eviction and issue-source changes matched only unscoped key prefixes, so
SSH-, runtime- and source-scoped entries survived repo removal. Keys are now
composed by one builder and read back by its inverse, so eviction classifies
each key by host and keeps only a surviving same-id twin's bucket. Hosted
review (GitLab MR) entries are evicted too.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(github): read task-source scopes with empty parts back whole
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(github): an issue-source change evicts only the changed twin's buckets
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The deferral keeps a terminal's connected flip, noted inside syncWindowGraph's
record update, from re-entering syncWindowGraph and recursing. Layout-owner
(switch-on) builds only.
validateRegisteredRepo took the first row with the requested id, so for a repo id
registered locally and over SSH it read the other twin and refused the second
twin's fetch as a path mismatch. It now matches id and path together, uses the
GitHub source host when one is named, and lets a PR-refresh candidate's host pick
between twins at one path; a lone row's own fields still win over stale candidate
fields.
Create, read, and save Markdown notes on headless hosts through authorized file operations and persisted editor tabs.
Addresses the Markdown-note reproduction in #22186, reported by @shota46. Thanks to @Jinwoo-H (#31101), @brennanb2025 (#25436), and @Meapri (#23357) for related community implementations and regression coverage. Those PRs retain broader/distinct scope.
Validation: focused tests, paired-headless E2E, iPhone simulator proof, and all CI checks passed.
* A paired desktop shows a layout-owner server's held panes on the existing banner; Reopen asks the server (switch-on builds only)
The server's frames carry status 'held' on a pane a newer build's terminal service may still
run. The desktop now lists those panes on its existing newer-build banner, with the same wording
and states. Reopen calls the server's new `layout.reopenHeldPanes` under the server's ids: the
server stops its newer service, as the desktop's own Reopen does, and starts each pane still
held. The desktop starts nothing; the server's next frames clear the status and bind the pane.
A refusal or failure shows the banner's failed state.
Served and advertised (`layout.reopenHeldPanes.v1`) only by a layout-owner build, to paired
desktops (not phones). A server without the capability, or that publishes no status, shows no
banner, as today.
* test(layout): held server pane stays Reopening until the reattach frame, stays failed after a refusal, and shows nothing without the reopen capability
* fix(layout): a held pane's Reopen acts only on its own host and on main's published status (switch-on builds only)
- Reopen needs the clicked pane's key; a pane that already left the banner's store does nothing,
so a server banner clicked as its frame drops the pane never stops this machine's newer service.
- Local versus server is decided by the pane's host, not by whether it has a release.
- The banner portal for a pane with no terminal handle exists only in a layout-owner build.
- The server reopens only panes its published layout shows held, the status the desktop saw.
* test(layout): Reopen ignores a pane rebound to a new terminal whose held entry is still stored
* test(layout): the dropped-pane Reopen test checks for the reopen method, since the view report shares the RPC client
The fetch picked a twin's row by id first, so the SSH copy's results were cached
under the local copy's key; the task-page reader keyed by the raw executionHostId,
so an SSH row carrying only connectionId read the local bucket; and an unstamped
row was keyed by the focused server while its fetch ran locally. The repo lookup
now prefers the row matching both id and path, and writer and reader both key by
getRepoExecutionHostId.