Commit Graph
16002 Commits
Author SHA1 Message Date
Neil 49450b7144 fix(projects): stop offering a project on a paired server's SSH host, and say where to add it (#29919)
Since #27609 a server serves no SSH target and refuses repo.addRemote, yet the add-project dialog
still offered "Project on this server's SSH host" and the refusal read "SSH connection not found
or not connected". The row is now disabled with a message pointing to adding the SSH host on this
computer, and the server's refusal says the same for older clients that still offer it.
2026-10-16 19:33:47 -07:00
Neil 03be5bb9b8 fix(ssh): a Disconnect or Remove during a background connect sticks (#29918)
A background connect (managed tunnel, census, runtime context) that was
waiting for the old transport to close resumed after the user's
Disconnect or Remove host and registered a fresh connection. Re-check the
attempt token after that await so the cancelled attempt never registers.
2026-10-16 19:32:51 -07:00
Jinwoo Hong 776c791554 feat(orcad): ship the mobile web page so phones paired to orcad get it (#29882)
* feat(orcad): ship the mobile web page so phones paired to orcad get it

orcad never carried out/mobile-web, so it did not advertise
mobileWeb.bundle.v1 and phones paired directly to an orcad server fell
back to the app's built-in native screens.

The built page now ships in every orcad package at out/mobile-web, the
path getBundledMobileWebBundleRoot already probes under the install
root. Its manifest is a fixed artifact; its content-addressed assets
join the hashed artifact list from that manifest, as one shared copy in
the template's common files. build-orcad fails when the page is missing
or stale, and CI lanes that build orcad now build the page first.

* ci(orcad): check out mobile/ for the Windows host template build

* refactor(orcad): copy the page tree whole and build it once per CI run

The guard already refuses unlisted files, so staging copies the
bundle directory as native-chat-visuals is copied. The shipped template
takes the page the qualification lanes tested, the e2e build artifact
carries it, and the PR smoke builds it through install-mobile-dependencies
behind its own scope step. Drops tests that duplicated the guard's own.

* fix(orcad): refuse a template page asset name that escapes the slot
2026-10-16 19:29:41 -07:00
Jinwoo Hong 0013011409 refactor(sidebar): move the pure workspace-list builder into src/shared/sidebar-list (no visible change) (#29906)
* test(sidebar): capture golden fixtures of today's sidebar row model

Golden output of the unmodified window pipeline (filter, sort, grouping,
pins, lineage, host sections, Cmd+1-9 targets) across every groupBy x sortBy
x projectOrderBy x pinned policy, plus host-filter, repo-filter, hide-sleeping
and hide-default-branch scenarios, each expanded and collapsed. Kept as the
correctness reference for moving the row builder.

* refactor(sidebar): move the pure row builder into src/shared/sidebar-list

The sidebar's list rules (visible filter, grouping, pinned rows, project
group sections, placeholders, host sections, collapse keys) move to
src/shared/sidebar-list as pure functions. Headers carry an icon key instead
of a React component; the window resolves glyph and tone when it paints, and
passes localized header labels in. The window keeps calling through thin
adapters (build-rows, compute-visible-worktrees); sort latch, settled epoch,
sleeping retention and pending creations stay where they are.

github-cache-key moves to src/shared/github because PR-status grouping needs
it; the renderer path stays as a re-export so its callers are untouched.
Other edits outside the sidebar are import paths only. The golden fixtures
from the previous commit pass unchanged.

* refactor(sidebar): resolve header labels at render, add an inbox golden

Header labels are read on every re-emit again, as before the move. Settings
and labels types move next to the other row types; header visuals no longer
export what nobody imports. Adds a golden for the new-external-worktrees inbox
row, captured on the pre-move pipeline.

* refactor(sidebar): shrink the shared builder's inputs to plain readers

- The builder takes a PR-lane reader instead of the PR cache and settings;
  getPRGroupKey stays in the window, so github-cache-key no longer moves.
- Pinned, All and PR-lane headers carry only an icon key; the window names
  and draws them at paint (status headers keep their data label).
- The status icon key carries the status id; the window looks up its look.
- Window callers use planSidebarRows directly; the positional helper is a
  test fixture.
- Folder path-status routing and rendered-row keys stay in the window.

Goldens pass byte-identical.

* refactor(sidebar): point stale builder comments at planSidebarRows
2026-10-16 19:28:58 -07:00
Neil 6bbc3cbb5b fix(gitlab): load MR reviewer choices from the paired server that owns the repo (#29917)
The runtime GitLab API answered listAssignableUsers with an empty list, so the reviewer picker
was always empty for a paired server's repo. Add an optional gitlab.listAssignableUsers RPC on the
host; a host that predates it answers method_not_found and the picker stays empty as before.
2026-10-16 19:23:49 -07:00
NeilandClaude ed147d5d68 fix(ssh): re-check a macOS host's ended launch-session warning so another desktop's restart can clear it (#29915)
* fix(ssh): re-check a macOS host's ended launch-session warning so another desktop's restart can clear it

The ended verdict was cached for the whole SSH session, so when another desktop restarted the server under a live session, this desktop kept the warning. It now expires like every other verdict: a live re-probe clears the note, and an unanswered one keeps the proven verdict.

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

* fix(ssh): only a live probe clears a proven launch-session end, and a failed recheck republishes it

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

* fix(ssh): a failed launch-session recheck restores the warning only for the same live connection

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 19:18:09 -07:00
NeilandClaude 710120991f fix(ssh): relay-era pane reattach keeps output printed after its screen, and an exited or abandoned pane never comes back (#29910)
A reattach applies the relay's screen from main's output stream instead of the invoke reply, so its clear never erases output printed after the snapshot. An endpoint refresh re-reads leases after asking its relays, so a pane that exited meanwhile is not republished live. An attach answered after its pane left hands its viewer back to main.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 19:06:20 -07:00
Brennan Benson 0e599dddfe feat(acp): the app follows a host's managed runtime install state, shared by every surface (#29048)
* feat(acp): the renderer follows a host's managed runtime install state, shared by every surface

One entry per host runtime instance and agent holds the host's answer. While any shown surface
observes it, one managedRuntime.status long-poll follows the host, idle included, so another
client's install shows up. A failed first read is its own state, never "not installed"; a reply
older than the one shown is dropped. Observed only for agents the host marks as Orca-provided, on
hosts that advertise managed-runtime.install.v1.

* fix(settings): keep one install status wait per host, retry it when the host is back, and wait for the host's agent list

- Hiding and showing a chat no longer leaves a status wait behind on the host for each cycle: the
  wait is never aborted, and a surface shown again reuses the one in flight.
- The host answering again after an outage retries a failed status read at once.
- While the host's capabilities or agent list are still loading, the install state is pending
  rather than "not provided", so a chat doesn't flash its composer; a failed list read reads as
  a failed status read, so Settings says so instead of hiding Install.

* fix(chat): only a reachable host's outstanding agent list holds the Install slot; an offline host or a list without the managed mark keeps the composer

* fix(acp): the renderer follows install status over one host subscription instead of a long-poll

Each host and agent shown on screen holds one managedRuntime.subscribe stream, ended when the last surface hides it. A lost stream resubscribes with backoff, or at once when the host is back; every subscription is a new generation, so a restarted host's snapshot replaces the old state even though its revision count restarted, and a late frame or install reply from a superseded subscription is dropped. Cancel's reply is no longer applied: the stream shows its outcome.
2026-10-16 19:02:54 -07:00
Neil 0bf5209ac8 fix(cli): infer the repo for worktree create when the parent is host-scoped by identity (#29909)
When two hosts share a worktree id, the caller's cwd resolves to an identity: selector, which
carries no repo id, so worktree create reported "Missing repo selector". Keep the selected
worktree id alongside the selector and infer the repo from it.
2026-10-16 19:02:47 -07:00
Neil bdecb92932 fix(ports): show the Ports panel for folder workspaces on a managed SSH server (#29908)
A folder workspace has no repo row, only a synthetic repoId, so the panel's repo lookup came back
empty and it said "No workspace selected", hiding the SSH forwards and the server's listeners.
2026-10-16 18:54:56 -07:00
Brennan Benson b61b9da778 fix(mobile): a streaming reply reveals word by word, like the desktop (#29148)
* fix(mobile): a streaming reply reveals word by word, like the desktop

The phone drew each streamed chunk as it landed, so a reply arrived in a few
jumps. Move the desktop's paced reveal into src/shared and use it on both
surfaces: the phone paces the growing block of the newest reply, remembers
progress per chat list so a recycled row resumes, and draws everything at once
with reduced motion. The terminal-backed chat's live row now counts as growing.

* fix(mobile): hold the reply reveals in the chat overlay

The list's turn resolver must not scan terminal-backed transcripts, and the
overlay outlives the list across chat and terminal toggles, so it owns the
reveal memory and the reduced-motion read. Also fix the moved hook's import
in the onboarding route, and mock the OS read where tests render the overlay.

* test(mobile): mock the message row's other children in the reveal test

Their real modules reach React Native's Flow sources, which Bun cannot load;
the other message row tests mock them the same way.

* style: format the desktop paced-text hook

* Keep phone reply reveal progress owned by its message

* Add braces around phone reveal test assertion

* Start phone prose streaming only at a text block

* Find the mocked phone tool row with a node predicate

* refactor(mobile): move chat prose rendering into its own file

Keeps MobileNativeChatMessage.tsx under the 400-line limit after the main merge.
2026-10-16 18:51:18 -07:00
Brennan Benson 634ca45fbb feat(acp): hosts install a managed runtime over RPC, and mark agents whose program Orca provides (#29024)
* feat(acp): hosts install a managed runtime over RPC, and mark agents whose program Orca provides

managedRuntime.status / managedRuntime.install / managedRuntime.cancel, negotiated by
managed-runtime.install.v1, act on the host that receives them, the one that runs the agent's
chats, with no workspace needed. A status read naming the revision it shows is a long-poll on its
own call-queue lane, so every client sees another's start, progress and outcome.
agentSession.agents marks an agent whose program Orca provides on that host.

* fix(runtime): install status says a copy outside Orca's runs only when a start would really run it

* fix(runtime): install status is pushed to subscribers instead of long-polled, and Cancel answers with the outcome

managedRuntime.subscribe {agent} sends a full snapshot on subscribe, then one
snapshot per install state change, and closes with the transport. It replaces
the managedRuntime.status long-poll and its slot registrations.
2026-10-16 18:47:12 -07:00
Brennan Benson 07558ba8ce fix(native-chat): a dot between the queue header's label and its count (#29268)
* fix(native-chat): a dot between the queue header's label and its count

* style(mobile): wrap the queue header dot's props

* refactor(mobile): move the queued card's text into its own file

The queue file sat at the 400-line cap, so the header dot had no room.
2026-10-16 18:39:08 -07:00
Neil fd1c6a592c fix(orcad): never claim or reclaim another machine's server, and recover a stale lock without displacing a live one (#29902)
* fix(orcad): never claim or reclaim another machine's orcad, and serialize stale-lock recovery

* fix(orcad): judge data-root locality without a synchronous spawn

* fix(orcad): break a recovery lock only once its owner provably exited

* fix(orcad): publish the recovery lock with its owner in one rename

* fix(orcad): recover a stale lock in generations so no live reclaimer is displaced

* fix(orcad): keep a released recovery generation until the stale lock is replaced

* fix(orcad): fail closed on an unreadable recovery owner, and never let cleanup fail a won lock

* fix(orcad): key recovery locks by the stale record alone
2026-10-16 18:15:32 -07:00
Brennan Benson 64e544a442 feat(mobile): Resume in New CLI from a phone chat tab (#29783)
* feat(mobile): Resume in New CLI from a phone chat tab

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

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

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

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

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

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

* Fix mobile chat CLI fork creation on owning host

* Fix mobile chat fork lint and split create owner helpers

* Keep chat fork beside phone anchor across tab groups

* Combine mobile terminal runtime type imports

* Repair mobile create CI contracts after chat fork extraction

* Classify chat fork preflight refusals and preserve mobile create recordings

* Keep route-owned create cleanup quiet after unmount

* Preserve typed runtime repair refusal in chat fork preflight

* Preserve checked store through mobile chat fork preflight

* Register mobile chat fork runtime fixture before test setup

* Remove unused mobile create recording value

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

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

* Keep quick command mutation aligned with formatted request

* Keep mobile chat CLI forks in terminal view

* Pass mobile chat resolution its existing controller arguments

* Match the mobile create fixture mock layout

* Keep mobile chat CLI forks open when placement is unavailable

* Brace placement contract fixture checks

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

* Expose pane admission in the runtime test fixture type

* Complete the chat fork contract controller fixture
2026-10-16 18:10:24 -07:00
Brennan Benson dd3bbf680e Catch up desktop chats from the newest page (#29125)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Bound desktop catch-up after independent request support

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Accept readonly client capability fixtures

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 17:46:54 -07:00
Neil a2933244e3 fix(ssh): link a moved project only to the server machine's own checkout, never a same-path project on another host (#29896) 2026-10-16 17:39:15 -07:00
Brennan Benson 99788a91fa feat(acp): a managed runtime installs only when asked, with progress and cancel (#29023)
* feat(acp): a managed runtime installs only when asked, with progress and cancel

One installation per runtime owns every change to its releases: it reclaims old ones once, first,
then installs one at a time. An install checks free space, downloads with byte progress, extracts,
hashes each member (abortable), writes the receipt, then publishes by rename, the commit point: a
cancel after it reports success. Reinstall rehashes the pinned release without downloading. A
status read waits, bounded, for a newer revision and rereads the installed release from disk.

* fix(runtime): Install repairs a damaged copy, says when unzip is missing before downloading, and reads a failed download plainly

- Install over a damaged pinned copy downloads and verifies a fresh one, then swaps the damaged
  folder into staging, so there is no dead end; status reports the damaged fact.
- A host that cannot unpack the archive (no unzip) reports it in status and refuses Install
  without downloading.
- A network failure while downloading reads "Couldn't download …"; the raw error goes to the log.
- Reclamation skips the whole pass when a release folder could not be read.

* fix(runtime): a status read that can't read the installed files fails instead of saying not installed, and extraction uses the extractor the preflight found

* fix(runtime): a host that can't report free disk space still installs, and says so in the log

* fix(runtime): a download that can't be fixed by retrying says so instead of blaming the network

* fix(runtime): Install unpacks the release itself, checks it starts before publishing, and Cancel answers with the outcome

- The managed install unzips in-process (fflate, bundled into main), so a host without unzip can install; the unzip preflight and the missingProgram status field are gone. The Node runtime download keeps its own extractor.
- A runtime may provide validate(): Install starts the downloaded copy once before publishing it, and an existing copy before reusing it; a failure reads "<agent> couldn't start on this host".
- No 'cancelling' phase: Cancel resolves once the work has unwound and returns cancelled, or succeeded when it had already committed.
- Byte progress every 250 ms; status rereads disk and PATH only on a first read or a phase change.
- The declared download size is checked only for an uncompressed body.
- An existing copy whose files hash correctly but whose receipt is missing gets its receipt rewritten instead of reading as damaged forever.

* refactor(runtime): move the steps that put one release on disk out of the installer

The installer class now holds only its lifecycle: the shared operation state, the one-at-a-time
queue, install, cancel, status, reclaim and progress publishing. Reusing a whole copy, the free
space check, the download, unpacking, checking each file, the start check, the receipt, the
publishing rename and the failure sentences move unchanged into managed-runtime-release-install.ts,
which gets the pin, folder, label, signal and two callbacks (progress, and the commit point right
after the rename) instead of the class. No behavior change; this keeps the installer well under
the file-length limit once Remove lands in it.

* chore(deps): lock fflate for the in-process runtime unzip

* fix(runtime): a failed download blames the release only for a proven mismatch, so a refused proxy reads as a network problem

* fix(runtime): runtime downloads skip the HTTP cache, so Remove frees the space and nothing is stored twice
2026-10-16 17:38:02 -07:00
Neil 696cf21d07 fix(orcad): folder workspaces can run scoped browser commands on a headless host (#29881)
* fix(orcad): resolve folder workspaces for scoped browser commands

Both orcad browser adapters resolved --worktree with the git-only selector, so
a folder workspace failed with selector_not_found before reaching the provider.

* test(orcad): parse browser results instead of asserting their types
2026-10-16 17:34:34 -07:00
NeilandClaude db0e62f4b4 fix(ssh): move the open workspace to its managed server after the cold-start scan (#29894)
On cold start the conversion skips its own worktree fetch and waits for
startup's scan, so the one rehome attempt found no server row and left
the active workspace on the retired SSH owner. Retry the move once the
startup scan completes, while the conversion is still current.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 17:18:44 -07:00
NeilandClaude 7d31ff6c15 fix(session-tabs): seed the first terminal when a warm empty worktree is selected again (#29893)
* fix(session-tabs): seed the first terminal when a warm empty worktree is selected again

A warm stream that got its only empty answer while its worktree was
unselected correctly held the bootstrap, but selecting the worktree again
reused the stream without re-deciding it, so the workspace stayed empty.
Keep the held verdict and re-decide it when the warm entry is selected.

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

* fix(session-tabs): drop a held first-terminal verdict superseded by newer host evidence

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

* fix(session-tabs): hold only single-worktree frames as deferred seeds

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 17:14:38 -07:00
Neil 0ed0caa663 fix(wsl): run wsl.exe probes from the system folder, never the profile (#29853)
* fix(wsl): run wsl.exe probes from the system root, never the profile

WSL holds wsl.exe's working directory busy for about its idle timeout
after the probe exits. Since #29209 the orcad startup probe ran in the
profile, so a temporary orca serve profile could not be deleted for about
a minute (diagnosed in #29845).

* test(orcad): drop the WSL profile-hold wait from serve parity

Reverts #29845's extended EBUSY retry: wsl.exe no longer runs in the
profile, so nothing outside the test's process tree holds it.

* build(cli): list the WSL spawn-directory module in the CLI typecheck project
2026-10-16 17:12:44 -07:00
Neil 74863c2a88 fix(runtime): hide retained SSH rows by repo id and execution host (#29873)
A visible repo on another host sharing the retained source's repo id un-hid
the retired SSH host and its worktrees in CLI and phone listings.
2026-10-16 17:08:22 -07:00
Jinwoo Hong 9aa537286d Report window saves that still change main's tab layout (dark, layout-owner build only) (#29886)
* feat(layout): report window saves that still change main's layout (dark)

In a layout-owner build, each window session save (session:set, session:patch, the shutdown
checkpoint) is projected to main's published layout before and after it is written. Any workspace
whose layout the save changed is logged, and appended to ORCA_LAYOUT_SAVE_DIVERGENCE_LOG when set,
so evidence runs can show which tab changes still reach main only through the save.

* feat(layout): name the fields a diverging save changed

* fix(layout): the save check diffs main's whole layout model and never fails the save

Review: a failed log write could reject a save that already landed; and the published layout omits
editor views, pane records (sleeping agents, incarnations, titles), closed-tab tombstones and the
workspace records, all of which a later save trim would drop.

* fix(layout): the save check covers record-only workspaces and row owners, and counts rebinding last

* fix(layout): the save check counts first binds as rebinds and keeps the count past the cut
2026-10-16 17:06:52 -07:00
Jinwoo Hong 8d2f6910ae test(orcad): start the locked-keyring start only after the first holds the migration lock (#29875)
The test raced two prepare() calls for the migration lock; when the failing-keyring start won,
it correctly started a fresh key and the old credential could not open.
2026-10-16 17:04:01 -07:00
Neil 93a5a1a20a fix(wsl-host): release a failed add's holder and cancel adds the experiment was turned off during (#29889)
A failed or deferred first add kept its detached distro holder with no record to find it; an add in flight when WSL hosts were turned off still finished as a ready host.
2026-10-16 16:59:01 -07:00
Neil 29765d4fd8 fix(ssh): don't hand a stopped pre-claim server on a shared home to whichever machine connects first (#29883)
* fix(ssh): leave a stopped pre-claim install on a shared home unclaimed until its machine is known

* fix(ssh): judge a pre-claim install by its activation record; the printed claim creates its directory
2026-10-16 16:58:43 -07:00
Neil 6f805084ea fix(orcad): keep agent ownership across orcad restarts and resume slept running agents on windowless hosts (#29885)
orcad built its runtime without the persistent claim signer, so each restart
minted a new key and the surviving terminal daemon could not match the old
owner: resuming the same conversation started a second writer. Load the same
signer the desktop uses from orcad's own user data.

A workspace-slept running agent's record was left for a window wake that a
windowless host never has, so opening its tab started a fresh agent. Such a
host now resumes it in the pane, under the existing duplicate-writer checks
and recorded-flags-only rule.
2026-10-16 16:58:23 -07:00
NeilandClaude 9846c65676 fix(terminal): replay a viewport claim made during remote recovery before typing (#29892)
* fix(terminal): replay a viewport claim made during remote recovery before typing

Recovery released typing at the host's subscribed ack, but claims and
resizes still returned early until the snapshot finished, and nothing
replayed them. Typing then reached the host at another viewer's grid.
Defer the intent and replay it on the stream before releasing input;
send claims directly once the subscription accepts input.

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

* fix(terminal): spend a deferred claim only once the host acks the subscribe

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

* fix(terminal): stop the ackless replay from marking a closed stream healthy

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

* fix(terminal): drop a queued resize when recovery defers the viewport

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

* fix(terminal): keep a deferred viewport claim when its replay write fails

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 16:58:19 -07:00
NeilandClaude fa0e933ce4 fix(terminal): keep the pane when a recovery snapshot is unavailable (#29874)
A server-pushed recovery the host marks unavailable carries an empty
image that is not the buffer. Applying it cleared screen and scrollback.
Classify recovery replies like tagged requests and retry instead.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 16:57:40 -07:00
Neil e23cfcdde8 test(native-chat): use an agent without chat modes in the paired-host permission tests (#29891)
#29335 gave Grok chats their own permission modes, so on a paired host
that predates per-chat modes a Grok chat is now checked like Claude and
Codex; that is intended. #29442's tests, merged a day earlier, used Grok
as the agent without modes. Use Gemini, which still has none.
2026-10-16 16:47:54 -07:00
Brennan Benson fcfe3196f7 fix(native-chat): decide provider failures by kind, never by searching the payload (#29092)
* fix(native-chat): decide provider failures by kind, never by searching the payload

A Claude chat opened with a red "claude · message:system:init" card whenever one of the
user's MCP servers failed to connect. The classifier searched every nested field of a
frame for failure-looking values and let that outrank the per-kind catalogue, so the
session-start roster's `status: "failed"` turned the whole frame into an error row. The
same search caused the earlier failed-background-task opcode cards.

A frame is now a failure only because its kind is one, or because a field its kind
declares as its outcome says so (Claude `hook_response.outcome`, Codex `hook/completed`
`run.status`). A Claude turn result is a failure when the result journaling says so,
since that depends on whether the user stopped the turn. With nothing left to override,
the typed-translator coverage list, its option and the task-frame sentence seam go.

Failed MCP servers write no row for Claude or Codex.

* test(claude): an uncatalogued frame keeps its sentence without becoming a failure

* fix(native-chat): keep Claude authentication errors visible
2026-10-16 16:41:14 -07:00
Kelvin Amoaba d963f2a23a fix(mobile): draw your own messages as markdown, fold long ones and open attached pictures full size (#29075) 2026-10-16 23:27:45 +00:00
Brennan Benson 027eb0ec83 Session History: save the parse cache one row per transcript, so restarts re-read nothing (#29401)
* Persist session parse results beyond the memory cache limit

* Expire Session History parse rows by idle time instead of a count cap

The parse cache dropped its oldest row once it held 4,096, so a scan over
more transcripts evicted its own upcoming hits and re-parsed every file on
every scan. Rows now stay until no reader has used them for 24 hours, swept
after each completed History scan and each search-index pass. Only the live
parser state keeps a 4,096 count bound; dropping it costs one whole re-read
of a file that changed, never a re-parse of one that did not.

Removes the overflow directory and the orcad persistence wiring from the
earlier revision of this change.

* Expire only rows History lists, and keep the newest transcripts under bounds

The search pass ran the idle sweep every 20 s but never re-stores History's
unchanged rows, so with search on a day away from History dropped them all
and reopening it re-parsed everything. Only a completed History scan sweeps
now, and it expires only rows History listed (on a monotonic clock). Rows
only the search index or a title lookup created keep main's 4,096 count
bound, so an index build can't grow the scanner process without limit.

Scans store newest transcripts first, so every store-order bound dropped the
newest, most active files: the saved snapshot kept the oldest ~11k rows and
the resume bound stripped the newest files' parser state. The snapshot is now
ordered by transcript mtime, and both count bounds drop the tenth of their
rows with the oldest transcripts.

* Drop the 24 h idle sweep; History rows live for the process

The sweep let whichever listing ran last decide when a row died, and
listings differ in scope: a smaller one (the phone's Agent History, a lower
desktop limit) expired rows only a larger one used, so the next Unlimited
scan re-parsed them. Rows History lists now live as long as the scanner
process; the snapshot's newest-transcripts cap bounds what survives a
reload, and rows other readers made keep the 4,096 bound.

Also clears the resume bookkeeping for a row the unlisted bound drops in the
same store.

* Skip resume bookkeeping for a row the unlisted bound just dropped

Running the resume trim first let a row the unlisted trim then deleted push
the resume set over its bound and cost 409 live resume points.

* perf(ai-vault): store the Session History parse cache one row per transcript

The parse cache was persisted as one JSON file, rewritten whole after any
change and capped at 64 MB / 1M tokens (about 11k rows). Past that cap, a
restart re-read the oldest unchanged transcripts, and an agent writing while
History was open rewrote 10-27 MB every few seconds.

The scanner child now keeps the rows in their own SQLite file. A miss in the
in-memory map reads the row by path; changes are written per row, debounced
and chunked; deletes run immediately. A used_at column, refreshed at most
daily, expires rows nothing has used for 30 days, so rows for transcripts
deleted outside Orca have a way to die. The old JSON file is imported once
and removed. Electron main only records the path for the child.

The rebuild-on-corruption open moves out of the search index into a shared
versioned cache database helper used by both.

* fix(ai-vault): import the old parse cache at open and expire rows only for missing transcripts

Review round 1 on the per-row parse cache store:
- The old JSON was imported only by a History scan or search pass, and only
  into an empty database, but deleted either way. Tab-title sync writes a row
  first, so the import was skipped and the first History open after updating
  re-read everything. The import now runs when the database opens, before any
  lookup, write or delete can reach it, keeping rows already present.
- A 30-day last-use expiry also dropped rows for transcripts that still exist,
  so opening History less than monthly re-read them. Rows unparsed for 30 days
  are now checked on disk and dropped only when the transcript is gone, then
  the freed pages are reclaimed. The daily last-use rewrite goes away.
- 8 KB pages (a ~2.3 KB row filled a 4 KB page), busy_timeout before the WAL
  switch, and lookups served from rows still waiting to be written.
- Revert accidental pnpm-lock.yaml churn.

* chore: revert accidental pnpm-lock.yaml churn

* fix(ai-vault): never stat WSL transcripts from the parse cache recheck

A stat on a \\wsl.localhost path outside the scanner's running-distro
filter and gated WSL lane could boot a stopped distro or hang on a stuck
one. WSL rows now skip the existence check and age out after 180 days
unparsed. SQLite-backed session keys use the shared OpenCode path splitter.

* fix(ai-vault): register the parse cache database with the WSL mover census

Each schema keeps its own meta table again, so the shared open helper
creates none and the search schema's SQL is unchanged. The parse cache's
tables are recorded as not moved: a cache rebuilt from transcripts.

* fix(ai-vault): a forced whole read never reuses a row a failed database delete left behind
2026-10-16 16:24:27 -07:00
Jinwoo Hong 3615d0423b Layout-owner follow-ups: closed waiting splits, paired-server tabs by host, first-tab e2e wait (dark) (#29866)
* fix(layout): E1 follow-ups: closed waiting splits, paired-server tabs by host, first-tab e2e wait

- A pane main closes before its tab mounts drops that tab's waiting split for it.
- Mounted tabs skip main's pane tree for paired-server workspaces by host kind, as frames do.
- ensureTerminalVisible waits for the activation tab before creating one.
- Main-asked splits write their pane through admitRuntimePane; startInAdmittedPane loses its single-caller `shown` option.

* refactor(layout): paired-server check in its own module, out of the frame sync
2026-10-16 16:22:42 -07:00
Brennan BensonandJae 47a5584ccb fix(acp): OMP, OpenCode and Grok chats follow their own permission mode (#29335)
* fix(acp): OMP, OpenCode and Grok chats run their own permission mode

An ACP chat's full access came from its agent's terminal Arguments, so OMP
and OpenCode, which have no bypass flag, asked on every tool call even when
the user chose Yolo. ACP chats now offer Ask for approval and Full access,
seeded from "Permissions for new chats", as Claude and Codex chats do; chats
from before take what they ran with, or the new-chat setting for an agent
that never had a bypass flag. Terminal launches are unchanged.

Co-authored-by: Jae <38941444+JaeCoding@users.noreply.github.com>

* fix(acp): hand the chat's permission mode to the ACP launch

The adapter narrowed the start to its identity and spawn token, so the mode
the host decided never reached the launch. Mobile's picker also showed
Claude's Ask wording for the other chat agents.

* fix(onboarding): Yolo mode also sets new chats' permission mode

Onboarding's Yolo mode says agents run without asking, but it wrote only the
terminal Arguments, so turning it off left new chats on Full access. It now
sets Permissions for new chats too, keeping an Accept edits or Approve for me
choice as it keeps custom Arguments. Pins that every ACP chat agent offers
Full access, and renames the backfill rule for what it decides.

* fix(onboarding): a replay left untouched keeps new chats' permission mode

Replaying onboarding, including from a paired browser whose Yolo switch starts
from its own defaults, rewrote the host's Permissions for new chats. It now
changes only when the person flips Yolo mode in this flow.

* refactor(onboarding): the Yolo switch's state lives in its own hook

Keeps the onboarding flow hook under the file-length limit after it began
passing whether the switch was flipped.

* test(native-chat): pin an OMP chat's pre-mode backfill at the record store

The host-install test's OMP row read no record; the backfill itself is
agent-agnostic, so pin it where the store writes it.

---------

Co-authored-by: Jae <38941444+JaeCoding@users.noreply.github.com>
2026-10-16 16:20:49 -07:00
Brennan Benson 39cf8703b7 fix(native-chat): keep the session host under its line limit (#29869)
#29121 and #27925 each added lines to structured-agent-session-host.ts and passed CI on their own; together they left it at 302 countable lines, so static analysis fails on main. Move the late-dispatch and unanswered-dispatch delegates into the mutation delegates that already wrap the same context.
2026-10-16 16:16:32 -07:00
Neil dd70294f0c fix(ssh): turn a host's orca CLI control on by default and stop resetting saved choices (#29856)
* fix(ssh): allow a host's orca CLI to control Orca by default, and stop resetting saved choices

An absent allowRemoteCliControl now means on; only an explicit false turns the
desktop browser bridge and relay-era orchestration commands off. Removes the
one-time reset of saved true values, the consent stamp writes and the reset
notice, which would have silently turned off hosts upgraded from stable.

* fix(ssh): keep the CLI control choice when adding a host from the sidebar

* fix(ssh): read an older client's omitted CLI control switch as off over RPC

* fix(ssh): check the managed fence before routing settings in browser bridge candidates
2026-10-16 16:08:37 -07:00
Neil c4c03e89bb Compact the Space window layout (#29857)
* Compact Space window layout

* Make Space summary and workspace rows single line

* Expose confirmed single-workspace delete in Space rows
2026-10-16 16:06:15 -07:00
Kelvin Amoaba 17a2bced11 fix(mobile): stop phone reconnects from undoing a desktop take-back (#29465) 2026-10-16 23:04:48 +00:00
Kelvin Amoaba 4d5f2a20ee fix(native-chat): dictate into the composer when it lacks focus (#29855) 2026-10-16 23:02:52 +00:00
Brennan Benson 8c1ade9e44 Keep pending chat decisions available outside loaded history (#29121)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 16:01:58 -07:00
Brennan Benson 6dd7ab5552 fix(native-chat): identify the owner when resume creation is refused (#29119)
* fix(native-chat): identify the owner when resume create is refused

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

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

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

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

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

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

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

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

* fix(native-chat): restore the refusal error import lost in the main merge
2026-10-16 15:57:54 -07:00
Brennan Bensonandm4air b7f6e71b7c perf(native-chat): flush send transactions without flushing every streamed revision (#28966)
* perf(native-chat): sync durable actions without flushing streamed revisions

* test(native-chat): provide the pending dispatch turn scope

* refactor(native-chat): reuse the journal transaction for durable writes

* fix(native-chat): save user intent before provider effects

* perf(native-chat): skip the per-commit fsync only for streamed assistant text

Every chat-journal commit ran a full fsync on the main thread. Replace the earlier allowlist of FULL rows (which missed control facts riding generic rows) with the inverse: FULL stays the default, and a transaction commits at NORMAL only when every row is a later revision of an assistant message that changes nothing but its content. An OS crash can lose only that streamed text; the next FULL commit makes it durable.

* fix(native-chat): keep image revisions fully durable

* fix(native-chat): preserve marked assistant revisions at FULL

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-16 15:56:21 -07:00
Neil f8089c787b fix(orcad): keep the integration credential key only in the owner-only key file (#29859)
* fix(orcad): keep the integration credential key only in the owner-only key file

Managed servers no longer create, read or wait on the macOS login Keychain or
a Secret Service keyring for their credential master key. A record an hourly
build pointed at the keyring is migrated once: the key is copied into the key
file when it reads back without UI within 5s, otherwise a new key file is
started and old credentials show the usual reconnect error. Removes the
locked-Keychain status fields, retry, the integrationCredentials.moveKeyToFile
RPC and the Settings notice.

* fix(orcad): serialize the keyring-record migration so concurrent starts agree on one key

* docs(orcad): note the migration lock

* fix(orcad): publish the migration lock with its PID and bound every wait

* refactor(orcad): reuse the file transaction lock for the keyring-record migration
2026-10-16 15:53:30 -07:00
Brennan Benson fa923c67e3 fix(projects): a manual folder-to-Git change proves the folder is the Git root first (#29282)
* fix(projects): refuse a folder-to-Git type change that would hide folder workspaces (STA-10638)

A folder project's extra workspaces exist only as metadata rows that the folder listing shows. Changing the project to Git switches it to `git worktree list`, so those workspaces drop out of the next refresh and their saved tabs are purged as if they had been deleted. Only the automatic upgrade checked for this; `orca project setup-update --kind git`, repo updates and clone-into-folder did not.

The check now lives in the one repo write every type change goes through, so any caller gets the same refusal and nothing is written. The automatic upgrade and both clone paths use the same check and keep the project as a folder project.

* test(repos): give the repo handler store mock the metadata read the clone path now uses

* fix(projects): a manual folder-to-Git change proves the folder is the Git root first (STA-10638)

Only the automatic upgrade checked that a folder project's path is the top of a Git repository and recorded Git's spelling of that root. A manual change (orca project setup-update --kind git, repo updates, clone into a folder project) flipped the type without either, so a project inside another repo became a broken Git project, and a project whose path Git spells differently lost its main workspace's id and tabs on the next refresh. All of them now run the same proof and write the proven root with the type change. SSH projects keep the unverified change because their Git runs on a host this process can't probe.

* Revalidate folder-to-Git conversion targets after Git proof

* Explain when a project changes during its Git check

* Fix clone conversion for replaced folder project

* test(runtime): create real Git root for clone upgrade fixture

* test: normalize runtime clone root stamp expectation

* test: check clone registration identity without unknown access

* chore: restore main's pnpm-lock.yaml that the main merge reverted
2026-10-16 15:53:12 -07:00
Jinjing c31712f1a0 fix(tabs): remove close-button tooltip (#29851) 2026-10-16 15:50:34 -07:00
Brennan Benson 69d877e272 fix(pty): window daemon spawns wait for a WSL guest's Pi/OMP relay, like host spawns (#29780)
A Pi or OMP agent started from the window inside WSL, with the terminal
daemon on, could start before the distro's hook relay was up. It then
reported no working/idle status for that session. Host starts and
local-provider starts already waited (#21721); the window's daemon path
was the one left out.

The shared host-env step now always runs the wait on the daemon spawns
it builds, so the lane input that skipped it for window starts is gone.
The wait is a no-op for anything but an explicit Pi/OMP launch in WSL.
2026-10-16 15:50:04 -07:00
Brennan Benson f9acfffa03 fix(unread): bold an agent row only while looking at it can clear it (#28830)
* fix(unread): one rule decides when an agent row is unread; title-only rows never are

The workspace card bolded a row Orca knows only from its terminal title or
process ("Claude Code - Idle") because that row restamps its state time on
every rebuild, so no acknowledgement ever covered it: opening the workspace
left it bold, clicking the row cleared it, and the next rebuild re-bolded it.
The dashboard already skipped such rows.

isAgentRowUnseen is now the single rule for the card, the dashboard snapshot
and counts, and the pop-out patch. It is built on the acknowledgement
module's own turn test, so a row is bold only when viewing it can clear it.

Fixes #28306

* fix(unread): bold an agent row only while auto-acknowledge can clear it

The row unread rule now reads the store's live and retained turn records, the
same records auto-acknowledgement clears, instead of a startedAt === 0 marker
on the row. Rows with no record of their own (title/process-derived, running
subagent children, migration-blocked panes) are never bold, so none can stay
bold with nothing able to clear it. Running subagent rows previously stayed
bold for their whole run.

* chore: drop accidental pnpm-lock.yaml churn

* perf(unread): build the row turn records once per pass, not once per row

* docs(unread): name the rule in the dashboard snapshot comment
2026-10-16 15:25:03 -07:00
Jinwoo HongandClaude afcca77a5e Window pane trees drawn from main's layout frames (dark, build switch off by default) (#29788)
* Window pane trees drawn from main's layout frames (dark, build switch off by default)

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

* Review fixes: paired-server saves keep their trees, last-pane guards count pending closes, draw main's answer when no frame is coming, redraw main's tree after a zoom

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

* Delta review: a closed stream's frozen cache never redraws pane trees; the frame reconciler never closes a tab's last pane

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

* Second delta review: draw the activation tab only when no frame showed it; a stream not yet open counts as coming; a redraw re-queues only its tab's workspace

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

* Independent review: a late record of a split main asked for restores it only if main took it back; a closed stream never redraws pane trees

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

* Delta review: deny a late record only for a split main wrote and did not take back itself; pane_closed only where the tab is; a reply before the stream opens waits for its first frame or end

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

* Re-review: replies wait only once the window opened its stream; a record that lands while main takes its split back waits for the outcome

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

* A record waits only while main is taking its split back

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

* refactor(layout): main keeps the pane of a split it asked for

Drops the timed withdraw and the window's late record of a main-asked split: main writes the pane and keeps it, a refused write fails the split, and a tab's first mount leaves queued main-asked splits to their request so the startup command still runs.

* refactor(layout): drop the start-without-pane option no caller uses

* refactor(layout): drop an always-false check in the queued main-split filter

* test(layout): main-asked splits into an unmounted tab, with the stream down, and with no start

* test(layout): type-safe withdraw stub

* fix(layout): equalize draws itself when no frame is coming

* refactor(layout): name the frame-tree pane moves for their role

* refactor(layout): frames' pane trees reach the store through the frame store patch

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 15:24:12 -07:00