Commit Graph
16002 Commits
Author SHA1 Message Date
Jinwoo HongandClaude 5f134d8f01 feat(mobile): show and use the desktop's server workspaces on the phone (#26501)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs

Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): show SSH host health on the workspace row host badge

Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): compose the phone-mirror topology and check the cold snapshot

Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name an unhealthy SSH host even in a single-host list

The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): keep phone-mirror teardown errors from masking test failures

Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): clear stale SSH host badges and announce them without the dot

A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(runtime): let a paired desktop register its phones as delegated mobile devices

A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(runtime): move the mobile device revoke primitive below dispatch

Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): keep a runtime grant until its delegated phones are revoked

A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked

Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): relay a paired phone's server-workspace requests through the desktop

A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): type the single-host health fixtures inline so the test tsc passes

Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): state the desktop relay contract in its entry module

main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the relay's configured-server adapter

The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the repo-metadata goldens for SSH host health

The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): tighten the desktop relay after elegance review

Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): bound relay frames and stop sticky relay verdicts

Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run targeted desktop-owned calls on the desktop

The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone

The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing

The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers

The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name a server workspace's host on the phone's calls

A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete

A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch

Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the paired desktop's server workspaces on the phone

Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin that a desktop failing the hosts list leaves only its own rows

The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): mirror the desktop's shown hosts exactly, one host included

Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): scope a server workspace at its route, never the desktop

Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e

A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): gate a server workspace on that server's own features

A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(ci): run the remote-runtime WebSocket opener test on Node

Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): give the file preview the workspace router, back to 128 page assets

The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): derive the phone allowlist from the routing table

Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): drop unread freshness fields from server workspace lists

The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): read only the rows of a server's workspace list

The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show a server workspace as disconnected while its server is down

The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): share one desktop server-list feed between the host list and workspaces

A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag

eff410fe30 dropped `stale` from mobileRelay.hosts.worktrees, so the mirror spec's
schema never parsed and its first poll timed out. It now waits on the server's
relay/health from mobileRelay.hosts.list, the field the phone itself reads.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* build(mobile): re-measure the page's script sweep for server workspaces

Main already sat on its 78-script ceiling (re-swept: 78, table: 74); the workspace
route's shared modules split importer sets three ways more, reaching 81. Re-swept per
route prefix on this head, as #24257 did for its feature; the spread is now 1 to 11.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): export the server-list feed's React reader beside the feed

useRelayHosts moves from workspace-server.ts into relay-hosts-feed.ts, so every reader (workspace screens now, the new-workspace sheet next) subscribes through the feed's own module. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep polling the server list and read a server's reachability from relay

A hosts-list read slower than the poll stopped the feed for good, since the next tick was
scheduled when the read started; it is now scheduled from the finish. A server workspace now
reads connected only when the desktop relays to it, so a server with no status yet is not shown up.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show a reached server as connecting and a too-old one as disconnected

A server the desktop has reached but whose status is not answered yet is moments from relaying,
so it reads connecting; a server too old to relay to never will, so it reads disconnected.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(shared): move the account capabilities into their own group module

Main's merge put protocol-version.ts at 301 lines, over the 300 cap. The account group moves out
like the relay and skill groups did, same strings in the same order; the page's script sweep is
re-measured because the split changed its chunking (81 to 79 at 16 routes).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): name a server workspace through the request envelope

The server name now rides the generic request envelope from #27815 instead of
its own executionHost option, bridge grant and per-transport plumbing. Server
rows show only where the transport carries an envelope (native, or a shell
that grants rpcEnvelope), so an older app keeps today's list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the server-call wire and the no-envelope deep link

A server workspace call still reaches the desktop as the same top-level
executionHost frame, on the direct and relay wires, and a deep link into a
server workspace under an app without rpcEnvelope shows the unreachable screen.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): read a relay stream's envelope slot in sorted key order

Equal envelopes name one cleanup slot whatever their key order, the scoped view
passes its client's envelope answer through as it does the generation, and the
direct wire harness is shared instead of rebuilt.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): a phone's new chat in a server workspace follows the desktop's chat setting

A relayed agent.launch ran on the server, which picked chat vs terminal from its
own setting. The desktop relay now stamps its own default as nativeChatDefault;
new servers use it in place of their setting (feasibility still decides), older
servers strip it. Outside the replay fingerprint.

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

* refactor(mobile): move the relay's delegated phone grant cache into its own module

Keeps mobile-desktop-relay.ts under the line limit once #27749's page relay lands on it.

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

* fix(mobile): show the early terminal tab from the relaying desktop's chat default

The early-tab preflight read the server's own chat setting while the launch
used the desktop's stamped nativeChatDefault, so on a windowed server with
chats off a relayed chat launch flashed a terminal tab. Both now read the
settings through readAgentLaunchModeSettings(runtime, nativeChatDefault).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 23:35:33 -04:00
OrcaWinandm4air f50c23cc35 fix(ssh): let the stdio bridge clear an ssh -L an earlier Orca left on its port (#28509)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 20:32:48 -07:00
Neil 13a75d3939 [CI] Reduce redundant test work and import existing helpers directly (#28365)
* [CI] Reduce redundant test work and import existing helpers directly

* [CI] Preserve provider guards and pin the mobile browser test core

* [CI] Fix CI test setup and verify retained contracts

* [CI] Compare native media chunks without boxed byte arrays

* [CI] Detect shifted native media reads with position-sensitive bytes
2026-10-13 20:25:00 -07:00
701e9f2bdf feat(new-workspace): add "None" agent option that opens only the setup script (#25617)
* feat(new-workspace): add "None" agent option that opens only the setup script

New workspaces previously required choosing an agent or a blank terminal,
so a setup script that launches its own agent always ran next to an extra
shell. The quick-create agent picker and Settings > Agents now offer
"None (setup script only)": when setup runs, the workspace opens with just
the Setup tab focused; without a setup script it falls back to a blank
terminal.

The default is stored as defaultTuiAgent 'blank' plus a new optional
newWorkspaceSetupOnly flag, so clients and servers that only know 'blank'
degrade to a blank terminal instead of rejecting an unknown value.

Closes stablyai/orca#25247

* fix(new-workspace): honor the "None" default for CLI-created workspaces

"None (setup script only)" only took effect in the new-workspace dialog.
A workspace created with `orca worktree create` still opened a blank
terminal first and left it selected, because the main process builds
those tabs and never read the setting.

The main-process tab provisioning now skips the blank terminal when the
default is "None" and a setup script runs in its own tab, so Setup is the
only tab. The same rule applies to CLI creates that activate the
workspace, and to dialog creates the user navigated away from before
they finished. A split setup or repo default tabs still get their shell.

The setup-only check moved to src/shared so main and renderer share it.

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

* fix(new-workspace): register the CLI activation caller and stay under the line limit

The CLI activation path now passes providesInitialSurface for the "None"
default, so it has to appear in the test that lists every caller using
that option; the full test run failed without it.

Merging main pushed worktree-creation-flow-execute.ts to 301 lines against
the 300-line limit. Naming the background-create condition brings it back
to 300 with no behavior change.

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

* fix(new-workspace): open a blank terminal when the setup-only Setup tab fails to spawn

The "None" default skips the blank shell because the Setup tab will be the
only surface. If that spawn fails, the workspace was left with no terminal.

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

* style: format the setup-only fallback terminal call

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

* fix(workspace): save None through host settings and honor per-create choices

* fix(settings): clear None when legacy clients choose a blank terminal

* fix(settings): clarify None with split or skipped setup

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-13 20:12:44 -07:00
Brennan Benson a622bb0f4b fix(native-chat): phone Stop reaches a message the agent hasn't answered; unreadable task list reports unconfirmed (#28243)
* fix(native-chat): phone Stop reaches unanswered sends; unreadable task list is unconfirmed

The phone named the running turn on every Stop, so a Stop pressed before the
agent echoed the message said "Stop not sent" and the agent ran on. Against a
host advertising agent-session.conversation-stop.v1 the phone now sends the
Stop that names no turn, as the desktop already does; the host stops whatever
the chat has in flight. A card's Cancel still names its turn, and older hosts
keep the turn-named Stop.

A background-task Stop whose host could not read the session's task list read
the failure as "no tasks" and answered cancelled:false. It now refuses as
outcome unknown, the refusal a timed-out background stop already returns.

Adds host tests pinning the Stop-before-echo races on the unnamed Stop: Codex
turns admitted or started before the echo, a later send just before the Stop,
a retried Stop, and a Stop after a failed one. All pass on main unchanged.

* fix(mobile): show Stop before the turn opens; a card's Cancel keeps its turn

The phone offered Stop only once a turn existed, so the Stop that names no turn
was unreachable while a send waited for the agent's echo. Against a host that
advertises conversation stop, a working chat now offers Stop.

A card's Cancel chose the untargeted form from the prompt after the
prompt-cancel capability filtered it out, so against a host with conversation
stop but no prompt cancel it sent a Stop naming nothing. It now always names
the card's turn.

The race test for an admitted turn that cannot take an interrupt accepts
success only once the host has let go of the agent, and otherwise requires the
unconfirmed refusal.

* ci: rerun against current main

* ci: rerun against current main
2026-10-13 19:50:06 -07:00
Kelvin AmoabaandBrennan Benson 464bd5a54b feat(native-chat): combine model and options into one picker (#27972)
* feat(native-chat): combine model and options into one picker

Setting a model and its effort took two menus that each closed on a pick.

* feat(native-chat): close the model picker on an effort pick

Effort is the last thing set before typing.

* fix(native-chat): write the picker's kept rows after commit

React Doctor rejects a ref written in render, and a discarded render could leave a row behind.

* fix(native-chat): open the picker once per /model or /effort, tooltip only when informative

The session kept a slash command's picker request, and the picker remounted
open from it whenever a question or approval card gave the composer back. The
request is now consumed when the picker sees it, and open state lives in the
popover, which unmounts with the pill, so it comes back closed.

The pill's hover tooltip only named the categories the pill already shows,
and the picker covered it on click. It now shows only the disabled reason or
the not-confirmed note.

Also drops the removed Options key from the locale test and covers the new
short effort labels.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-13 19:27:05 -07:00
Neil 31e5003572 fix(accounts): launches a WSL host starts itself use the account selected for its distro (#28464)
* fix(accounts): launches a WSL host starts itself use the selected account

The desktop now pushes the account env it selected for a WSL host's distro
to that host's orcad (accounts.setHostLaunchEnv, capability-gated). orcad
lays it over every terminal create (CLI, orchestration, automations, bare
commands and presets), structured chats and AI commit text, as the
desktop's own launches into the distro do. `orca account` on the host now
points at the desktop that owns the accounts.

* fix(accounts): cover splits, child agents and restarts; skip SSH workspaces

Review follow-ups: terminal splits and structured chats' child agents take
the host account env too; an SSH workspace's terminals never get distro
paths; orcad saves the pushed env so a launch before the desktop reconnects
keeps the account; a failed push retries unless a newer one supersedes it.

* fix(accounts): retry a host account push when the host's capabilities are unknown

* test(orcad): the startup mock restores host account env

* refactor(accounts): serialize host account pushes with the shared keyed queue
2026-10-13 19:15:28 -07:00
Neil 89e8aafadc fix(accounts): a focused WSL host shows and switches its distro's accounts (#28465)
* fix(accounts): a focused WSL host shows and switches its distro's accounts

A WSL host's orcad has no account services, so the status-bar switchers,
Settings > Accounts and the Codex restart prompt asked it for accounts and
got 'Account services are not configured'. This desktop owns that distro's
accounts (the same ones a WSL pane uses), so those surfaces now route a WSL
host to the desktop roster for its distro, and WSL-host panes join the
distro's Codex restart lane.

* fix(accounts): reuse the localized WSL runtime label path for a WSL host
2026-10-13 19:15:17 -07:00
Brennan Benson 9db7b8b6b2 fix(files): never write through a dangling symlink (#28385)
* fix(files): never write through a dangling symlink

A path that doesn't exist yet was authorized by walking up to the nearest
existing folder. A dangling symlink fails realpath with ENOENT, so the walk
stepped over it and approved the link's own path; the write then followed
the link and created its target outside the allowed folders. The walk now
continues from the link's target, and returns that real destination.

* fix(files): refuse any dangling symlink in the missing-path walk

Following a dangling link by hand resolved relative targets against the
lexical parent, not the link's real folder: through a folder link the walk
could loop forever or authorize the wrong file. Refuse instead: realpath
can't say where a dangling link leads, but a write would follow it.

* test(files): drop the loop case that duplicated the dangling-link refusal
2026-10-13 19:08:15 -07:00
Brennan Benson efbe3210d6 perf(session-history): keep a timed-out scan's finished transcripts (#27937)
* perf(session-history): save finished transcripts while a scan runs, not only after it succeeds

* test(session-history): check the mid-scan save without relying on a cancel race

* fix: avoid cache checkpoints for unchanged scan batches

* fix: checkpoint committed session cache changes

* fix(ai-vault): retry failed in-flight cache save on flush
2026-10-13 19:07:10 -07:00
Neil b525a3ba91 refactor(wsl): reuse the WSL-host predicate, error helper and a shared owner lookup (#28447)
* refactor(wsl): reuse the WSL-host predicate, error-message helper and a shared owner lookup

- isWslHostRuntimeEnvironment becomes a type guard and replaces inline
  orcadDeployment?.kind === 'wsl' checks in main and renderer.
- WSL modules use shared/error-message instead of two local copies and three inlined ones.
- The workspace row-owner lookup moves out of the IPC bridge into lib/, since the terminal
  pane and host removal import it too.
- The per-terminal WSL unavailable selector returns early when no WSL host exists.

* test(wsl): the unavailable selector is quiet when no WSL host exists
2026-10-13 19:01:07 -07:00
OrcaWinandm4air e4824ce9c7 fix(ssh): a system-SSH host whose sshd refuses forwarding reaches its managed server over exec (#28408)
* fix(ssh): a system-SSH host whose sshd refuses forwarding reaches its managed server over exec

A host Orca reaches through system OpenSSH (ProxyJump/ProxyCommand, security
keys) and whose sshd runs AllowTcpForwarding no showed connected while no
managed-server traffic could pass: the ssh -L listener comes up before sshd is
ever asked for a channel, and the stdio bridge excluded system SSH.

The managed tunnel now dials once through a fresh system-SSH forward and reads
OpenSSH's own channel answer from its stderr. An administrative refusal closes
the forward and carries the tunnel over the stdio bridge on a system-SSH exec,
on the same persisted local port; a host where the bridge cannot run fails the
connect with that reason instead of reporting a dead forward as up.

Refs #24863

* fix(ssh): keep refusal logs under a quiet LogLevel; fail a forward that exits mid-check

* fix(ssh): an orphaned forward from before LogLevel=INFO still matches for recovery

* fix(ssh): confirm the persisted port is free before the stdio bridge binds it

* fix(ssh): record a forwarding refusal against the transport generation that answered it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 19:00:54 -07:00
NeilandEvgenii 5517163679 fix: nest settings translation keys for language packs (#28251)
Reshape dotted translation keys into nested objects while retaining identical translation paths and values in all six locales. Keep the existing language-pack parser and its safety restrictions.

Co-authored-by: Evgenii <kumiro@me.com>
2026-10-13 18:56:10 -07:00
Neil f7235f6e2e feat(wsl-mover): accept local WSL projects on the migration wire, only in their own distro (D1b-2) (#28466)
* wip(wsl-mover): local-wsl manifest source on the wire (D1b-2)

* wip(wsl-mover): narrow SSH-only steps, cover WSL source parsing

* feat(wsl-mover): WSL host orcad accepts its own distro's projects and advertises it (D1b-2)

* fix(wsl-mover): bind a WSL source to its distro registration

* fix(wsl-mover): keep the manifest parser under the line limit
2026-10-13 18:44:38 -07:00
Neil a0f3e78815 fix(terminal): keep verified file links when another candidate rejects (#28272)
Settle terminal file-link candidates independently so one rejected path guess cannot discard verified neighboring paths. Preserve host authorization, cache scope, wrapped ranges and stale-buffer filtering.
2026-10-13 18:33:00 -07:00
NeilandMatheus Breguêz bda694181f fix(speech): release dictation window listeners after stop (#28260)
Release dictation window callbacks by their owner and identity when sessions finish or are replaced. Clean superseded, rejected and coalesced starts without removing a newer session callback or changing audio handling.

Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
2026-10-13 18:32:56 -07:00
Neil 4f5ffcb429 feat(wsl-mover): decide every piece of project state and add a losslessness check (D1b-1) (#28459)
* feat(wsl-mover): decide every piece of project state and add a losslessness projection (D1b-1)

* fix(wsl-mover): replace casts with checked reads

* fix(wsl-mover): tighten the projection after review

* fix(wsl-mover): second review round on the projection

* refactor(wsl-mover): keep the disposition tables in shared, out of the layout-writer scan
2026-10-13 18:32:33 -07:00
Neilandoliver-mee 7a5495493b fix: distinguish unreadable cookies from related import skips (#28269)
Count unreadable cookies separately from related cookies withheld by existing import safeguards. Keep import and clearing decisions unchanged and retain the legacy warning for older or invalid summaries.

Co-authored-by: oliver-mee <102673257+oliver-mee@users.noreply.github.com>
2026-10-13 18:25:06 -07:00
NeilandKalvin Mizzi 624a095993 fix(cli): warn when a workspace status has no board column (#28264)
Return optional warnings from the owning host when a requested workspace status does not match its configured columns. Preserve the requested write and older-peer behavior; show warnings separately from normal CLI output.

Co-authored-by: Kalvin Mizzi <kalvin@mizzi.com>
2026-10-13 18:21:09 -07:00
Neilandoutof-place 7b31e739d2 Fix Vercel recipe failures on stock macOS Bash (#28257)
Guard empty argument-array expansion so the bundled setup recipe runs under Bash 3.2 with unset-variable checks. Preserve exact arguments and cleanup exit behavior; normalize only test fixture line endings.

Co-authored-by: outof-place <contact@outofplace.space>
2026-10-13 18:18:50 -07:00
Neil 3f81541188 Support restarting Windows Orca as administrator (#28073)
* feat(windows): add administrator PowerShell terminal setting

* fix(settings): localize administrator terminal search keywords

* fix(windows): restrict elevated terminal host to Orca's runtime and requesting account

* fix(windows): harden administrator terminal routing, inventory and restore

* fix(build): build the Windows agent hook when electron-builder packages Windows

* fix(build): build the packaged Windows agent hook for the target architecture

* fix(windows): accept the development terminal runtime and test real elevation in CI

* feat(windows): show administrator status and restart instructions

* fix(settings): localize administrator mode search keywords

* test(settings): isolate the desktop permission section in shell picker tests

* Add Windows administrator restart and simplify Terminal settings

* Keep administrator restart silent and settings actions within narrow panes

* Keep Windows shell choices and permission guides usable at minimum width

* Prevent later restarts from replaying administrator terminal reset

* Fix CI route resolution and transient Windows fixture cleanup
2026-10-13 18:06:05 -07:00
Neil 7b915d587e fix(orcad): keep Codex status hooks in a managed account home on a WSL host, and list its sessions (#28469)
* fix(orcad): put Codex status hooks into a client-sent account home and list its sessions

* fix(orcad): keep account-home-only hooks, check wire paths lexically, find account transcripts by id

* fix(orcad): keep Orca's Codex hook in an account home in place, as in ~/.codex

* fix(native-chat): an unlistable account home never blocks a transcript lookup

* test(codex): type the account-home hook test's hashes without a cast
2026-10-13 17:53:41 -07:00
Brennan Benson 9c113ed857 fix(native-chat): draw slash-menu command names at the chat's code size (#28461)
The command name was the only monospace text in the chat drawn at full
message size, so /electron looked larger than the surrounding text.
2026-10-13 17:51:25 -07:00
NeilandJoe Esposito ba07bd71b7 fix(terminal): retain CRLF split across output chunks (#28224)
fix(terminal): retain CRLF split across output chunks

Carry a final CR until the next chunk determines whether it is a line
ending or a redraw. Preserve normalized line content, cursor counts,
existing standalone redraw behavior, and bounded terminal tails.

Fixes #27245

Co-authored-by: Joe Esposito <joeespo@gmail.com>
2026-10-13 17:26:45 -07:00
Neil f4da672447 refactor(wsl): checked command-channel construction and the shared keyed queue (#28448)
- The WSL command channel and command loop keep the typed result of Object.assign instead of
  casting the bare Duplex through unknown to RemoteCommandChannel.
- The distro holder serializes per distro with runKeyedSerializedOperation instead of its own
  copy of the same never-rejecting-tail queue.
2026-10-13 17:25:36 -07:00
Neil 0a12d52c7c fix(wsl-host): orcad's own git sees the login shell's SSH agent, proxy and signing env (#28456)
* fix(wsl-host): orcad's own git sees the login shell's SSH agent, proxy and signing env

A WSL host's orcad starts under wsl.exe --exec sh -c, so no rc file runs and git, gh and glab
calls orcad makes miss what the user's shell exports. Adopt an allowlist of those variables from
the login shell before the terminal daemon starts. The real-distro lane now proves orcad's git
sees an rc export, and a terminal reaches Windows interop, long after the launching wsl.exe exited.

* fix(wsl-host): adopt login env before hook reconcile; honor the shell probe marker; cover git config and glab variables

* fix(wsl-host): give the login-env probe the PATH probe's 10 s budget
2026-10-13 17:25:10 -07:00
Brennan Benson ac03226b1e fix(agent-status): preserve open chat status when terminals close (#28188) 2026-10-13 17:25:03 -07:00
Jinwoo Hong 23bd828af1 fix(usage): usage requests answer authenticated-proxy challenges on a cold start (#28411)
* fix(usage): usage requests answer authenticated-proxy challenges on a cold start

Claude, Cursor, Gemini, Grok, Kimi, OpenCode Go (API key) and MiniMax (API key)
usage fetchers called Electron's net.fetch directly, which cancels every proxy
login challenge. They now go through the shared desktop HTTP client, which
answers the challenge with the configured proxy credentials (#26957), like
Codex and ZCode already do.

* test(cursor): route the wire fixture through the desktop HTTP client
2026-10-13 20:25:00 -04:00
Brennan Benson 36e1291aef perf(session-history): end a scan only when it stops making progress (#28434)
* perf(session-history): end a scan only when it stops making progress

A cold Session History scan of a large history was killed 130 seconds in
every time, however healthy it was, and every Refresh died at the same place.

The scanner child now reports progress for a running scan, at most once a
second and only when real work advanced: a directory listed, a batch of
files statted, a transcript chunk read, a candidate file parsed. Progress is
keyed to the scan's own cancel signal, so the search index or an
interactive read in the same child never extends it. The parent re-arms the
130 second window only for an active, uncancelled scan whose counter grew.

When a request deadline does fire, the child gets the shutdown frame and the
existing two-second grace to abort and save its parse cache before the kill.
Exit, disconnect, malformed-message and cancel-timeout faults still kill at
once. Startup, cancel and interactive bounds are unchanged.

* fix(session-history): keep the deadline module off child_process and let the orcad smoke skip progress frames
2026-10-13 17:17:56 -07:00
NeilandJoe Esposito 3308eb3d00 fix(process-host): accept string stdin in synchronous runs (#28225)
fix(process-host): accept string stdin in synchronous runs

Request raw subprocess output with null encoding so Node can encode string stdin as UTF-8. Preserve explicit text decoding and byte capture, with real-runner coverage for both output modes.

Co-authored-by: Joe Esposito <joeespo@gmail.com>
2026-10-13 17:16:03 -07:00
Neil 16efc33a91 Fix linked reviews on detached workspaces (#28228)
Fix linked reviews on detached workspaces
2026-10-13 17:15:21 -07:00
NeilandSamuel Vila 565e34081a Preserve significant spaces in Explorer contents search (#28222)
Preserve significant spaces in Explorer contents search

Co-authored-by: Samuel Vila <svila.smr@gmail.com>
2026-10-13 17:15:16 -07:00
Neil 52432e9952 refactor(wsl): register WSL host IPC only when the WSL host service exists (#28446)
* refactor(wsl): register WSL host IPC only when the WSL host service exists

With the internal gate closed (or off Windows) the preload exposes no WSL host API, so the
seven handlers and the settings listener were registered only to refuse. Register nothing
in that case and drop the per-handler gate checks that a live service makes unreachable.

* test(wsl): cover the settings listener, re-registration and non-Windows gate-open cases
2026-10-13 17:15:05 -07:00
NeilandEddie 03bf6b49d4 fix: refresh status bar and tray labels when language changes (#28226)
fix: refresh status bar and tray labels when language changes

Subscribe the memoized status bar to language changes and rebuild the existing tray menu after language or language-pack updates. Keep tray actions and attention state, and defer native menu updates off AppKit callbacks.

Co-authored-by: Eddie <tw199501@elf.tw>
2026-10-13 17:14:35 -07:00
Neilandoliver-mee 6920c6a002 fix(browser): reject ambiguous unscoped keypress targets (#28221)
fix(browser): reject ambiguous unscoped keypress targets

Co-authored-by: oliver-mee <102673257+oliver-mee@users.noreply.github.com>
2026-10-13 17:14:32 -07:00
NeilandMatheus Breguêz 0d1e367d05 fix(automations): format the jobs-file limit consistently (#28220)
fix(automations): format the jobs-file limit consistently

Use English number formatting in the English jobs-file limit error so non-English host locales still report 10,000 jobs.

Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
2026-10-13 17:14:05 -07:00
Brennan Benson d7f7474c5e Give Session History's rows one decision for open and resume (#28360)
A session row decided "can ▶ resume this" twice: once for the button's
disabled state and again inside its click handler, and the chat/CLI and
switch-to-pane offers were each gated inline. A second caller of those
offers would have had to copy the same conditions again.

- One projection, resolveAiVaultSessionRowChatActions, now decides every
  open/resume offer a row makes; ▶'s greyed-out state and its target come
  from the same value.
- The panel's host-scope menu and per-session resume lookups move into
  their own hooks unchanged, and the session-list request reads the
  panel's workspace through one selector.

The sidebar behaves exactly as before.
2026-10-13 17:06:06 -07:00
ed18c2c580 fix(ssh): Cancel on an SSH password prompt no longer brings it straight back (#28402)
* fix(ssh): a cancelled sign-in stays cancelled until the user reconnects

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

* test(ssh): drive the credential cancel through the IPC harness

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

* fix(ssh): an explicit server connect lifts the cancelled sign-in of its SSH host

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

* fix(ssh): the cancelled attempt may still offer a password; a successful sign-in lifts the decline

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

* test(ssh): build the managed-server link without the newer kind field

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 16:52:37 -07:00
Kelvin AmoabaandBrennan Benson 885d8a0046 feat(native-chat): show context usage in Codex chats (#28084)
* feat(native-chat): show context usage in Codex chats

Codex turn-row writes become journal revisions so the reading survives a turn end.

* test(codex): run the context-usage journal suite under Node

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-13 16:47:07 -07:00
Jinwoo HongandMax Hsu 8b0fa15e94 fix(notifications): remove read macOS notifications from Notification Center by pane thread (#28425)
* fix(notifications): remove read macOS notifications from Notification Center by pane thread

Orca could only take a delivered notification out of Notification Center
while main still held its Notification object, which it releases after five
minutes and loses on restart. Older notifications for a pane then stayed
forever.

On macOS each pane notification now goes in a thread named after its pane,
and reading the pane removes that thread. A native-chat alert has its own
identifier instead, and is removed only by a read that covers it. Windows
and Linux are unchanged.

Fixes #26732

Co-authored-by: Max Hsu <3001335+maxmilian@users.noreply.github.com>

* refactor(notifications): remove by pane thread inside the dismiss loop; only structured alerts by id

Only alerts posted with an identifier are removed by id, so renderer-rebuilt
ids no longer reach Notification.remove. Drops the extra pane-key accumulator,
the try/catch wrapper and redundant tests.

* docs(notifications): place the macOS removal rationale on the removal function

---------

Co-authored-by: Max Hsu <3001335+maxmilian@users.noreply.github.com>
2026-10-13 19:29:04 -04:00
Jinwoo Hong 099afc8962 fix(usage): show "Network issue" for any provider whose usage request fails on the network (STA-9799) (#28415)
A Codex usage read that failed on the network ("failed to fetch codex rate
limits: error sending request ...") was labelled "Limited" because only
Claude mapped failureKind 'network' and other providers fell through to a
rate-limit regex on the error text.

- The status bar shows "Network issue" for failureKind 'network' on every
  provider; a structured kind beats the message regex, which now only
  labels errors with no kind.
- Codex classifies app-server transport errors at the probe boundary;
  Claude, Gemini, Grok, Kimi and OpenCode Go tag requests that throw before a
  response through one shared classifier.
- A kept stale snapshot carries only the latest error's failure kind.
2026-10-13 19:10:57 -04:00
Brennan Benson 4d65146f2d fix(native-chat): Stop is saved before it acts, and a late or repeated Stop does nothing (#27076)
* feat(native-chat): add a chat-owned command receipt store

Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.

* fix(native-chat): key command receipts like today's ledger ids

Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.

* fix(native-chat): order receipt lookups by insertion and key by operation id

Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.

* fix(native-chat): accept sends through their own receipt

Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.

The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.

* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once

- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.

* test(native-chat): expect receipt answers where tests assumed the send ledger

Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.

* test: run the queued tests that open the journal database in the Node runtime project

* fix(native-chat): queued-message actions and prompt answers accept through their own receipt

Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.

* fix(native-chat): replay a queued action from what its receipt recorded

Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.

* docs(native-chat): receipt comments say what a replay answers from

* fix(native-chat): Stop saves first, captures its target, and holds queued messages

Stop accepts through its own receipt: one transaction saves the Stop record
(target turn, agent generation, cutoff), returns unsent messages to the queue as
paused cards, and writes the receipt; only then does it interrupt the captured
target. A failed save leaves the agent running and says so. A late Stop is a
recorded no-op, and a retry is answered from the receipt.

* fix(native-chat): Stop round 1: late Stop is a no-op, saved-then-failed refuses, hold only what the Stop did not stop

- A Stop naming a turn that already ended is an accepted no-op.
- A card's Cancel saves its receipt with the dismissal row.
- A Stop saved but not carried out is refused as stopFailed ("Couldn't stop <agent>. Try again.") and its receipt is rewritten as rejected.
- A Stop during a start is saved first, then aborts that start; the abort-before-admission path is removed.
- The message the Stop stops is withdrawn as before; messages behind it are held as paused cards. Non-text messages are still withdrawn (TEMPORARY).
- Cleanups: one receipt commit path, dead fields and helpers removed, the returned-to-queue text names the agent.

* fix(native-chat): Stop round 2: narrow to receipts and save-first; message fate as on main

- Stop's withdrawal of queued sends is main's own rows (rejected, cancelled), written in the same
  transaction as the Stop event and its receipt; a failed save withdraws nothing.
- A Stop arriving during a start aborts that wait on arrival, as on main (admits-now guard), then
  captures its target once in the session lane. The out-of-lane admission is removed.
- A committed receipt is never rewritten: a failure after the save answers from the receipt and
  writes the existing 'Cancellation was not confirmed.' note. 'Couldn't stop <agent>' only when
  the save fails; a card Cancel that can't be saved says the question or approval wasn't cancelled.
- A Stop naming a turn a rewind removed is late (an accepted no-op).
- Removes the held-card settlement and returnedToQueue: what happens to a stopped message stays as
  on main.

* test(native-chat): name the Stop row comparison for what it drops

* fix(native-chat): Stop round 3: a committed Stop always acts; notes only for unconfirmed interrupts

- A Stop whose write throws after its receipt committed still interrupts (never 'Couldn't stop').
- An interrupt that worked answers stopped even when only its note fails to write.
- A strict card interrupt that throws after its receipt committed gets the 'Cancellation was not
  confirmed.' note, through one shared helper with the turn path.
- A target-less Stop stands while the main agent works, so a turn that opened while its save waited
  (a send made before the Stop) is interrupted, as on main.
- A card Cancel that can't be saved says 'Couldn't cancel this question or approval. Try again.'

* fix(native-chat): Stop round 4: name the turn the Stop actually stops

- The Stop event's turn is resolved when its write runs (the named turn, else the live turn), so a
  turn that opened while the save waited is the one marked interrupted and shown stopping.
- The unconfirmed note is keyed by that turn; tests cover it and its own write failing.
- Whether a Stop withdrew anything comes from its capture, so a throw after the commit can't flip it.
- A Stop counts as saved anyway only when its own write committed the receipt.

* fix(native-chat): a new chat's first send gets the Stop-aware operation receipt

Main's create path (from #26332) hands performSend the founding receipt; the
Stop changes widened the turn context's receipt to track its commit. Wrap it
with the existing observedReceipt.

* fix(native-chat): a background task the agent won't stop still says not done

A background-task Stop now saves its receipt before it asks the agent. A
refusal after that save (task still running, or a timeout / lost connection)
was being replaced by the receipt's replay, so the person never heard the Stop
failed. That refusal is now the first answer, as on main; the receipt stays
saved and a retry of the same id is answered from it without asking again.

* test(acp): a Grok Stop naming an ended turn leaves the unechoed follow-up alone

The late-Stop rule makes a Stop naming a turn that has ended an accepted no-op
for every agent. The Claude case was rewritten with it; this is Grok's.

* test(native-chat): Stop tests exercise what they name after the late-Stop rule

- The "leaves the child alone" Stop now reaches the provider (handed over, a
  turn with no row yet); the at-rest refusal case was a duplicate of the late
  no-op test and is removed.
- The adapter comment and two test names say the gap rule covers a named turn
  with no journal row yet.
- The Grok late-Stop test checks no status row and that the follow-up completes.
- Name the two refusals refusalAnswers passes through.

* fix(native-chat): a background-task Stop answers its acceptance and notes what happened

A background-task Stop is saved before it asks the agent. The answer is
that saved acceptance: a stopped task answers cancelled:true as before
(the phone keeps the row "stopping" on it), and anything else answers
cancelled:false. A retry answers from the receipt without asking again.

What the agent then says comes back as a note keyed by the Stop's own id,
written through the existing Stop-note path, and never as a refusal:
- still running: "The background task wasn't stopped." (or "The
  background tasks weren't stopped." for Stop all), the new status-row
  failure kind backgroundTaskNotStopped;
- timed out or lost the agent: "Cancellation was not confirmed."
The note is the thread's, since tasks outlive their turn. A note that
cannot be written is logged and changes no answer.

The background-task body moves out of performCancel into the targeted
cancel, and the refusalAnswers flag and its receipt are deleted.

* test(runtime): the Codex strip's Stop notes a command it could not stop

A background-task Stop now answers its saved acceptance and says what happened in a note: "The background task wasn't stopped." when the command still runs, "Cancellation was not confirmed." when Codex was lost.

* test(native-chat): keep the Stop tests that pin behaviour, cut the rest

Merges the receipt, save-first, failed-after-save and background-task Stop
tests into one flow file; drops unit and per-branch tests already covered
by a flow test.
2026-10-13 15:30:55 -07:00
Brennan Benson b70f13ff89 fix(orchestration): stop retried failed worker starts from piling up live agents (#27795)
* fix(orchestration): stop retried failed worker starts from piling up live agents

A failed worker-start keeps the terminal it created (a pane the user typed
into is theirs), so a coordinator retrying a start that can never succeed
left one more live agent behind on every retry, with nothing to stop it.

- A --retry-of start now releases the failed attempt's still-owned terminal
  through the normal worker-release path (output archived, user takeover
  kept, recovery finishes it after a restart).
- A failed start now counts toward the Task's existing three-failure circuit
  breaker, carried across retries; a retry of a circuit-broken Dispatch is
  refused with a plain error.

Linear: STA-7113

* test(orchestration): type the failed-start retry fixtures without casts

* fix(orchestration): carry failed-start count only along --retry-of; plain breaker refusal

- Only a --retry-of start inherits its predecessor's failure count, so worker
  exits across separate starts keep the old behaviour.
- The circuit-broken refusal carries failureCount and next steps that say to
  stop retrying, read the last attempt, and that its pane stays open.
- A repeated failed report on a circuit-broken Dispatch is a duplicate.

Linear: STA-7113

* fix(orchestration): breaker refusal only points at a terminal that is still open

Linear: STA-7113

* fix(orchestration): log a retry's release that could not prove the old terminal closed

Linear: STA-7113

* Preserve worker diagnostics before terminal release and mask wrapped tokens
2026-10-13 15:20:19 -07:00
Brennan Benson 26a265d704 Host plans agent launches with the window's runtime rule and one runtime fact (C1) (#27692)
* feat(agent-launch): host plans agent launches with the window's runtime rule and one runtime fact

Host-started agent launches now pick their platform with the same rule the
window uses (project runtime, else a \\wsl$ path, else the host), and resolve
one runtime fact that supplies both the command quoting and the PTY shell.
Adds the agent.launch.terminal-inputs.v1 capability and its optional
title/extraAgentArgs/requestKind/terminalOnly fields; no sender yet.
OpenCode's createAgentSession stays on today's rules until C2 (temporary).

* fix(agent-launch): one shared runtime rule, a derived fact, and only quoted commands carry it

Round-1 review fixes for C1:
- Window and host read a workspace's runtime through one shared resolver
  (local-workspace-execution-runtime.ts); a repo-less \\wsl$ folder gets no
  runtime, as in the window.
- The fact is { platform, remote, pty } with the spawn's own PTY type; the
  quoting shell is derived from it, never stored.
- createAgentSession uses the same fact (its pre-C1 exception is deleted).
- Worktree startups quote from a fact for the repo and pass it on; a runtime
  that needs repair fails the startup, not the create. `shell` is required
  when building a launch command.
- The host planner stamps a fact only on commands quoted with it; a
  caller-quoted command keeps the spawn's own rule.
- Qoder probes where the process runs (\\wsl$ cwd, then a WSL shell).
- wslShellRule 'host-legacy' keeps today's WSL-shell outcome where no
  project chose a runtime (temporary, removed by C8).
- terminalOnly reaches the early-tab decision with an accurate receipt
  sentence; the unused client gate is removed.
- Adds one host C0 row for a \\wsl$ repo with no saved runtime.

* fix(agent-launch): round 2: keep request fields for C2, extend main's resolver, one answer per workspace

Problem: the first round still carried request fields and window-parity
switches that only the next step sends, added a new shared resolver that
also changed window code, and left some background launches using a
different runtime answer than the one they spawn with.

- Move the agent.launch request fields, their capability and fingerprint
  handling, and the prompt/Codex parity switches out to the next step;
  today's host behaviour is kept.
- Drop the shared resolver: main's resolveLocalWorkspaceRuntime now finds
  the project by repo id or source repos, folders borrow their single
  local repo, and a \\wsl$ path is not refused on a stale distro list.
  Window code is back to main.
- Agents started in an existing worktree use that worktree's answer; the
  phone's fallback paths spawn with the answer they quoted for, and a
  runtime that needs repair shows its message instead of a plain shell.
- The OpenCode model check looks where the agent will run.
- A Windows --shell in a \\wsl$ workspace names the path, not a runtime
  setting that cannot change it.
- The temporary folder rule names its follow-up, STA-10231.

* fix(agent-launch): OpenCode model picks in WSL folders keep today's Windows runtime

Problem: once a folder follows its repo's WSL runtime, an OpenCode launch
that picks a model would be checked and started inside WSL, where Orca
cannot yet verify OpenCode models, so picks that work today could be
refused.

Where main had no runtime for the scope (only folders), a model-picked
OpenCode launch keeps main's answer. Start, resume and the worker's model
check read the same answer. Temporary until OpenCode models are verified
inside WSL.

* fix(agent-launch): round 3: OpenCode keyed on the agent, new worktrees quoted for where they land

Problem: round 2's OpenCode exception keyed on a model pick, so a resumed
chat could run somewhere other than where it started, and the worker's
model check read WSL from the Terminal shell and refused folders that work
today. A new worktree's startup used the repo's answer even when the
worktree lands in a WSL share.

- In a folder whose repo runs in WSL, every OpenCode launch keeps main's
  Windows answer: create, resume and the worker's model check agree. The
  model check reads WSL from the path alone, as main did. STA-10251.
- A new worktree's startup is planned for its WSL-share base folder when
  one is set (newWorktreeLaunchScope).
- A \\wsl$ working directory runs in WSL whatever the project says, so
  non-OpenCode agents there are written for Linux.
- The planner test derives the answer instead of passing it in.
- Smaller shapes for the PTY settings type, the cache-less resolve,
  executionRuntimePlatform, spawnShellInputs and two comments.

* fix(agent-launch): round 3c: one labelled rule for where the path decides; OpenCode follows the WSL-share fix

Problem: OpenCode in a repo on a WSL share whose project is saved as
Windows host still got PowerShell quoting typed into WSL, because the
WSL-share fix skipped OpenCode. The fix and the two temporary parity
rules were also split across two returns, so deleting the temporary
ones could take the permanent fix with them.

- One expression decides when the path places the agent, each term
  labelled: WSL share + Windows host (permanent fix), folder + Windows
  host (STA-10231), OpenCode in a folder (STA-10251).
- OpenCode now gets the WSL-share fix too; a model pick there is refused,
  as main's model check already did.
- The OpenCode entry-point tests set a WSL Terminal shell, so the model
  check test fails if it reads WSL from that shell again.
- Tests trimmed with no coverage loss; the spawn-precedence unit test is
  dropped (the C0 WSL-share row covers the reachable case).

* fix(agent-launch): keep the window's WSL answer for panes the window spawns

Live Windows QA (Terminal shell wsl.exe, global default Windows host, repo
with no project setting) saw an agent command written with PowerShell
quoting typed into a WSL pane, so codex lost the apostrophes in its prompt.
The host wrote the command for its own answer (Windows, PowerShell), but on
routes where the window spawns the pane (the phone while a window is open,
a focused desktop chat create, `orca terminal create --command` in a repo)
the window spawned it by the Terminal shell, which is WSL. Quoting and the
pane came from two different answers.

Those routes ran in WSL with Linux-style quoting on main and worked, so the
temporary host-legacy term (STA-10231) now also covers them: when the window
spawns the pane, the path and Terminal shell decide, as on main. Background
routes keep the approved change (PowerShell instead of cmd.exe with
Linux-style quoting, which was broken on main). createTerminal and the
pre-launch planning share one `windowSpawnsTerminal` rule so they can't
disagree about who spawns the pane.

* Revert "fix(agent-launch): keep the window's WSL answer for panes the window spawns"

This reverts commit 3e4311db19.

* fix(agent-launch): report a runtime that needs repair as a refusal, not an unknown outcome

`orca agent launch` into a folder whose one repo is set to a WSL distro that
is no longer installed printed "Orca could not confirm the launch outcome;
the agent may already be running", although nothing had started. The host
knew the launch was refused before any spawn, but the refusal went out as
the generic `runtime_error`, which the CLI must treat as possibly started.

The launch planner now raises that refusal under its own code,
`project_runtime_repair_required`, with the same message, and the RPC layer
passes the code through. The CLI then prints the repair message as a plain
failure. Older CLIs read the unrecognised code as a failure too; a newer CLI
against an older host still gets `runtime_error`, as before.

* fix(agent-launch): say a repair refusal in plain words, and repeat them on a retry

The repair refusal from the last commit read "Project runtime requires repair
before terminal spawn: wsl-distro-missing", an internal reason code rather
than what happened and what to do. It now says, for example, "No agent was
started. Ubuntu is not installed in WSL. Choose an installed distro or switch
this project to Windows.", adding "This project follows the default in
Settings > Project Runtime." when the runtime came from the default.

Those sentences are the project settings page's own. They now live in one
shared module that maps each repair reason to the page's existing
translation key; the settings page passes its translator, and the host writes
English, since it also runs headless where the desktop's translations can't
load. The planner checks for a repair before the shell lookup, so the shared
spawn message, which window spawns still use, is untouched.

A retried launch (`--retry-launch`, or a phone resend) used to answer with
the bare error code. The failed launch record now keeps the refusal's words
and the retry repeats them.

* fix(agent-launch): test that a retry repeats a refusal's words, and keep the host's English equal to the window's

A retried launch now repeats a coded refusal's own words, but nothing tested
it. A new case refuses a launch with a coded error and checks the retry
answers with the same words.

The repair sentences live in one shared module whose English the host
writes and whose keys the settings page translates. The localization checks
can't see call sites outside the window and main, so the module's test now
checks each sentence against the window's English catalog for its key.

Smaller, behaviour-identical tidy-ups: the reason-to-words mapping is a
table, the refusal type fits on one line, and the length cap the retry
words share with folder refusals has a neutral name.

* refactor(agent-launch): keep main's behaviour where the stacked fixes change it

The repair refusal's own code and words, the \\wsl$ path's distro in the agent list,
--shell refusals for agents (decision and words), OpenCode's model check in a \\wsl$ repo,
and finding a repo's project by its id move to their own stacked PRs. Until they land,
each keeps main's behaviour with temporary code and a test that pins it.

A disabled agent is refused as disabled before its runtime is looked at again, as on main.

* fix(agent-launch): keep main's folder agent list when only the group names another host

The agent list for a folder reads the folder's own host pin, as on main; only launches also
read the containing group's host tag.

* ci: re-run checks now that main's capacity-capture replay fix (#28286) has landed

* test(agent-launch): give main's account-env startup test the launch runtime its build now returns
2026-10-13 15:00:32 -07:00
Neil 8a6d6ea8c8 fix(terminal): send only the text a Linux IME commits, not the candidate key (#28275)
* fix(terminal): send only the text a Linux IME commits, not the candidate key

fcitx4 with Sogou reports a single-character candidate pick as a plain
Space/digit keydown whose keypress and insertText carry the candidate.
xterm encoded the keydown instead: under kitty keyboard mode the Space
leaked and the candidate was lost, and the old Linux digit window
cancelled the keydown that carried the commit.

Install the native text forwarder on desktop Linux (terminal pane and
dashboard preview) so printable bytes come only from committed text, and
delete the Linux timer heuristics it replaces. Pair an XIM pick's keyup
with its code-less press, and stop dropping unclaimed input while an
xterm composition transaction settles.

Replay tests use real fcitx4 recordings (Sogou via GTK module and XIM,
Google Pinyin in-client preedit).

Fixes #28213
Fixes #26352

* test(terminal): replay Chromium's text insertion for uncancelled keys in the Hangul sequence

The observed IBus Hangul replay dispatched a bare `a` keydown with no text insertion,
which only matched reality while xterm cancelled that keydown. Now that printable
Linux text comes from the committed insertText, supply it when the keydown is not
cancelled and open the next session at the caret that follows it.

Point the IME reliability gate at the fcitx4 replay test that replaced the deleted
Linux candidate-state test.
2026-10-13 14:47:05 -07:00
Brennan Benson 8a0f1ee4b6 Resume in New Native Chat for Grok and OMP terminal conversations (#28049)
* feat(chat): Resume in New Native Chat for Grok and OMP terminal conversations

A Grok or OMP conversation started in the terminal now offers "Resume in New Native Chat" in its
Session History row menu and its tab's right-click menu, like Claude and Codex: a new chat of that
agent takes the conversation over, showing its history and continuing it. The host finds the
conversation through the agent's transcript import and fills the chat from it, the same adoption
path Claude and Codex use; the adopted handle is now any agent's, not only theirs. Both agents keep
conversations per folder, so the action shows only in the workspace the conversation was recorded
in, as for Claude.

* fix(chat): hide Grok 1.0's runtime context and rules in a chat that takes over its conversation

Grok 1.0 appends the user's rules to its runtime-context row; the decoder only knew the older shape,
so a Grok chat taking over a terminal conversation showed that row as the first message.

* Fix OMP and Grok transcript lookup for chat adoption

* Fix scoped Grok and OMP chat history lookup
2026-10-13 14:41:46 -07:00
Kelvin AmoabaandWooseong Kim c473aedd14 feat(browser): collapse the annotation panel to one row (#28138)
* refactor(browser): remove the annotation tray's unused open flag

Nothing ever set it to false.

* feat(browser): collapse the annotation tray to its header

Fixes #20969

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>

---------

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-13 14:38:57 -07:00
Brennan Benson a36b2ac3f8 perf(native-chat): read the composer's text and caret straight from the editor (#28064)
* perf(native-chat): read the composer's text and caret straight from the editor

Every composer read (text, caret, selection) rebuilt a per-character text-to-position
map of the whole draft: 8 times per typed character and 3 per caret key. Reads now ask
the editor directly (ProseMirror textBetween) and convert a text offset to an editor
position with one node-level walk only when a caller sets the selection or edits.

STA-7911

* test(native-chat): check a pill edit through the value setter keeps the exact text

* fix(native-chat): preserve newline before selected paragraph
2026-10-13 14:33:38 -07:00
Brennan Benson 93e459fbb4 perf(session-history): stop a cancelled scan inside the file it is reading (#27952)
A Session History scan only checked for cancellation between batches of
files. A cancel that landed while it was reading a huge transcript let the
read run to the end, so the scanner process missed its two-second
cancel acknowledgement and was killed, losing its in-memory parse cache.

Pass the scan's existing abort signal down to every expensive read: the
resumable transcript reader and its JSONL loop (checked per chunk, also
while skipping an over-budget record), the resume-point newline probe,
Cursor sidecar enrichment, generic file discovery and its stat batches,
and the search index pass. A cancelled read stores no cache entry and
keeps the earlier resume point.

A cancel is not a failure: the read outcome now says it was cancelled,
so search leaves the file's row as it was instead of counting it toward
the three-strikes hold-out, discovery records no issue for stats the
cancel cut short, a cancelled sidecar read is not cached as "no
metadata", and the scan drops a cut-short batch before recording issues.
2026-10-13 14:30:58 -07:00
OrcaWinandm4air 335cc32c85 fix(ssh): an SSH host comes back after restarting Orca instead of staying offline (STA-10476) (#28398)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 14:10:42 -07:00