mobile/ is a separate pnpm project, so the root pnpm install leaves
mobile/node_modules empty and pnpm build failed in build:mobile-web with
hundreds of unexplained esbuild "Could not resolve" errors.
build-mobile-web-app-bundle now checks for mobile/node_modules first and
exits with the one command that fixes it. The probe path is shared with
the bundle tests' dependency check. CONTRIBUTING documents the install in
Local Setup and points "Before Opening a PR" at it.
Fixes#24324
Co-authored-by: Neil <neil@stably.ai>
* fix(wsl-host): give phone, resume and fork agent launches the Windows-only refusal
* fix(wsl-host): hint the Command setting on required-CLI refusals
* fix(wsl-host): refuse explicit resumes of a Windows-only CLI with the fix-it message
Idle retirement (last-client disconnect, initial-adoption timeout, ownership
loss, and shutdownIfIdle) skipped onShutdownRequested(), so a stalled resource
disposal left the daemon alive with no five-second exit deadline.
Fixes#29406
Co-authored-by: Neil <neil@stably.ai>
* feat(media): read remote media ranges through the paired runtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(media): stream remote media through orca-media grants
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(editor): play video and audio from remote runtime workspaces
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(media): surface update-required for remote media and cancel in-flight chunks
Probe files.readChunk once per environment and pairing revision before minting a
grant so older servers show the update message. Closing a media stream now aborts
the in-flight remote chunk. Also validate the IPC address, fix extension-only
names, and tighten the preview return type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(files): one classifier decides which viewer opens a file
Local reads, files.readPreview on hosts, git blob previews and the desktop's
remote binary handling each kept their own image/PDF extension table, and
media had another. file-preview-kind.ts is now the one table: image, PDF,
audio or video, with the MIME type. AVIF, which only local reads knew,
previews on hosts and in git diffs too.
* perf(runtime): give byte-range reads their own small call lane
files.readChunk (media streams, paged files, downloads) now runs in a
two-call lane per environment, so a playing video or a download never holds
the slots terminal input and workspace calls use.
* feat(files): stream previews from paired runtimes by byte range
Generalises the remote media grant into one byte-range preview path: an
orca-media:// grant keyed by environment, worktree, path and pairing
revision, streamed over files.stat/readChunk. Audio and video use it now;
the shared classifier decides which viewers do.
- One files.stat at mint replaces the readChunk probe: it proves the host
serves range reads and that the path is a file, so an old host or a
missing file fails in the tab, not as a silent player error.
- A burst of range requests shares one stat (reused for 1s), so seeking
costs one round trip less on a slow link; later requests re-stat, so a
growing recording keeps playing.
- Removing a paired runtime revokes its grants; a re-paired one answers
410 and drops the grant.
- Only readers that render bytes mint a stream; baseline checks don't.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
* fix(files): prove chunk reads at mint, and let a reload report the new size
Hosts from v1.4.5 answer files.stat but only v1.4.111 and later answer
files.readChunk, and no capability names it, so minting reads one byte,
cached per pairing, and an older host fails in the tab with the update
message. A re-mint seeds the grant with its fresh stat, so a reload never
reuses an older size. A renderer gone mid-mint mints nothing. Preview
chunks time out after 20s so a stalled one frees its lane sooner.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
* fix(files): resolve preview hosts in the pairing registry's own directory
The remote preview reader looked environments up under the canonical
user-data path, while the pairing IPC writes the registry under
app.getPath('userData'), which can differ in casing on case-sensitive
installs. Both now use one getter. A chunk-read verdict is cached only
when a pairing revision pins it, so it can't outlive a re-pair.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
* fix(files): read the pairing registry from one directory everywhere
Doc previews resolved paired servers under the canonical user-data path,
the same casing mismatch the media reader had. They and every other main
caller that names the registry directory inline now use
getRuntimeEnvironmentRegistryRoot(), so it has one source.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
* fix(files): pin an unpinned preview grant to the pairing it was minted on
A renderer that hadn't loaded the environment's pairing revision yet sent
none, and every later chunk read went unguarded, so a re-pair under the
same environment id would let the old URL read from the new host. Main now
pins the registered revision at mint.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
* refactor(files): route the remaining pairing-registry readers through the one getter
AI-vault host scans, the settings runtime-environment preference, skill
capability checks and the mobile relay host list named the registry
directory inline. They now use getRuntimeEnvironmentRegistryRoot() too;
callers that also use the directory for other storage keep their own path.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
* refactor(files): write new pairings through the same registry root
The ephemeral VM pairing writer named the registry directory inline; it now
uses getRuntimeEnvironmentRegistryRoot() like every reader.
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
---------
Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A body:has() rule with a descendant subject (the modal blur and toast rules) made
Chromium invalidate the whole document's style on every DOM mutation anywhere in
the app. A workspace switch paid two full-document recalcs (~1200 elements each,
~9 ms). A childList-only MutationObserver on body now mirrors the overlays into
data-modal-overlay-open / data-onboarding-overlay-open and the rules key on those.
Co-authored-by: m4air <m4air@Mac.localdomain>
* perf(wsl): hold the distro alongside the read-only context probes
The kernel probe has already started the distro, so the holder now runs alongside the four
read-only context reads instead of before them; it is still held before recovery, which may
launch orcad. No new wsl.exe spawns. The WSL 1 check and the parallel hold move to
wsl-orcad-held-context.ts.
# Conflicts:
# src/main/wsl/wsl-orcad-deploy.ts
* fix(wsl): stop the context probes once the distro hold fails
A failed hold left the context probes running; after the deploy disposes its transport they fall
back to fresh wsl.exe execs, which could boot a distro that just went away (E9). The probes now
run under a child signal of the connect's, aborted on a hold failure or any other rejection.
* test(wsl): cover the context probes running while the hold is pending
Moves the ordering check out of the deploy suite, which is at its line cap after main's new
refusal tests, into the held-context suite.
A distro restart reconnects with the same active version and daemon, so the ~9 guest commands of
version GC find nothing new. GC now reports whether it left any stale version dir behind; the WSL
deploy skips it for the rest of the app session once a pass under the same active version and
daemon version came back clean. SSH callers ignore the new return value.
After the three measured restarts, the idle-release lane runs one more, unrecorded restart with
orcad launched under --cpu-prof (through the lane's own launch env), waits for its idle exit and
copies the profiles of orcad and the Node children it started into the receipts.
* fix(wsl-host): Remove keeps a WSL host's server that another Orca on this computer shares
A WSL host's server lives in the Linux login's ~/.orca, so two Orca installs on
one Windows account (stable and dev), or a desktop reaching the distro over SSH,
share one server. Remove used to close every terminal there and decommission
it, ending the other desktop's sessions and agents.
The server now answers host.peerDesktops (capability orcad.peer-desktops.v1):
the other desktops paired with it, whether each is connected now, and when each
was last seen. Before tearing anything down, Remove asks. If another desktop is
connected or was seen in the last 30 days, it only closes this desktop's bridge
and reports the server kept. If the server can't answer, the teardown is
refused. An older server without the capability gets today's teardown, plus a
warning.
* fix(wsl-host): a shared Remove releases this Orca from the host, and says the host keeps running
- On a shared server, Remove now revokes this desktop's own device (and its
phones) on the host, through the new host.releaseDesktop, before letting go.
Otherwise its recent row would make a peer's later Remove keep the server
forever. Peers also skip rows of the caller's own installation (same caller
fingerprint), such as an enrollment from before a re-add.
- The renderer says when a removal kept the host for another Orca, and the
Remove confirmation no longer promises to end terminals unconditionally.
- Remove-path files now route CI to the fake transport e2e spec, which
exercises Remove.
* i18n(wsl-host): Russian and Traditional Chinese for the shared-host Remove copy
* fix(wsl-host): tell desktops apart by device key, and release this Orca on every Remove
- Peers are no longer hidden by caller fingerprint. Every device-key fork of
one pairing token row inherits that row's fingerprint, so two installations
that paired through one code hid each other, and one's Remove could tear the
server down under the other. Only a shared device public key now marks the
same installation, as with a re-add under a new token.
- This desktop's own device is released as the last call over the bridge on
every Remove that proceeds (no terminals left), not only a shared one. A
later Add then Remove never meets a stale row of ours.
- When the release fails on a shared Remove, the toast says the host may keep
running for a while. The Russian confirmation copy is corrected.
* fix(wsl-host): move undo refuses a removed host whose server another Orca kept
A Remove that kept a WSL host's server for another Orca (R39) now records
that. Before, undo of a move onto that host read the missing environment as
"its terminals and automations stopped with it", and would have restored the
project here while the host's copies kept running, duplicating its agents and
automations. Undo now reads such a host as unreachable and refuses, as it does
for any host it can't reach. A later Remove that proves the server stopped
clears the record. If the record can't be saved, the host stays listed as
pending instead.
* i18n(wsl-host): Turkish for the shared-host Remove copy
* fix(wsl-host): each WSL desktop splits off its own row from orcad's startup pairing
orcad publishes one startup pairing offer and never rotates it, so every
desktop that enrolled through it shared one token row and device id. Stable and
dev Orca on one distro were invisible to each other as peers, and releasing that
shared row broke the next re-add and pairing repair.
- New host.forkDesktop (same orcad.peer-desktops.v1 capability). A runtime
device on a shared desktop token mints its own token row, forked from it and
named by the caller. Forks share the device-key fork cap, and a released fork
frees its slot.
- The WSL desktop forks right after enrolling through the startup offer, or
after a repair lands back on it, and keeps the forked pairing (named after its
userData folder, e.g. Orca or Orca Dev). On an older orcad, or if the fork
fails, it keeps the shared pairing and warns.
- A token row that desktops forked from counts as a peer only while something
is connected on it. One that nobody forked from keeps the last-seen rule.
* fix(wsl-host): keep host.forkDesktop to the startup offer's holder, over E2EE, with a clean label
host.forkDesktop mints a credential, so:
- only the holder of the desktop pairing offer this server advertised last
(orcad's startup offer) may fork; an older offer, a fork, a phone or any
other row is refused;
- the fork takes exactly the caller's grants and reach; params are just a label
(strict, so a grant request is refused);
- it runs only on an authenticated E2EE socket; the token goes back only in
that reply and is never logged;
- the label is display-only: control, zero-width and bidi characters stripped,
whitespace collapsed, at most 64 characters; an empty result is refused.
* refactor(wsl-host): count only Orcas connected right now as WSL host peers; drop fork and release
Review found the token fork and self-release add more identity state than R39
warrants: orphan forks, a desktop left on the shared token releasing it, and a
changed caller fingerprint. So this drops host.forkDesktop, host.releaseDesktop
and the 30-day last-seen rule, and restores the restart test.
A peer is now another Orca installation with a control socket open on the
server right now. The desktop declares its installation (a hash of its userData
path) with host.declareClient on its shared-control socket after every ready,
including reconnects. The server tags that socket and drops the tag when it
closes. host.peerDesktops takes the caller's installation id, so its own other
sockets (multiplex, one-shot) never count, and two desktops sharing one startup
token stay distinct. Undeclared sockets, phones and browser sessions are
ignored. Peer names go through sanitizeDeviceLabel, and a startup-offer
"CLI <date>" name shows as "another Orca". An older orcad lacks the method:
nothing is declared, and Remove keeps today's teardown with a warning.
Limit: an Orca that's closed while its terminals keep running isn't protected.
* fix(wsl-host): keep a reconnecting Orca as a WSL host peer for 60 seconds
Lost contact isn't proof an Orca left. When an installation's last declared
control socket closes (a bridge exit, an orcad restart, resume from sleep,
reconnect backoff), the server keeps counting it as a peer for 60 seconds, or
until it re-declares. A Remove inside that gap no longer tears down its
terminals.
* fix(wsl-host): say an open Orca keeps the host, and name each peer once
- The Remove confirmation now reads "Unless another Orca that's open on this
computer is using it", since only a connected Orca keeps the server.
Updated in all 10 locales.
- The kept-host toast dedupes peer names, so several unnamed peers read
"another Orca" once.
* test(wsl-host): pin the reconnect grace to the last socket's close, and to the server's own forget
- The unit test advances the clock between the two closes and checks that the
peer still counts when the first close's grace would have run out.
- The integration test waits until the server has dropped the closed socket
before reading peers, so it fails without the grace (checked by setting the
grace to zero).
* fix(wsl-host): sweep expired reconnect graces on every close, on a monotonic clock
Expired graces are dropped at the start of forget() as well as peers(), so the
map stays bounded even when nobody asks for peers. The default clock is
performance.now(), so a wall-clock jump can't stretch or cut the grace.
* i18n(wsl-host): German for the shared-host Remove copy
* fix(agent-session): a chat whose working folder was deleted says so, not that the CLI is missing
Node reports a spawn whose cwd is gone as ENOENT naming the command, and the supervisor's
report names no path, so a deleted worktree read as a missing agent CLI. On a spawn ENOENT,
one stat of the cwd the spawn was given now tells the two apart, and the chat reports the
existing launchFolderMissing reason.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agent-session): a missing supervisor binary is never reported as a missing agent CLI
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(wsl-host): tell users how to fix a distro Orca can't host
With the WSL bridge going away there is no fallback for a distro Orca's server can't
run in, so each refusal now names the step that fixes it:
- WSL 1: the wsl --set-version command, also shown in the Add host form, where the
disabled option could never show it.
- An unregistered distro, at Add and at connect: how to list installed distros.
- NixOS's stub loader: turn on nix-ld.
- glibc below every runtime's floor: the minimum release to use.
- A WSL distro with an unknown platform no longer reads as "this SSH host".
Connect refusals other than WSL 1 report as 'unsupported-distro' instead of reusing
'unsupported-wsl1'.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(wsl-host): show only the plain message when a WSL host can't connect or be added
* test(wsl-host): fold the plain-message case into the existing readIpcErrorDetail tests
* fix(wsl-host): keep SSH's old-glibc and NixOS refusals as they were; add the fix only for a WSL distro
The glibc and nix-ld hints were appended in code SSH hosts share, so SSH users
saw new wording with the WSL hosts gate closed. The shared messages are back to
main's text; the WSL add flow adds the fix to its own refusal.
(cherry picked from commit c7a07b61c440e87652ca956570b8b91492c2a375)
* refactor(wsl): map a missing distro in the unsupported-distro module, keeping orcad deploy under its line cap
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
The real-distro lane now times each restart's ensure by phase (first exec and boot, holder,
pre-launch checks, launch, orcad startup, post-readiness, bridge, pairing) from wrapper marks and
guest-command start times. orcad logs its own startup phases to its slot log only when the
lane's launch env sets ORCA_E2E_ORCAD_STARTUP_TIMING; the lane reads that line into the receipt.
A probe that failed while the SSH connection was down left "Couldn't list terminal agents"
until the window remounted: the disconnect clears the list, and nothing re-ran detection when
the connection returned. The hook now follows the target's connected state, as #33816 did for
runtime hosts, and re-lists on the return to connected only.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): keep Codex history across WSL account switches
Bridge the distro's system home and every sibling managed WSL account home in
the same distro into the selected account's home at launch, so a session
started on account A is resumable after switching to account B (#18486).
Split from #33578 (WSL half only).
* fix(codex): bridge WSL siblings only into a selected account home, one loop per home
System default's own ~/.codex in the distro gets no sibling sources, so the
#24511 product decision stays out of this change. Concurrent launches join the
loop already bridging a target home. Restores #16854's drain-target assertion
with the launch bridge mocked.
* feat: make WSL host terminal agent launches consistent
On a WSL host (orcad inside WSL with ORCA_WSL_ORCAD_HOST), interactive
terminal agent launches now check if the agent is Windows-only before
typing the command. If only a Windows copy exists, show the same error
message as other spawn paths: "installed only on Windows, not in this
WSL distro." This ensures terminal launches have feature parity with
chat and background agent spawns added in PR #34590.
When an agent is found as an absolute path in the WSL distro, terminal
launches prefer that resolved path over the bare command name, improving
consistency across launch paths.
Desktop, native Linux, macOS, and SSH terminal launches are unchanged —
WSL host path resolution only applies when isWslHostOrcadProcess() returns
true.
Test coverage: 9 tests verify desktop/Linux/macOS/SSH launches remain
unchanged, and WSL host resolution behavior (Windows-only detection,
absolute path preference, bare command fallback).
* fix(wsl-host): refuse Windows-only agent CLIs in terminal launches without touching the terminal env
* fix(wsl-host): launch a native alias binary before refusing a Windows-only agent
* fix(wsl-host): refuse wrapper launches whose required CLI is Windows-only
---------
Co-authored-by: bench <bench@example.invalid>
Co-authored-by: Neil <neil@stably.ai>
Daemon log lines used appendFileSync per event and the trace sink used
writeSync, so a disk or writeback stall froze the daemon and orcad together.
Both now go through one bounded writer: lines queue in memory, one async drain
writes a batch at a time and is the only code that rotates, a full queue drops
its oldest lines behind a count marker, and flush() waits only for its own
lines. Crash breadcrumbs and exits append the remaining queue synchronously.
Orderly quit waits up to 500 ms for the async flush.
Refs #34410
Refs #33870
* test(wsl): measure reboot to first prompt for the WSL host and the old bridge
The real-WSL lane's phase 3 now terminates its distro and times, median of 3 each, how long a
WSL-host project takes to a working terminal (holder boot, orcad start, terminal spawn and an
answered command) against one wsl.exe into the login shell, as the old bridge does. Test-only.
* test(wsl): relabel the restart baseline honestly and raise the WSL2 job cap
The baseline measures distro restart (VM warm) to first command output; the old-bridge figure is a
non-PTY wsl.exe --exec lower bound, not ConPTY first paint. The job cap rises to 40 minutes so it
no longer undercuts phase 3's own 30-minute timeout.
* fix(wsl-mover): ask before quitting while a WSL project move runs
A plain quit (Cmd/Ctrl+Q, closing the window) during a move gave no warning: the next launch
recovers safely, but the move is lost. While main reports a move running, the window's close
guards now ask "A project is moving to its WSL host. Quit anyway? The move will be undone at next
launch." Behind the internal WSL hosts gate: the guard registers only with it open, and main
answers false with it closed. An intentional restart and an unanswered question never prompt.
* fix(wsl-mover): the quit prompt's running check gives up after 2 s
The close request is acknowledged before the guards run, so a hung wslMove:isRunning would block
quitting for good. The guard now treats no answer within 2 s as no move running, like a failed
ask.
* test(wsl-mover): give the flag-off host test the confirmation dialog the quit guard reads
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(wsl-move): undo connects the WSL host after a relaunch, and startup heals a crash mid-pairing save
Undo is the user's act, so it connects the host the way Connect would before asking it anything;
without that, a relaunched host with no open work refused every undo as unreachable.
A crash between refreshWslHostPairing's two writes leaves the record's bridge port off the saved
pairing, and the overlay then drops the WSL link. Startup realigns the record to the pairing.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(wsl-hosts): startup realigns bridge ports before any bridge connects, and not while gated off
Co-Authored-By: Claude <noreply@anthropic.com>
* test(wsl-move): undo's terminal stop never connects a host whose Remove is pending
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(i18n): add German UI language
Register German (de) as a built-in UI language alongside en, zh, zh-Hant,
ko, ja, es, fr, pt, ru and tr: language setting, system-locale detection
(de, de-DE, de-AT, de-CH, de_DE.UTF-8), renderer and main-process lazy
catalogs, Settings picker label in every catalog, the "Sprache" settings
search keyword, onboarding picker, docs and a German glossary and style
guide (config/i18n-german.md).
de.json covers every en.json key except the sparse-policy entries that must
fall back to English (status-bar credit balance copy and the incomplete goal
change notice). Tests that used de-DE as the unsupported system language now
use it-IT.
The catalog is machine-translated, reviewed by two independent passes with
arbitrated fixes, and swept for terminology consistency.
* feat(i18n): translate German strings added on main
Translate the 158 keys main added since the branch point (WSL project move,
diff size limits and others), reviewed by two passes like the rest of the
catalog, and drop the 5 keys main retired.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix: release dictation when renderer documents retire
* fix(speech): give each renderer document its own dictation owner
A reload during a pending local startup let the successor document's first
run reuse the retired owner key and silently join the canceled startup.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* feat(wsl): say WSL hosts are paused in serve-only
Settings → WSL hosts explains that saved hosts and their projects keep working
while adding hosts and moving projects is paused, and the add-host menu says
where Add WSL distro went. Shown only after main answers serve-only, so open
and closed gates never show it.
* style(wsl): use the text-2xs token for the paused add-host line
* test(wsl): give the move status-line test's hosts mock the paused hook
The spy already retried its install through retryTransientMainEvaluate, but its
reads, clear and delay setter did not, so a 'Resulting promise was garbage
collected' (playwright#33737) failed a passing Windows WSL paste test.
* fix(i18n): drop queued-message keys #31180 removed from en
#31180 removed components.native-chat.queuedMessages.cancelEditing, .pasteOrDropImages and
.saveNeedsContent from en.json; tr.json, added since, still carried them, so the locale catalog
check failed on main. They are the only stray keys in any locale.
* fix(lint): split runtime-file-read-client under the max-lines cap
Move the remote chunked-download helpers into runtime-remote-file-download.ts so
runtime-file-read-client.ts is back under the 300-line lint cap. No behaviour change.
* fix(terminal): preserve Codex exit queries across snapshot restore
* fix(terminal): replay owed queries from discarded output and superseded replays
- Mark yielded queries discarded when ACK overflow trimming, the recovery
cutoff, or snapshot-buffer overflow drops their output unsent, and replay
them after the covering snapshot (including the ACK recovery snapshot).
- Purge snapshot query replays and escape-tail frames still queued behind the
ACK window when a newer snapshot is published, and put new replays at the
head of the ACK queue so they follow their snapshot.
- Share isPrimaryDeviceAttributesQuerySequence across DA1 call sites.
- Move replayed-query settlement into settleReplayedYieldedQuery.
* fix(terminal): replay DA1 still batched when an ACK recovery snapshot covers it
* test(terminal): type the overflowed initial-snapshot stream fixture
* test(terminal): give the host-chain recovery stream fixture its required query and batcher state
#34650 removed dashboardPopout.terminal.closed from en.json, but ru.json,
tr.json and zh-Hant.json kept it, so verify:localization-catalogs fails on
main ("extra: dashboardPopout.terminal.closed"). Drop it from those three. No
other locale has extra keys; ru's _few/_many plural forms are expected.
* feat(wsl): admit no new WSL host or move in serve-only
Main refuses wslHosts:add while the internal gate is closed and reports
acceptsNewWork in the WSL host capability; moves already need the open gate.
The renderer hides Add WSL distro and every Move entry point once main says
serve-only, while saved hosts keep their Connect and Remove.
* fix(wsl): offer no new WSL host or move until main answers the capability
The renderer's acceptsNewWork now starts false, so Add WSL distro and the Move
entries never flash in serve-only before the capability reply.
* fix(wsl): point the WSL-bridge chat refusal at Add WSL distro only when it is offered
In serve-only Add WSL distro is hidden, so the refusal keeps its terminal-only
words there.
* test(wsl): give the move status-line test's hosts mock the new-work hook
#34676's test mocks use-wsl-hosts-enabled; WslHostsSection now also reads
useWslHostsAcceptNewWork, which follows the same gate there.
* perf(browser): throttle idle guests between active operations
* perf(browser): lease only the guest for non-capture agent commands
Keep the renderer paint hold, host unthrottle, and host re-hide readback on
capture paths only, and never let the one-way paint-hint send fail the capture.
* style: format bridge paint-hold test
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(native-chat): open sandbox: images and files on the chat's host
Agents sometimes cite files they wrote as sandbox:/abs/path. Markdown
sanitizing dropped those addresses, so the chat showed only alt text and
an inert link. Chat Markdown now rewrites sandbox: links and sandbox:/file:
images to the chat's own file hrefs before sanitizing, and the shared href
router reads sandbox:/abs as the absolute path on the chat's host.
A paired host now grants a chat client a read-only exact-path grant for
any host file the chat links, instead of searching the chat transcript for
the path. The search missed structured chats, Markdown link targets and
older replies, and guarded nothing: a paired client can already run
commands on that host. The grant still refuses network shares, device
paths, hardlinks, pipes and sockets.
Desktop chat links and images now send their chat to the host, so files
outside every workspace on a paired host open and render. A chat link the
host does not have is reported missing there and never read from this
computer.
* Fix chat file reference and granted file actions
* Keep granted chat files on existing read-only tab route
* Remove accidental package manager lock metadata
* Match WSL artifact descriptor fixture to regular file stats
* Fix canonical host image grant acquisition
* Fix host image test transport request typing
* Drop sandbox: address handling from native chat links
sandbox:/mnt/data placeholders belong to a hosted code tool that swaps them
for its own download links; they never name a file on the chat's host.
* fix(wsl-mover): settle an interrupted move automatically, and hold the host's server update during a move
An app update or crash between sending a move's copy and the host's commit leaves a
`blobs-transferred` record. Startup recovery has no host to ask, so the projects stay fenced, and
the automatic mover only previewed, which refuses an in-flight move, retrying forever. The
projects stayed read-only until the user started the move from the dialog. The automatic mover's
preview now settles that record first (through the host: back to zero, or forward when the host
committed), holding the distro's run while it does so it never races a move the user starts. The
dialog's preview stays read-only. The preview moves to its own module.
A Settings Update or Force, or any reconnect, could also redeploy the host's server in the middle
of a move, cutting it off (safe, but the move is lost). Once a running move has journaled, a
version-changing redeploy now keeps the running server and reports the update as waiting for the
move, mirroring the SSH path's skip while migrating. The move's own first connect, before it
journals, can still update an outdated server.
* fix(wsl-mover): an automatic settle never reloads the window, and never holds its own host update
Review of #34823:
- A settle that finds the host committed ran the desktop rewrite, which reloads the window past
its unload veto. From the automatic mover it now records the commit only (the projects stay
fenced), and the next start's rewrite pass finishes it before any window opens. A move the user
starts still finishes at once.
- The server hold counted any unfinished record of the distro, so an automatic settle of an old
record with an outdated server was held, refused for an update, and retried the same way. The
hold now counts only the cutover the running move journaled itself.
* fix(wsl-mover): let the user finish a committed move in-session; park the automatic mover meanwhile
After an automatic settle records a commit for the next start, nothing could finish it in-session:
the user's Move refused as already in flight and the orchestrator retried for nothing.
- A settle that finds the head committed but not yet rewritten on this desktop finishes it now when
the user asked (their Move, or the status line's Retry), returning the existing "finished an
earlier move" answer; the automatic mover still leaves it for the next start.
- The automatic refusal carries detail `finish-pending`, which parks the distro (new reason
`finish-pending`) instead of retrying; the next start, whose rewrite pass finishes the move before
any window opens, puts it back to pending.
- The park rules move to wsl-move-orchestrator-park.ts.
* fix(wsl-mover): give the finish-pending park its own status line
Main's move status line (#34676) names every park reason; the finish-pending reason this PR adds
needs one: the earlier move is done on the host, and finishes here at restart or with Check again.
* fix(native-chat): a refused queued message still blocks the queue when its card write fails
Every reader of a queued card goes through one derived view of the journal's dispatch facts,
so a card whose message was refused reads as returned even when its card-table update and heal
both failed. One decision and one writer store the derived state for every action.
* refactor(native-chat): keep each queued message in the chat's journal, not a second table
A queued message lived in its own SQL table, and a hook that may be skipped copied
each refusal from the journal into it. When that write failed, a refused card
vanished: the cards behind it sent, and its Send and Delete did nothing.
Each queued message is now a `queue` row in the chat's journal beside the
submission that hands it off and the dispatch row that answers it. The reducer
folds the card from those rows, so a refusal returns the card and holds the
cards behind it with nothing written, and replay reads the same. Every action
writes its own rows in one transaction. Cards an earlier build stored are read
through the same classification until they move: each action moves its own
card, and a bulk import runs once per open. A new epoch restates every card
still owed an answer in the transaction that deletes the old rows.
The settlement hook and its savepoint, the open-time heal, the drain's heal and
the derived-card view are gone. The published card list is unchanged.
* test(native-chat): adapt the queue tests to cards kept in the journal
Cards are written through the journal, so tests queue them with their body
alone and hand them off under their real fingerprint. Tests of the removed
settlement hook, heal and SQL store are deleted; the text-update spec now
covers the edit decision alone, and a test of an earlier build's stored holds
seeds the legacy table and reads the imported cards. Rolling over an epoch now
keeps a card whose hand-off was never handed over, so that test checks the
retried id never sends rather than that nothing sends.
* test(native-chat): a refused card holds the queue through rewind, a new epoch and a restart
One whole-flow test through the host: the refusal writes no card row, a rewind
that retires the refused hand-off and a rolled-over epoch keep the card
returned, a restart and Resume send nothing behind it, its own Send refused
again still holds the queue, and its Delete releases the next card in order.
* test(native-chat): an earlier build's queued cards move into the journal once, each read the same
One mixed-import test through the host. The first open's import fails and
moves nothing, yet every card reads as it will; Delete and Send each move only
their own card. The next open moves the rest in one transaction, writing a
refusal only where its hand-off is gone, nothing for answered cards, and leaving
an unreadable row on disk. A later command turn spends a moved command card, a
third open writes nothing, an old Delete replays from its receipt, and a chat
with only answered legacy rows clears them in a delete-only transaction.
* test(native-chat): adapt typechecked tests to cards kept in the journal
The row-kind table lists a queue row, the clear and legacy-import tests
match the current signatures, and the downgrade test keeps only the
leftover-send case: a queued card is now a newer-version journal row an
older build keeps read-only, and users don't downgrade.
* refactor(native-chat): drop the journal row writer's unused transaction hook
No caller passes one since the queue's savepoint bookkeeping left.
* test(native-chat): downgrade probes write rows at the version older builds read
The highest version this build reads is now 4, the queue row's. A newer
build's row in these probes is written at version 3 so the older build
meets its kind, not a newer version, and the Stop-event probe hands the
older build the rows as stored, since a read raises each row's version.
* test(native-chat): unit tests follow the queue row's version 4
The legacy-import test opens real SQLite, so it runs in the Node runtime
project. The newest version this build reads is now 4, and a read raises a
row's version to it, so the turn-lifecycle test no longer pins the
in-memory version (the written one stays pinned by journalRowSchemaVersion).
* fix(native-chat): one batch echoing two refused queued messages spends both
The per-row delivery check returned only the first waiting card an echo
proved delivered, so when one settlement carried echoes for two cards the
second stayed waiting and was sent again. It now returns every card the
row proves delivered, and the fold withdraws each; replay folds the same.
* fix(native-chat): a rewind moves an earlier build's cards into the journal first
While the open's import had failed, an earlier build's card that read as
refused through its hand-off lost that reading when an in-place rewind
retired the hand-off: it disappeared and the card behind it sent. The
rewind now adopts every legacy card ahead of its tombstones and deletes
their rows in its own transaction, as an epoch replacement already does,
so the fold latches each card's reading before its hand-off is gone.
* fix(native-chat): keep an earlier build's card whose hand-off is still unconfirmed
The import dropped a legacy card handed off as a send whose delivery was
unknown, though the fold keeps such a card linked and a later refusal
returns it. The import now keeps every card whose hand-off a later row can
still answer, through the same check the reducer folds dispatch rows by.
* fix(native-chat): one delivery echo withdraws at most one queued card
One provider echo proves one delivery, yet it withdrew every waiting card
with the same text whose hand-off was refused, so a second identical card
was lost. The fold now pairs each echo with one card: on its own row the
earliest-positioned match no other echo of that row took, and for a card
that waits again the earliest applied echo no card has taken. Paired echoes
live in the fold's state, so live and replay pair the same; readers of
cards not yet imported work on a copy.
* fix(native-chat): a queued message handed to the agent is sent again only on proof it never got it
Pressing Stop after Orca handed a queued or steered message to the agent,
before the agent confirmed it, put the card back in the queue, and Resume
or the next send sent it again. A check matching the agent's echo of the
text was the only guard against a double send, and it kept trading a
resend for a lost card. Whether the card waited again was decided by the
rejection's category, not by proof the agent never got the message.
A refused hand-off now waits again only where Orca proves the agent never
received it: the host still held it, or whoever settled it recorded that.
A failure still returns the card and holds the cards behind it. Anything
else may have reached the agent, so the card is spent and its message
reads as sent, derived on every fold from the stored rejection: with the
turn it was answered into, as that turn's unconfirmed message where it was
handed over; withdrawn before any turn opened, as a person's own send a
Stop took back. The reopen mark ignores a recovered doubt, and the restart
reconciler no longer judges a queued hand-off unsent from what history
lacks. The echo text matching and its pairing are deleted.
A close or quit while the agent starts settles sends with the unrun hold.
The host hands nothing to a child before its start answers
(structured-agent-session-delivery-loop.ts: a handover needs phase
ready), so such a send went to an earlier child and may have run: it now
reads as sent instead of waiting again.
* fix(native-chat): Claude's report that it removed a queued message unrun lets the card wait again
Claude says when it removed a send without running it: the interrupt
receipt's cancelled list, or a command_lifecycle cancelled, discarded or
refused frame for a command that never started. Every one settles through
rejectClaudeDispatchWaiters, which now marks the settlement unrun. For a
queued card's hand-off the host records that as proof the agent never got
it, so the card waits again under the queue's pause. A direct send keeps
the plain refusal: there the proof would owe it as stopped history.
* fix(native-chat): an earlier build's card stopped after hand-over follows the same rule on upgrade
Earlier builds put a card back to waiting after any refusal without a
verdict and cleared its link, so a card the agent may already have would
be sent again after the upgrade. The card's own hand-offs in the journal
decide now: unchanged and last sent as one that may have reached the
agent, it is spent on that hand-off and its message reads as sent; edited
or sent again since, it is held for its own Send under a host-only marker
that publishes as a plain hold; with no hand-off in doubt, including one
that never left Orca, it waits as before. The import writes a card spent
on a hand-off in doubt, so its message shows.
* fix(native-chat): read a queued row's hand-off before the lookup that uses it
The lookup's callback lost the narrowing of the row's change, which the
mobile test typecheck rejects.
* test(native-chat): a card steered into a turn a Stop ends stays sent in the Stop tests
Three tests pinned the old rule, that a card handed into a running turn
comes back to waiting when a Stop ends it. Under the new rule nothing
proves the agent never got it, so the card is spent: with the turn named,
its message reads as that turn's unconfirmed one; without, as a send the
Stop took back. Neither is ever sent again.
* fix(native-chat): a queued message stopped before Orca handed it to the agent waits again
A Stop that aborts a hand-off after its row is written but before the agent call now records
the withdrawal as never reaching the agent, for queued hand-offs only, so plan D's rule returns
the card to waiting under the Stop's pause instead of spending it. Direct sends keep the plain
withdrawal (the flag would owe them as stopped history). The agent's own refusal while aborted
is kept rather than overwritten, and an abort thrown during the call stays in doubt.
* fix(native-chat): a queued message stopped while the agent prepares it waits again
The abort signal reaches only an adapter's preparation (its options, the previous turn opening),
never its write, so the signal's own reason thrown back is the adapter saying nothing was
written. The handover now reads that, for queued hand-offs only, as never reaching the agent,
so the card waits again under the Stop's pause. Claude no longer wraps that abort as an option
refusal, which had returned a stopped card as a refused one blocking the queue. A real refusal
keeps its cause and its barrier; any other throw stays in doubt and is never resent.
* test(native-chat): the usage-limit Continue backlog case writes its card through the journal queue
Main's #30396 wrote its backlog card with the stored fingerprint and host-instance stamp the
journal queue derives instead; the card the Continue must yield to is the same waiting card.
* test(claude): the real-CLI queued Stop expects the unrun mark Claude's cancelled-unrun receipt carries
* feat(mobile): edit a queued message's images on the phone: remove or attach
* fix(mobile): gate queued image edits on the host reader verdict
* test(mobile): declare the upload fixture chat mode
* fix(native-chat): retain spent legacy messages when import fails
* fix(mobile): reveal the full queued image editor on open
* fix(mobile): invalidate queued editor geometry without a render ref write
* refactor(chat): prepare composer drafts and measured queue dragging
* feat(native-chat): add conditional queue removal and authoritative reordering
* feat(native-chat): move queued edits into the normal composer on desktop and phone
* fix(native-chat): own complete phone drafts before authoring
* fix: preserve submitted composer drafts across storage replacement
* fix composer validation and mobile CI diagnostics
* fix mobile composer test contracts and diagnostic output
* Capture mobile test compiler diagnostics in CI
* Fix queued message contract typing and English strings
* test: complete queued extraction transport fixtures
* refactor: separate composer skill binding from send mutations
* fix(mobile): grant draft storage within route limits
* fix(native-chat): settle each draft save on its own and publish complete composer drafts
Phone: a queued Edit copy no longer consumes the save that later typing
owns. Each ordinary save and the exact copy settle only their own result;
an explicit save waits for its own pending write, and a failed or retired
write never schedules another or revives the copy.
Desktop: a pasted-text leaf no longer shifts which selected file binds
where, and a hand-back held during an IME composition reaches the store
only merged with the visible editor document, including writes the editor
admits without an update and the composition settle.
* fix(native-chat): import the draft type once in the draft cache
* test(mobile): type the held-write state without an assertion
* test(native-chat): assert the handed-back file on Send without reading a typed transport as a mock
* test(mobile): pass main's agent-confirmed flag to the submitted-files harness
* fix: reveal queued reorder grip on touch screens
* fix: save reopened phone drafts and batch reorder measurements
* test: use an explicit React module type
* Preserve queued message sortable identities on unchanged renders
* Prevent stale queued drag completion after unmount
* fix(mobile): hand off navigation across session workspaces
* test(mobile): use resolved session identity in dictation fixture
* feat: edit queued messages inline with the chat composer
* fix: normalize queued edit skill ownership
* fix: retire phone skill selections with the sent draft
* fix: consolidate queued edit module imports
* fix: separate edit save action and stabilize image callbacks
* fix: complete phone inline composer integration
* fix: retain same-name queued skill selections
* fix: complete desktop queued composer integration
* test: use typed phone composer host lookups
* fix: include queued edit localization keys
* Preserve inline edit input across save and attachment completion
* Preserve complete queued edits across delayed saves
* Preserve queued edit input across delayed completion
* Complete queued edit lease loss and pending cancellation
* Use recognized queued edit test fixtures
* Consolidate queued edit imports and type host test spy
* Print mobile test compiler diagnostics on ratchet failure
* Correct queued edit integration fixture types
* fix: keep queued edit placeholder independent of readiness
* Fix queued edit compiler fixture boundaries
* Align upload completion test with ordinary insertion
* Combine mobile composer draft imports
* Load queued edit host fixture during test collection
* Size inline phone queue editor to available space
* Use typed scroll components in phone presentation contract
* Save typed text in a queued edit as written
The inline editor is the chat composer, so it keeps the composer's own identity model and no longer refuses Save for text that looks like a command. Command rows picked from the menu are still refused, Enter still means Save, and Edit never sends or runs anything.
Regression tests cover Undo after deleting an accepted command or skill pill, a scanned skill named like a command, two saved files behind one token, and typed command text saving literally. Also fixes two merge leftovers in tests: a missing import and a fixture that stored the whole menu row as a skill.
* Keep ordinary sends on the main composer contract
* Fix mobile composer imports and fixture loading
* Consolidate queued edit imports and duplicate regression
* Restore ordinary send catalog short circuit and repair test boundaries
* Preserve returned rich drafts through send acceptance
* Remove obsolete queued editor strings from new locales
Follow-ups from the large-file audit (E7):
- files.search had no byte budget: a result past the remote reply cap failed outright with
reply_too_large. It now keeps the whole files that fit and marks the result truncated, as
files.listAll and searchPaths already do. Local callers stay uncapped.
- A diff side past the 10 MiB read cap came back as binary, so the diff said "Binary file
changed" for a large text file. Blob reads mark it `tooLarge`, the binary diff result carries an
optional `tooLarge`, and both diff views say "File too large to diff". Older clients ignore the
field and keep the binary view. buildDiffResult builds its binary sides without a cast.
- The real-WSL lane pages a 100 MB file over the plain distro's shared control socket in the
editor's 384 KiB chunks and records the time, chunk count and orcad's VmHWM. Record-only: it
asserts nothing, and a failure is recorded in the receipt.
The terminal context menu only showed a close item when the tab had split
panes. A single-pane terminal now offers "Close tab", which sends the same
close request as Cmd/Ctrl+W. Pinned tabs and one-off command terminals keep
hiding it, matching the pane header's close button via a shared
canCloseTerminalPane/terminalPaneCloseLabel.
Fixes#20598
Co-authored-by: Neil <neil@stably.ai>
Fold every forge SSH host alias (ssh.github.com, altssh.gitlab.com,
ssh.bitbucket.org) through one shared normalizeForgeWebHost so main-process
provider detection and renderer manual review links agree. Remotes on
ssh.bitbucket.org now open bitbucket.org pull request forms instead of
ssh.bitbucket.org URLs, and Gitea no longer claims the host.
Co-authored-by: Eric Junker <4758+ejunker@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(wsl-mover): a moved project's chat opens and answers here; a fenced terminal names its host
New e2e: a moved project's existing chat still answers after the move, run by this computer's
agent, with no host involved. Writing it surfaced two product gaps:
- Opening the chat from the moved project's sidebar entry asked its composer for focus before
the prompt editor mounted; tiptap threw and the terminal workbench crashed. The editor handle
now focuses (and answers contains) only once its view is mounted.
- A new terminal in a project its WSL host now owns (one a chat's agent might ask for) was
refused with "is moving ... try again when the move finishes". It now names the host and the
way back: use the project on this computer again.
* test(wsl-mover): wait for the moved chat's composer before typing and sending
Like #34573: the composer must be editable before typing, and hold the typed text before Send.