Commit Graph
13951 Commits
Author SHA1 Message Date
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
Neil c827ed5324 ci: run full native mobile typechecks without compiler caches (#28409) 2026-10-13 14:27:14 -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
Jinwoo Hong 975c27b780 fix(usage): cap Cursor's Retry-After at 24h and honor its HTTP-date form (#28406)
Cursor parsed Retry-After with a bare Number(), so a huge header could block
usage refreshes for years and an HTTP-date was ignored. Claude and Cursor now
both call parseUsageRetryAfterMs, which applies the 24h usage cap to the shared
Retry-After parser, replacing Claude's private copy of the same algorithm.

The shared parser is renamed from relay-retry-after-header/parseRelayRetryAfterMs
to retry-after-header/parseRetryAfterHeaderMs: relay, the updater, mobile and
usage all use it.
2026-10-13 17:09:06 -04:00
Brennan Benson 0903ed9577 Start a startup terminal while Orca's window is hidden instead of waiting for it to be shown (#28351)
* fix(terminal): don't hold a startup command for the grid settle while the window is hidden

A startup pane waits for its grid to stop changing, counted in animation
frames, so a TUI isn't drawn at a transient size. A covered or minimized
window produces no frames, so the launch waited until Orca was visible
again. Skip the settle while the window is hidden, as hidden panes already
do, and end an in-flight settle when the window becomes hidden.

* test(terminal): pass connectPanePty doubles through one checked cast
2026-10-13 13:38:47 -07:00
Jinwoo Hong 6cfe678dbe ci(relay): use the runner's preinstalled gcloud instead of downloading the SDK (#28392)
* ci(relay): use the runner's preinstalled gcloud instead of downloading the SDK

dl.google.com downloads of google-cloud-sdk time out on the runners, failing every relay
operation at setup-gcloud. skip_install keeps the WIF cred-file login.

* ci: drop the temporary gcloud skip_install proof workflow
2026-10-13 16:24:53 -04:00
OrcaWinandm4air 64dd8f5abf fix(ssh): a host whose ssh_config forwards a port can still open its server tunnel (STA-10471) (#28396)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 13:24:34 -07:00
Jinwoo Hong c745051d7d refactor(main): share one caller-signal + timeout composition (#28393)
Behavior-preserving refactor. Eleven sites built the same
`signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms)`
inline or behind a local wrapper (Codex `createCodexBackendRequestSignal`,
`probeDeadline`, `boundedSignal`). They now call one
`abortSignalWithTimeout(signal, timeoutMs)` in
src/main/lib/abort-signal-with-timeout.ts:

- usage requests: Claude OAuth, Cursor, Grok, OpenCode Go API key, ZCode,
  Codex backend usage + reset-credit read
- local worktree path presence, fork sync, retarget divergence budget,
  upstream-ref watch resolution

The Codex reset-credit redeem, which never had a caller signal, uses plain
AbortSignal.timeout. Transports, request options, timeout values, error
mapping, and Retry-After handling are unchanged.
2026-10-13 16:14:17 -04:00
Neil b3ad227650 ci: use Bun 1.4.3 across all workflows (#28384)
* ci: share Bun 1.4.3 across current toolchain workflows

* ci: use shared Bun pin for server migration tests too
2026-10-13 13:03:19 -07:00
Jinjing 06a05b2e56 Auto-hide the menu bar in web popups and re-layout when it appears (#28388)
On Windows and Linux, Orca's app menu is attached to every window, including web popups, so it permanently takes up vertical space above the popup content. Popups now hide that menu by default, so they match a normal browser window. Alt still reveals it, but revealing it shrinks the content area without resizing the window. The origin bar and content view kept their old heights, which left a gap or clipped content. The popup now listens for `bounds-changed` on its content view and re-runs its layout, so the content fills the space under the revealed menu bar.
2026-10-13 13:01:58 -07:00
Brennan Benson c5d7628686 fix(native-chat): chats from before permission modes keep the mode they were running with (#28099)
* fix(native-chat): chats saved before permission modes run the new-chat default

A Claude or Codex chat created before chats had their own permission mode has
none saved, and it ran Ask, so every existing chat of a Yolo user started asking
for approval after the update. A chat with no saved mode now runs Settings ->
Chat -> Permissions, read from the host's settings on each launch, read and
restart check.

* fix(native-chat): an older chat keeps the new-chat mode its first start ran with

A chat saved before chats had permission modes ran the new-chat default on every
start, so it kept following that setting. Its first start now writes the mode it
ran into the chat's saved options, through the same write that already carries the
chat's other options over a start; from then on it is a chat with its own mode.

* fix(native-chat): save older chats' permission mode once at host startup

Replace the save-at-first-start path with one startup pass: before the chat
host is exposed, every Claude and Codex record with no saved permissionMode
gets the host's new-chat default, in one store transaction. Saved values,
including invalid ones, and every other option are left alone; a read-only
store is skipped and a failure is logged and retried next start.

The runtime fallback in launch, acquisition, provider-started and attach is
restored to main's behaviour. A client-supplied attach that creates a new
record now gets the same new-chat seed as every other create path.

* fix(native-chat): fill each older chat from its own agent's arguments

An older Claude or Codex chat ran with that agent's own Arguments before
permission modes existed, so the startup pass now saves full access or ask
per agent instead of the combined new-chat default. A user with Claude on
bypass and Codex on Manual keeps Claude chats on full access.

The attach create path always passes its permission seed; the reservation
already applies options only when it creates the record.

* refactor(native-chat): read each chat's agent arguments directly in the permission backfill

* test(native-chat): give stub runtimes the launch seed the attach path reads
2026-10-13 12:34:27 -07:00
Jinwoo HongandClaude 24bda717f4 fix(terminal): a slow window no longer makes orca terminal create fail and then duplicate (#28158)
* fix(terminal): start CLI-created workspace terminals in main so a slow window cannot duplicate them

A focused or interactive-agent `orca terminal create` asked the window to make the
terminal and reported failure after 10 s, while the window still made it later; a
retry then made a second one. Main now starts every terminal that names a workspace
and shows it in the window best-effort, as it already did for every other create.

* fix(terminal): start a main-created terminal at its worktree's measured pane size

An unfocused CLI agent opens as an inactive tab that is not resized until the user
opens it, so starting it at 120x40 left its output wrapped to 120 columns. Start it at
the size of the pane active in the worktree's active group, where the new tab opens;
fall back to 120x40 only when that worktree has no measured pane.

* fix(cli): keep sending rendererBacked for hosts before v1.4.211

Hosts without #22384 leave a background-started agent's startup queries unanswered,
so a new CLI talking to an old, still-running host must keep routing interactive agents
to the window. Newer hosts ignore the flag.

* fix(terminal): start host-created terminals at the measured size of their tab group

The window publishes, on its graph sync to main only, the grid an unsplit terminal
would get in each shown tab group, computed by one pure function that mirrors
FitAddon.proposeDimensions and is pinned against it; a ResizeObserver on each group
body republishes when that grid changes. Main starts a new terminal at the size of the
group it opens in (the named group, else the active one) and falls back to 120x40 only
when no window measured it. This replaces the active-pane lookup, which mis-sized split
groups and could read a phone's fitted size.

* fix(terminal): measure group sizes from an untitled default-font pane and use the live active group

A pane with a title or banner pushes its xterm host down, which skewed every group's
row count; skip it like a zoomed pane. Size from the window's latest published active
group, which is where its reveal places the tab, before the saved session's.

* fix(terminal): size a CLI agent in a workspace the window has not shown

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

* fix(terminal): learn the single-group inset only from bodies shown in the content area

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

* chore: rerun PR checks against main with #28297

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

* chore: rerun PR checks against main with #28301

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 15:08:45 -04:00
Jinwoo Hong 32fc38d575 fix(ssh): a disconnect keeps an old relay terminal's tab on that terminal instead of starting a new shell (#28344)
* fix(ssh): a disconnect keeps an old relay terminal's pane on the bridge instead of a new shell

* fix(ssh): release held panes when a reconnect finds their leases retired

* test(ssh): let the node typecheck see the renderer's bridged-pane selection
2026-10-13 14:39:40 -04:00
Jinwoo HongandClaude b8e49222a3 fix(remote-runtime): wait for the desktop's terminal list before opening a terminal (#28154)
* fix(remote-runtime): wait for the host's terminal list before opening a terminal

A paired client opening a workspace whose terminals it had not heard about
yet read "no answer from the host" as "zero terminals" and asked the host for
one. When the host's list then arrived with terminals, the workspace gained an
extra terminal; when it arrived empty, the first-frame bootstrap created a
second one.

The client now treats an unknown host count as unknown. Activation without
explicit startup work waits, and the active subscription's first real answer
seeds the workspace if it is empty. An "ask me later" frame from a host that
has not published yet no longer records a count of zero, and the host's later
answer (sent as an update) is accepted as the first answer for seeding.

* fix(remote-runtime): count an unknown host as empty unless it answers later

Hosts before #25736 answer an empty workspace with "ask me later" and
never follow it with a real answer, so waiting for that answer left a
blank workspace with no terminal against them. Wait only on hosts that
advertise session-tabs.empty-worktree-answer.v1; older hosts keep
main's behaviour. Workspaces this client just created are known empty
and open their first terminal without waiting.

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

* fix(remote-runtime): log a failed delayed terminal open; drop an inert flag

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 14:39:37 -04:00
Jinwoo HongandClaude b5d739ee19 fix(remote): keep a paired pane when the desktop has not opened it yet (#28181)
* fix(remote): keep a paired pane when the host has not mounted it yet

A paired client read the host's `no_connected_pty` answer as a clean
exit and closed the pane. The host also sends it when its window has
not mounted the pane within its wait, so it proves nothing about the
terminal. Back off and resubscribe within the existing recovery budget
instead; only an explicit exit verdict closes the pane.

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

* test(remote): a pane the host stops listing after no_connected_pty ends as lost

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 14:14:17 -04:00
Jinwoo Hong 347ab83b8d fix(automations): stop offering Resume workspace for runs that never started an agent (STA-10103) (#28179)
* fix(automations): stop offering Resume workspace for runs that never dispatched (STA-10103)

* fix(automations): hide the run page action when there is nothing to open
2026-10-13 14:07:55 -04:00
Jinwoo Hong 3a011e48d1 fix(native-chat): load long chats on the phone by keeping every update within its limit (#28203)
* fix(native-chat): fit chat stream pages in one mobile bridge frame

A long native chat failed to open on a phone with "the shell ended this
stream (overflow)". The host sized each history page to ~1.94 MiB (half
the relay cap), but the mobile app ends any stream whose event frame is
over 640 KiB, so the opening snapshot of a long chat never arrived.
Pages now fit that frame; older rows load through Load earlier as before.

The page also words a stream the app ended in plain language instead of
the protocol's reason.

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

* refactor(native-chat): keep the outline on the relay page share; plainer end words

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

* test(mobile): read the bridge frame cap from its shared definition

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

* feat(native-chat): bounded frames for the phone's chat stream

A phone page now asks the host (when it advertises
agent-session.bounded-frames.v1) to keep every agentSession.subscribe
frame within the bridge's 640 KiB frame:

- each row is cut to what the phone draws (tool output to its detail
  length unless it draws as a diff, tool inputs to the diff view's
  length) and then to one row's share of a page, its long strings cut to
  a common length; the row is marked `truncated` with its full size
- history frames carry rows and metadata held to a fixed ceiling; the
  draft queue and the background-task roster go in frames of their own;
  the command list is not sent, since the phone reads it from options
- the queue's admission bound counts whole cards and is sized to one
  frame, so a phone always gets every card whole

Other subscribers get the frames they always did.

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

* refactor(native-chat): bounded frames review round 1

- the phone waits for the host's capabilities before opening the
  stream, so its opening frame is never sent unbounded
- rows are bounded once, by the reader that sizes the page; the item
  form is an options field and always a function
- one cut rule (`cutLongStrings`) serves the display trims and the fit
- a side list rides an empty history batch rather than a second frame;
  commands changes send a bounded subscriber nothing
- queue admission measures the candidate list once; a legacy queue of
  more cards than fit lists the ones it sends first

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

* refactor(native-chat): bounded frames review round 2

- an over-budget legacy queue cuts its text to one common length and
  marks the cards it cut, and no longer names a next card it left out
- one predicate says whether a subscriber is owed the `/` list
- a lighter invariant fixture

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

* fix(native-chat): type the shortening guards against named shapes

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

* refactor(native-chat): bound every phone's chat stream at the host

The host now bounds frames for every phone (clientKind 'mobile') at the RPC
boundary, so the subscribe param, the host capability and the phone's wait
for the capability probe are gone. Phones of every version already ignore
the stream's slash commands, and a phone reached through the desktop relay
signs in as a phone too.

- A live gap of more than one page reaches a phone as the newest page, so
  no publish outruns the bridge's unacked window; older rows load on scroll.
- Queue admission sizes each card as wide as a failed send can make it.
- Rows are bounded only as a page takes them; the unread row marker is gone
  and a shortened queue card is marked `truncated: true`.
- A bounded subscriber no longer reads or tracks the slash commands.
- The stream-end words are back to main's: `overflow` also covers a page
  that stopped acking, which "too large" would misstate.

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

* fix(native-chat): count every card in line and never shorten one

Queue admission now counts every card the queue can publish again without
passing admission: waiting and returned cards, and sent cards whose delivery
is not yet accepted, which a refusal or a Stop puts back. The fixed per-card
reserve for a failed send's words is gone.

When a phone's list is over its frame room anyway (an older build's larger
queue, or a card that grew after admission), the phone gets the cards it
sends first, whole, and the rest once those leave. Cards are never shortened,
so no app version can Edit a partial copy over the full draft, and the
`truncated` card flag is gone.

Bounded frames strip only the roster from an event; no event carries the
other side lists.

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

* refactor(native-chat): bound only phone frames; keep main's queue and page sizes

Queue admission is main's again. The phone's list is bounded where it is
sent: the cards it sends first, whole, so every frame fits for any queue
size and no card is shortened.

History pages keep main's size for every reader but phone subscribe frames.
A reader is now its item form plus its page budget, so desktop pages, the
outline and phone history replies (chunked by the app's bridge) are as on
main, and only bounded frames use the bridge frame's share.

Drops the queue's `nextQueuedMessageId` rewrite, which no phone reads, and a
test that duplicated the prefix check.

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

* refactor(native-chat): bound phone rows by size alone; final tidy

Rows the journal already caps at a 16 KiB head gain little from a second,
display-shaped cut, and the diff check behind it passed a character count as
a line count. A row is now held to its frame share by shortening alone, and
native-chat-diff is back to main's.

Unexports file-local bounds, fixes a capability-era test comment, and adds
tests for string shortening and a phone's stop-note refresh.

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

* test(native-chat): assert a phone's stop-note refresh pages

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

* test(native-chat): run the bounded-frames suite in the Node runtime project

It opens a real SQLite journal.

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

* fix(native-chat): bound legacy dispatch reasons on read

Rows written before the write-side bound carry a provider's full error text,
which every page and phone frame attaches with its submission.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-13 13:39:34 -04:00
Jinwoo Hong 8471b3a5c2 fix(layout): restore the saved active group as a window does (#28174) 2026-10-13 13:04:55 -04:00
Neil 962c338b67 feat(wsl): Windows path actions for WSL hosts through one owner-routed capability (WSL-via-orcad C8) (#27955)
* feat(wsl): open WSL host paths from Windows through one owner-routed capability (WSL-via-orcad C8)

* test(wsl): typed shell handler lookup for the WSL owner tests

* fix(wsl): read known WSL hosts through a registered accessor; e2e for the Windows folder picker

* fix(wsl): open the host-path entry filled in after a Windows pick; e2e screenshots

* test(wsl): read the e2e picker's default path through the main process env

* refactor(wsl): read a WSL host's distro from its record instead of a new accessor

* fix(wsl): skip the host browser only for a path Windows' picker or an Explorer drop filled in

A path left filled from an earlier pick on an SSH or paired host opens the host browser again, as it did before.
2026-10-13 10:00:55 -07:00
Kelvin Amoaba 86e42a37be feat(native-chat): expand mermaid diagrams into a pan and zoom viewer (#27715) 2026-10-13 09:44:07 -07:00
Jinjing deb6a71b29 Show folder workspaces in the Cmd+J jump palette (#28175)
* Show folder workspaces in the Cmd+J jump palette

Cmd+J only listed git worktrees, so folder workspaces could not be found or opened from it. The palette now includes folder workspaces alongside git worktrees and renames the section from "Worktrees" to "Workspaces" to match.

- Folder rows are searchable by name, comment, and attached GitHub item, and follow the sidebar's host scope.
- A blocked activation (for example, a folder path that no longer exists) keeps the palette open and shows only the path error, instead of closing or showing a second generic error.
- Folder path-status caching and sidebar reveal are keyed by execution host, so folders with the same id on different hosts no longer share cache entries or reveal the wrong group.

* Consolidate runtime target imports in folder-path-status

`getRuntimeTargetHostId` now comes from the same module as `runtimeTargetForOwnerEnvironment`, so both are imported from `runtime/runtime-client-target` instead of splitting them across two modules.

* Wake sleeping folder workspaces from Cmd+J

Picking a sleeping folder workspace from the jump palette did not restart its terminal or restore its Files explorer view. Folder and git worktree activation now share one code path in workspace-activation.ts, so both wake the same way.

- Wake a sleeping folder the same way as a worktree: bump the tab generation and tag the pending spawn when no PTY is live, including when the workspace is already selected.
- Restore the remembered right-sidebar explorer view for folders.
- Skip the GitHub PR refresh for folder workspaces, which have no PR.
- Clear unread only on the folder entry whose execution host matches the activation.

* Reveal folder workspaces on their catalog owner host

Cmd+J activation revealed folder workspaces against the focused runtime
server's host, so a folder owned by a local or SSH project could be
revealed under an unrelated server. The reveal host now comes from the
folder workspace's catalog owner, and an explicit caller host still wins.

Add a regression test covering local and SSH owners while another
runtime environment is focused.

* Keep shared workspace activation in the existing layout writer
2026-10-13 09:41:52 -07:00
Neil 9f2abd5e70 feat(wsl): Add WSL distro host UI and status (WSL-via-orcad C7b, experimental) (#27934)
* feat(wsl): Add WSL distro host UI, host row status and fake-transport e2e (WSL-via-orcad C7b)

Experimental toggle (Windows only), an Add WSL distro choice beside SSH host and remote server,
the WSL host's own row in host pickers with its liveness state, a Connect action that starts a
stopped distro only on request, a WSL hosts list in Settings, and the renderer report of which
WSL hosts an open project or terminal needs.

* feat(wsl): show WSL hosts as turned off while the experiment is off, and confirm their removal

With Experimental → WSL distros as hosts off, a WSL host keeps its row so its projects never
seem to vanish, says it is turned off, and offers Turn on (opening that setting) instead of
Connect; Settings lists it the same way with Connect disabled. Removing a WSL host now asks
first in the dialog SSH hosts use, with WSL copy. The e2e covers both.

* fix(wsl): skip the WSL host status bridge when the preload lacks it

* fix(wsl): show when this build cannot run WSL hosts instead of failing an add

The UI reads main's template capability once. Without it, the add dialog says this build can't
run WSL hosts and disables Add, existing hosts' rows and Settings say the same with Connect
disabled, and the Experimental toggle notes it. The e2e covers a fake build without the files.

* fix(wsl): re-read the WSL distro list when the add dialog's window returns, and on Refresh

The add dialog read the registered distros once, so one imported while it was open never
appeared. It now re-reads when the window regains focus or becomes visible, and from an explicit
Refresh button, never on a timer. The selection is derived from the current list, so a refresh
cannot leave a stale pick. The e2e imports a distro mid-dialog.

* fix(wsl): list WSL hosts only as WSL hosts, not among the generic remote servers

Settings → Remote Orca Servers, the server-update checks and the status bar's server list now
leave out environments whose deployment record is a WSL host (by kind, never by name). The WSL
hosts section reads them from the store itself.

* fix(wsl): say a WSL host's removal ends its terminals, and report what it left running

The confirmation now says removing ends the host's open terminals as well as Orca's server, and
a removal main could not complete in the distro (a terminal or the daemon kept running) is
reported instead of passing silently.

* fix(wsl): ship the removal copy that says a WSL host's terminals end

The build resolves copy from the English catalog, which still held the old text under the
reused key, so the new inline default never reached the dialog. The copy gets its own key, the
stale entry is dropped from both catalogs, and a test reads the shipped catalog.

* fix(wsl): leave a removed host's workspace, keep its removal warning up, and prove removal in e2e

After removal the active workspace is left when it lived on that host, so nothing keeps calling a
host that no longer exists, and a warning about what still runs in the distro stays until
dismissed. The e2e now checks that removal ends the host's terminals on the server standing in
for orcad, shows no warning, leaves the workspace, and starts nothing for the host afterwards.

* test(wsl): read the shipped removal copy and the fake's connect log without reflection

* fix(wsl): say a WSL host is turned off in its terminal instead of 'Reconnecting'

With the experiment off (or without server files) nothing retries, so a WSL host's terminal now
says so, with Turn on, rather than claiming Orca is reconnecting.

* test(wsl): reproduce project-removal survivors and orphan terminals in e2e

The headless server standing in for orcad now carries a project whose terminal must end when the
project is removed (and leave the host's own catalog), and an orphan terminal whose project the
host already forgot, which removing the host must still end. Both checks fail without the fixes.

* fix(wsl): drop the IPC wrapper from file-explorer and editor load errors; offer Turn on when WSL hosts are off

The file explorer showed "Error invoking remote method 'runtimeEnvironments:call': Error: ..." for a
WSL workspace whose host was turned off. Explorer read errors now go through the shared IPC-error
unwrapping, and when the workspace's WSL host is turned off the empty pane shows the same turned-off
copy and Turn on button as the terminal banner instead of a read error and a Retry that cannot work.
The editor's file-load error view drops the same wrapper at render time (stored text is unchanged
for retry classifiers).

* test(wsl): open the internal gate for the WSL host e2e, and prove a closed gate shows nothing

The fake-transport e2e launches its desktop with ORCA_INTERNAL_WSL_HOSTS=1.
A new case leaves the gate closed with a WSL host already saved and the
Experimental setting on: no WSL host API, list entry, host row, Add WSL
distro action, Experimental toggle or Settings section appears, the fake
never starts a distro, and the saved host stays on disk.

* test(wsl): count only live shells when proving a removed project's terminals ended

On Linux CI the hub keeps a pty-less tab record for the removed worktree after terminal.stop ended its shell, so counting every terminal.list row never reached zero.
2026-10-13 09:37:50 -07:00
Neil 7ed23199a5 refactor(orcad-migration): migration source abstraction, capability-gated source.kind, path-rewrite stage (WSL-via-orcad D0) (#27790)
* test(orcad-migration): pin the SSH manifest digest before the source abstraction

* feat(orcad-migration): capability-gated source.kind discriminator, SSH wire unchanged

* refactor(orcad-migration): MigrationSource abstraction with SSH relay as the only source; table-driven path-rewrite stage (identity for SSH)

* fix(orcad-migration): name the wire source type

* chore(orcad-migration): exhaustive path-kind switch; no test assertion

* test(ssh): run the relay migration source's SQLite test in the Node runtime project

* perf(orcad-migration): rename record keys in linear time and skip the walk for the identity

SSH exports run the path-rewrite stage on every stage and commit check; re-reading Object.keys per entry made a 10k-key record cost seconds on the main thread.
2026-10-13 09:23:59 -07:00
Neil fc4bf86319 [CI] Avoid native jobs for isolated policy self-test edits (#28334) 2026-10-13 08:35:56 -07:00
Neil a65fdc887f feat(wsl): WSL distro hosts, main-process wiring and persistence (WSL-via-orcad C7a, experimental) (#27933)
* feat(wsl): persist WSL host deployments in their own store and type orcad deployment links by kind

* chore(wsl): drop an unused link type export

* feat(wsl): add, persist and connect WSL distro hosts behind an experimental flag (WSL-via-orcad C7a)

Main-process wiring for WSL hosts: list registered WSL 2 distros, add one (C4 deploy and
launch through C6 liveness, then persist the paired environment and its C2 record), keep a
relaunch's new device token, connect a host only for the user or an open project or terminal,
adopt holders at startup, and remove a host (release its holder, drop both records). Refuses a
distro whose orcad this desktop already reaches as another server (R25).

* fix(wsl): read distro registrations through one reader that returns the canonical unbraced GUID

C4's registry reader returned the braced GUID while the C2 record only accepts it unbraced, and
C7 parsed the same output a second time. The one reader now yields lowercase unbraced ids and
each registration's WSL version; the host distro list uses it.

* fix(wsl): refuse to connect a WSL host while the experiment is off

Adding, connecting and background connects all stop while Experimental → WSL distros as hosts
is off; removing still works. The pre-deploy refusals move to their own module.

* fix(wsl): stop a connected WSL host's server when the host is removed

Remove ended the bridge and released the distro's holder but left orcad to exit with the
distro. A connected server is now ended right away, only in a distro already running.

* fix(wsl): import the orcad host errors from their new orcad/ home

* fix(wsl): refuse WSL hosts in a build without the orcad template, and say so up front

A build shipped without the orcad deployment template let the add dialog run and fail after
seconds with a raw internal error. Main now checks the template as SSH hosts do, refuses add
and connect (and skips lazy connects and holder adoption) before any deploy work, and exposes
the capability so the UI can show it.

* test(wsl): let the e2e WSL fake re-read its distro list, to register one mid-run

* fix(wsl): end a removed host's terminals and daemon, and stop WSL hosts when the experiment is off

Remove now ends the host's terminals through orcad's terminal RPC, waits for the terminal census
to prove none remain, closes the bridge, and runs the managed decommission SSH hosts use, which
stops orcad and retires its idle terminal daemon. What it could not end is reported back.

Turning Experimental → WSL distros as hosts off closes every connection and liveness watch,
releases the holders so distros can idle-stop, and keeps liveness from relaunching for visible
work; turning it on resumes only the hosts open work needs.

* fix(wsl): keep WSL hosts out of the servers a phone reaches through this desktop

They are this desktop's own distros, connected only for work open here; a phone listing or
routing to one would wake it behind the user's back.

* fix(wsl): end a removed host's terminals through its own route, and never respawn it

Removal reached orcad with the bridge's raw pairing offer rather than the routed environment and
saved pairing the app already uses for the host; on a real orcad ending terminals failed fast and
left them, the daemon and orcad running. It now calls by environment id through the same
transport as every other call to the host, and the e2e fake runs these real steps against a
headless server.

Removal also stops tracking the host first (open work, connects, runtime calls), so nothing
reconnects it while it stops, and a spawn guard in the connector refuses any holder, deploy or
bridge for a distro whose host is gone (the respawn loop after removal). A WSL host that isn't
connected answers runtime calls with why, instead of failing as an unreachable network server.

* fix(wsl): suggest reconnecting a WSL host, not Tailscale, when it can't be reached

Unreachable-server hints are chosen by the owner host's kind: a WSL host is a distro on this
computer, so it points at its reconnect instead of the Tailscale advice for network servers.

* test(wsl): parse the terminal list removal reads, and log the e2e fake's connects to a file

* fix(wsl): close a removed host's terminals by handle and report stopped only on proof

Removal stopped terminals per workspace, so one orphan whose project was gone threw
selector_not_found and aborted before orcad was stopped. It now closes each terminal by handle,
keeps going past failures and names them, and still asks orcad to stop.

It reports stopped only when the census shows no session left, the daemon retired, and the
orcad the distro records as active is the one this computer was connected to; a re-add that
adopted an orcad the record does not name is reported instead of claimed stopped.

* feat(wsl): close WSL hosts behind an internal-only gate in every build

WSL distros as hosts must not be reachable by users until moving a project
between hosts is lossless and the feature matches today's WSL support.
ORCA_INTERNAL_WSL_HOSTS=1 opens the gate; otherwise main creates no WSL host
service (no holder adoption, wsl.exe spawn or reconnect), refuses add,
connect, remove and every runtime call to a WSL host, keeps persisted WSL
hosts out of the server list, startup status probes and AI vault scans while
leaving them on disk, and the preload exposes no WSL host API.

* fix(wsl): keep SSH and paired servers listed whatever the WSL deployment file holds

An unreadable WSL record no longer hides an SSH-linked server, and with the internal gate closed a corrupt WSL file is warned about instead of failing the whole registry.

* fix(wsl): let an unreadable WSL record hide only a loopback pairing

A WSL host always pairs over a loopback endpoint, so a stale record can no longer remove a remote paired server from the list.

* fix(wsl): ignore unreadable WSL deployment state while the internal gate is closed

A newer or stale record could otherwise hide a re-paired loopback server for users who never open the gate.
2026-10-13 08:18:08 -07:00
Neil 5e9489c0c0 feat(wsl): account env for agent tabs on a WSL orcad host (WSL-via-orcad C9b) (#27940)
* feat(wsl): account env for agent tabs on a WSL orcad host (WSL-via-orcad C9b)

terminal.createAgentSession and an explicit terminal.ensureAgentSession (new
agent tabs, restored and sleeping agents) accept an optional accountEnv, gated
by the new agent-session.account-env.v1 capability because both schemas are
strict. The desktop shim attaches it only for an advertising WSL orcad, reading
its capabilities from status.get; an older host keeps its default account and
the desktop shows a one-time notice per distro. accountEnv stays outside the
create replay fingerprint.

* test(wsl): name the C9b runtime store stub's cast

* test(wsl): expect the WSL account notice listener in the app-lifetime IPC lifecycle
2026-10-13 07:42:38 -07:00
Jinwoo Hong f1d1a9b076 test(e2e): sample the delete dialog until it has enough frames (#28323)
* test(e2e): sample the delete dialog until it has a full frame floor

* test(e2e): drop the frame-count assertion the sampling loop now guarantees
2026-10-13 10:30:37 -04:00
Neil 41e6ea530a feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9) (#27874)
* feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9)

The desktop prepares the selected Claude/Codex account's in-distro home with the
existing WSL account code and sends it as env on terminal.create /
session.tabs.createTerminal, or as agent.launch's new optional accountEnv.
Structured sessions keep their host-derived home and warn. Launch-time CLI
resolution inside a WSL guest skips /mnt/<drive> shims.

* refactor(wsl): track WSL host environments beside their registry
2026-10-13 07:23:53 -07:00
Neil fc7dff8ce1 ci(wsl): real WSL2 lane for WSL via orcad (C10) (#27921)
* ci(wsl): real WSL2 lane for orcad deploy, bridge, liveness and holder (WSL-via-orcad C10)

* ci(wsl): upload the template's dot-directories; save the rootfs cache on failure

* ci(wsl): update the runner's WSL to 3.0.1+ before asserting WSLInterop survives

* test(wsl): let a released holder settle before asserting it exited

* ci(wsl): install a pinned, cached WSL 3.0.1 MSI instead of wsl --update

* test(wsl): keep the lane's survival seconds typed in its assertion message
2026-10-13 07:09:02 -07:00
Jinwoo Hong aa7afabb1d test(e2e): wait for the paired-reveal decoy to settle at its new size (#28319)
* test(e2e): read the parked-reveal decoy grid only after it settles at the new fit

* test(e2e): name the settle wait and give it the file's reveal timeout
2026-10-13 10:00:18 -04:00
Neil 4280eb118c feat(wsl): liveness, failure classification and recovery for WSL orcad (WSL-via-orcad C6) (#27916)
* feat(wsl): liveness, failure classification and recovery for the WSL orcad connection (WSL-via-orcad C6)

* fix(wsl): a user connect supersedes liveness recovery; report a stop once

* chore(wsl): exhaustive terminal verdict switch; typed test state
2026-10-13 06:52:35 -07:00