* 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>
* 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>
* 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
* 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.
* 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
* 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.
* 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.
* 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
* [CI] Retire the weak local title-corpus comparison
* [CI] Retire tests for seven unconsumed helper modules
* [CI] Reset the Windows launch resolver graph once per group
The ready toast added in #28238 read the Active Server setting through
getKnownExecutionHostIdForWorktree, which the focus-read ratchet rejects,
so every PR's static-analysis job failed on main.
* fix(terminal): preserve preparation and discovery for delayed agent launches
* refactor(agents): centralize launch intent recognition
* refactor(agents): keep launch intent in one module apart from process recognition
Move recognizeAgentLaunchCommand next to the sleep-prefix scanner so the strict
process-table matcher is untouched, name the module away from the agent-launch
funnel files, share one executable-name normalizer, and colocate the
preparation tests with their modules.
* fix(terminal): name a new tab's first pane so its saved layout is never empty
createTab now always mints the first leaf id, so the window never saves a
terminal tab with an empty layout root. With no empty roots coming from the
window, main's save-time swap that kept its own root over an empty window
root (from #1909) has nothing left to guard, and is deleted.
* test(persistence): an empty-root layout from an old session still loads and gets its pane
* fix(persistence): name an old session's empty layout root once at load
A session saved before tabs named their first pane can hold an unmounted tab
with an empty root. If main bound a terminal there, the window's next save of
that tab dropped the binding once the save-time swap was gone. Name the pane at
load instead (the same pane the window would mount), so no save carries an
empty root. The rootless-pane resolver moves to shared so main and the window
pick the same pane.
* test(persistence): name the bind-then-stale-write order the load repair covers
* fix(persistence): name an old empty tab's pane with the stable headless leaf id
The load repair fell back to a random id, so an unsaved headless serve gave a
phone a new tab::leaf id on every restart. Use deriveHeadlessLegacyTerminalLeafId,
which phones already saw for these tabs. Also import the rootless resolver from
shared directly and return the loaded state unchanged when nothing needs repair.
* fix(floating-workspace): reveal workspaces where they live instead of selecting the floating one
Every renderer path that takes the user to a workspace hand-wrote the main-window
selection sequence, so a floating workspace id (terminal switch, CLI focus, agent
launch, notification and mobile reveals, terminal handle links) became the main
window's selection and blanked it while the floating panel stayed closed.
revealWorkspaceSurface is now the one way to show a workspace: the floating
workspace opens its panel and leaves the main window alone, any other workspace
becomes the main window's selection and a visit. selectWorkspaceSurfaceOwner is
its non-navigating twin for tabs opening beside the user. The ad-hoc floating
checks in worktree activation, http links, the editor close queue, Activity and
open-terminal-beside-tab now go through the same module.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(notifications): bind clicks for floating and folder workspace terminals
A terminal notification only bound its click when the worktree id carried a
repo ("repoId::path"), so floating-workspace and folder-workspace terminal
notifications did nothing when clicked. ui:focusTerminal now reveals any
workspace where it lives, so the click binds whenever there is a pane to focus
and ui:activateWorktree is sent only when a repo exists.
Co-authored-by: YuriNachos <19365375+YuriNachos@users.noreply.github.com>
* refactor(floating-workspace): tighten the workspace surface reveal
Move the floating-owned check into floating-workspace-panel-reveal as
revealFloatingWorkspacePanelFor so worktree activation no longer depends on
workspace-surface-reveal, rename selectWorkspaceSurfaceOwner to
selectWorkspaceSurface with a named state type, reuse the panel visibility
selector, and flatten the IPC bridges' surface-owner branches.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(floating-workspace): leave the floating setting alone before settings hydrate
Host reveals (CLI focus, mobile, notification clicks) can arrive before
settings load; re-enabling the floating feature then would overwrite a
saved `false`. Unhydrated settings now skip the reveal.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* refactor(floating-workspace): drop extra reveal concepts
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(floating-workspace): keep main's setActiveWorktree call shape for plain reveals
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
---------
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
Co-authored-by: YuriNachos <19365375+YuriNachos@users.noreply.github.com>
* fix(agent-launch): after a restart a launch pane is an ordinary pane, never "couldn't confirm"
A launch pane's spawn read the launch record (and the tab's saved outcome) when no launch was
running for it, so a row left pending by a quit showed "Couldn't confirm the agent started." The
record never observes the process; the terminal host does. With no launch running in this process
the pane now spawns exactly like any tab: a live agent reattaches, an exited or absent one gives a
fresh shell, and a gone daemon shows the normal unavailable state.
- the running launch tells its pane how it ended in memory (finish carries the verdict); only a
definite failure says "couldn't start", an unknown outcome leaves an ordinary pane
- deleted: agentLaunchPaneVerdictFromRecord, listAgentSessionOperationRowsOwningPane, the
`unconfirmed` verdict and its notice string, the tab's persisted launch pane, ownedPane writes
- rows written by an older build that carry ownedPane are ignored; they expire with the row
* test(agent-launch): the restart pane test joins the attach suite it shares mocks with
* fix(agent-launch): a launch pane whose agent couldn't start keeps its notice for the session and never spawns a shell under it; a reload saves only a pending launch
* test(agent-launch): drop assertions on code that is gone, and the taking-back case the IPC suite already pins
* fix(agent-launch): narrow the finished-pane check, and test the saved session's launch pane with a typed tab
* fix(agent-launch): a launch whose outcome is unknown still says "Couldn't confirm the agent started" for the session, and attaches its agent if it turns up
* fix(agent-launch): main keeps a failed or unknown launch's answer for its pane until it quits, so the pane never spawns a shell under its notice; the window's spawn gate goes
* fix(agent-launch): a failure kept for a pane goes when the pane is closed during its launch, moved to another tab, or its workspace is removed; comments say what an unknown outcome and a reload now do
The new-chat permission default sat alone above Appearance with its own header.
It is a behaviour choice, so it now renders as the first row of the Behavior
card, laid out like the rows already there. Its search entry and section id are
unchanged, so search and deep links still land on it.
* chore(session): delete three session paths nothing calls any more
- session:set-sync IPC (bridge, type member, web adapter stub); quit uses
app:stage-before-unload-sync
- the session-write persist gate (shouldSchedulePersist /
subscribeToPersistGateOpen); the one production subscriber passes neither
- reconcileDeadSessions and the binding's reconcileIfSessionDead; the live
reconcileIfSessionMissing path stays
Tests that drove these are retargeted to the live paths or removed.
* chore(session): name shouldPersistWorkspaceSession in the deferral comment
* fix(editor): keep a pinned clean sibling open when closing its same-owner twin
Closing a document also closes clean same-owner duplicate records; a record
whose tab is pinned is now left open with its tab.
* fix(editor): ignore setActiveFile for an id with no open file
A stale tab or pane id could make a missing file active and persist it as the
workspace's active file.
* fix(i18n): correct Korean Settings translations
Consolidates the Korean Settings corrections from the community audit
(#27820) into one change. 514 catalog values are corrected: reversed
meanings (session search privacy notice), literal machine glosses
(Custom as 관습, gutter as 배수구, Open as 열려 있는), the terminal
caret named after the Cursor app, and Latin terminal/agent/repo left
inside Korean prose. Every value is pinned in the ko key overrides so a
catalog repair cannot revert it.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): keep the contributor's wording where it was already correct
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): translate dictation shortcut help as complete sentences
* fix(i18n): retain existing dictation copy in other locales
* fix(i18n): translate complete settings count sentences
* fix(i18n): translate Git settings as complete rich sentences
* fix(i18n): clarify fast-forward protection in Korean Git help
* fix(i18n): translate repository hook guidance as complete sentences
* fix(i18n): delete settings fragment keys replaced by full sentences
The four settings sentence rewrites left their old fragment keys in every
catalog, kept alive only by tests asserting the new es/fr/ja/zh sentences
equal the old fragments concatenated. Drop those tests and the 32
now-unreferenced keys from all catalogs and the Korean overrides.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): align Korean dictation, Git, and setup-wait help with the English
Name the dictation modes with the same 토글/길게 누르기 labels as the mode
buttons, drop the Git fast-forward clauses the English does not contain,
and replace the leftover Latin "agent" in the setup-wait option.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): translate the new settings sentences faithfully in es, fr, ja, and zh
The sentence rewrites were assembled from the old machine-translated
fragments, carrying over English plural "s" suffixes (ja/zh), broken
agreement and an inverted meaning (fr), ungrammatical clauses (es),
word-salad word order (ja/zh), and missing spaces before "when one exists".
Retranslate each from the English. Also name zh's Hold dictation mode 按住
instead of 抓住 ("grab") so the button matches its description.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
---------
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): correct Korean Settings translations
Consolidates the Korean Settings corrections from the community audit
(#27820) into one change. 514 catalog values are corrected: reversed
meanings (session search privacy notice), literal machine glosses
(Custom as 관습, gutter as 배수구, Open as 열려 있는), the terminal
caret named after the Cursor app, and Latin terminal/agent/repo left
inside Korean prose. Every value is pinned in the ko key overrides so a
catalog repair cannot revert it.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): keep the contributor's wording where it was already correct
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
---------
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(native-chat): name the resume toast's action and keep it on the outcome's row
* fix(native-chat): give the resume toast's action the toaster's own button look
* feat(chat): read one saved row per subagent
Adds the singular `subagent` journal block (one row per child, revised in
place, with a plain-text twin for older clients) and teaches every reader
to draw it: desktop and phone fold a turn's sibling rows into the existing
"Ran N subagents" row, the client and off-page rosters name children from
their own rows, the reopen pass settles a row a dead host left working,
and the phone and worker-transcript wires bound it. No producer writes the
block yet; that switch lands in the follow-up.
* feat(chat): host writes one saved row per subagent
Claude, Codex and Grok now only report child-work evidence. The host writes each
subagent's row from its child record, so the chat row, strip and sidebar read one
lifecycle. Deletes the shared subagent tracker, the per-agent rosters and their
caps. An accepted Stop settles only the run it was sent to, provisionally, until
that run reports its own ending. Work the host store has no room to record keeps
the running-tasks strip and its Stop instead of reading as idle.
* fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows
- background is an open boolean so a later build's value never fails a chat load
- a helper's own helpers fold under a groupId of their own, so the phone opens them apart
- each subagent's own row rides along with the page window instead of counting as conversation
* fix(chat): subagent row writer reads the live turn once, stop acks survive a failed batch
- the row writer reads the journal's live turn at most once per change and only for a
subagent with no saved, queued or in-flight row; a revision keeps its first scope
- a background stop names each accepted task as its answer lands, so a stop-all batch that
times out or fails part way still ends the runs it was answered for (Claude and Grok)
- the strip's untargeted Stop rule lives in the shared view (offersStopAll); both strips
inline it again; incomplete tracking no longer holds commands or rewind
- clients admit a frame whose only change is incomplete tracking
- drop the Claude stop's dead publish; the real-CLI stop test asserts the run's own ending
- list the writer test with the real SQLite tests
* fix(chat): a late Grok report never revives an ended helper; Codex keeps announced helpers
- Grok: once a helper's run ended, a later progress or repeated spawn sends no live evidence,
so its record and saved row stay ended after the next send retires the record; the test rig
now retires finished records on each turn open, as the host does
- Codex: past the bound, only a child that ran and ended is evicted, so an announced helper
that has not started keeps its record, row, Stop and parent
- cut: Codex parentTurnId (written, never read), the Claude identities' turn-end forwarding
(call the corrections directly), the duplicate Grok outcome map
- update stale comments that described the deleted producers
* test(chat): Claude frames to saved row, kept Codex path and usage rules, agent ingestion exemption
- a captured background Claude helper keeps its saved row working past its spawn result and
the parent turn's end, through the real adapter, host and child records, until its own end
- restore focused tests for the Codex root path, label trim and placeholder, usage before
announcement and parent usage
- the per-session live limit test uses tasks: subagents are exempt from it
* fix(chat): incomplete tracking follows the refused children, and the strip's Stop reaches them
- the host remembers which children the store refused for room, by handle; each leaves when a
later report of it is admitted or it reports its own end, so incomplete clears on a truthful
signal; the provider session's end still clears all, and past 512 a flag holds until then
- a parent row the store refused counts every live child it reports as refused
- with work no record lists, an untargeted strip Stop while no turn runs ends the agent through
the idle sweep's stop; under a running turn it leaves the agent alone
- the background Stop moves into its own module
* fix(chat): the incomplete Stop is offered for every agent and decides inside its step
- the strip offers the untargeted Stop for work no row lists whenever no turn runs, whatever
untargeted stop the provider has (ACP agents, Codex before it proves terminal stops), since the
host reaches it by ending the agent; never under a running turn
- the Stop reads whether work is unlisted and whether a turn runs inside its own step, after the
listed stops land, so a wake-up turn opened meanwhile is never ended
- Codex executions past the bound evict the oldest ended child, else the oldest never started
- test the writer's unnamed-subagent label; qualify the writer's retry comment
* refactor(chat): the subagent row writer joins the views with their records
Drops the shared host-only row projection and its hook-server read: the writer's wiring reads
the views every surface reads and the records they come from, and takes residency and the
ending's basis from the record.
* refactor(chat): Grok subagent evidence reads its ending through the one outcome map
* refactor(chat): a subagent's group entry as one object
* fix(chat): a background Stop with nothing listed asks the agent for no task stops
So an agent with no stop route (an ACP agent the probe found none for) never refuses before the
Stop ends the work no row lists. Tests: the unlisted case is untargeted and reads unlisted work
inside the step; the repeat-press rig lists the task its Stops reach; reflow the writer header.
* test(chat): a nested helper's saved parent; describe how incomplete tracking clears
* fix(chat): a closed session claims no Stop; a helper's row follows its own running work
- a session close Orca proves is loss of contact, not a Stop the CLI accepted: Claude no longer
ends what still ran as stopped, so the host settles it unknown and its row reads unverifiable,
as before this stack; stop-acknowledged comes only from a Stop the CLI accepted
- a helper that finished while work it started still runs is written working, by the strip's
own rule; once that work ends, or the session ends, it reads its own ending again
- the CLI starting a completed helper again under the same spawn call is the helper waking when
its work ends: the decoder reports it as a restart, so its one row revises in place and ends
with its real duration; after any other ending such a start is still read as late
- tests from a captured 2.1.280 frame order; item 4 Stops pin their stop-acknowledged basis
* refactor(chat): drop the unused Stop basis on a child's own ending
Only the acknowledged-Stop edge writes that basis now; reflow the decoder header.
* fix(chat): an older history page keeps a turn's subagent ordinals and row
A page now reaches back to the first sibling of each subagent group it holds part of, at most one more page of conversation, so loading the older page no longer renumbers same-named helpers or moves (and closes) their folded row.
* feat(crash-reporting): name the allocation sites behind a renderer heap high-water mark (STA-3565)
* fix(crash-reporting): refresh the allocation profile on each memory sample so a fast leak still leaves one
* style: oxfmt
* fix(crash-reporting): keep every high-water mark when both surfaces also hold a profile
* fix(crash-reporting): name only bundle and builtin frames, sample only while the heap is near full
Frames outside the window's own bundle become fixed labels, so user-derived names, paths and
content cannot reach a report. Sampling is re-derived from every memory reading (starts at 60%,
stops below 50%, on reload or renderer loss), waits for the sampler to start before reading,
leaves no listeners behind, and gives way to the typing-lag capture script.
* fix(crash-reporting): read the profile on main's own 30 s clock; samples carry their window
A refresh asked for while sampling was still starting is no longer lost, and a hidden window's
throttled samples no longer decide how stale the last profile is. Periodic memory crumbs now carry
rendererSurface, so a restarted session keeps replacing the same retained profile.
* Preserve occupied debuggers during lag capture
* refactor(automations): main provisions an automation workspace's setup and default tabs
An automation's create handed setup and default tabs back to the window, which
launched them through its own background-terminal code. Main now provisions
them through the same provisionWorktreeTerminals the CLI create uses, and
returns neither, so setup runs once and the window's copy is deleted.
* fix(worktree): translate the Setup tab title wherever it is created
Main had no copy of the automation path's translated title, and the window and
CLI creates used the English literal. All three now read worktree.setupTabTitle
through the existing main and renderer translators. The create path's runtime is
required, since every production caller passes it.
* fix(worktree): keep the Setup title in English; runtime code cannot load main's translator
main-i18n imports Electron, which orcad and the runtime must not load. The
title stays the English literal the window and CLI creates already use.
* refactor(worktree): drop the runtime check the required runtime made dead
* fix(terminal): show a host-created setup split in the window that holds its tab (STA-9417)
Main only revealed a split of a saved tab when its copy of the window's graph
listed the source pane. That copy lags the tab the create reveal just added, so
the window never drew the setup split and its first-activation sweep left the
setup terminal with no pane or minted a second tab for it. Reveal unless the
window's graph shows the source pane running a different terminal, and reject
a split into an unknown tab before the window switches workspace.
* refactor(terminal): return only the split-source fields a caller reads
* test(terminal): explain the internals cast in the projected split-source test
A native chat was rendered as a fake terminal tab, so its conversation name
went through terminal agent detection and could replace the chat's provider
once a turn finished (a Codex chat named after Hermes showed Hermes).
SortableTab now takes the agent as a prop: terminal tabs resolve it through
TerminalSortableTab (useTabAgent, unchanged), and native chat tabs pass the
provider recorded on the tab. The chat's session id and provider reach the
session-history menu together instead of riding on a terminal launch hint.
STA-10347
* fix(runtime): stop a project's terminals before the host forgets the project
Removing a project on a runtime host (an SSH-managed or paired server) sent repo.rm first; the
host resolves terminal.stop through the project's workspace rows, which repo.rm deletes, so the
stop failed and the shells kept running on the host. Terminals stop first now. The removal toast
also drops the IPC wrapper from its message.
* fix(runtime): only stop host terminals when the host will accept the project removal
Probe with repo.show (same resolver as repo.rm) so a removal the host rejects, such as an
ambiguous project id, no longer kills the shells first. Also poll through terminal_not_writable
in the paired-client e2e.
* test(e2e): read the host shell pid through sh so non-POSIX login shells work
* fix(mobile): load markdown tabs from any desktop split group and center load-error Retry
The desktop answered markdown.readTab only for tabs in its focused split
group, so a phone opening a markdown tab from another group got
tab_not_found and showed "Couldn't load markdown". Resolve the tab across
all of the worktree's groups.
Load-error states now share one component that centers the message and its
Retry button; the markdown reader's button was pinned to the left.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(renderer): mobile markdown read picks the requested tab across groups
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(renderer): resolve mobile markdown tab to one file id
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* Remove repeated daemon smoke builds and idle compiler slots
* Test compiler scheduling through its process interface
* Keep preview component below the existing line limit with a type-only split
HtmlDocPreview.tsx reached 403 lines on main (#27608 + #27875), over the
400-line limit for .tsx, so lint and the pre-commit hook fail on any
branch that merges main. The focus handoff effect moves unchanged into
use-doc-preview-guest-focus.ts.
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(browser): annotation card and outline follow the selected element
The grab overlay keeps re-measuring the picked element after the click
(scroll in any box, resize, element size changes), and the isolated
viewport bridge reports that live box so the pending card follows it,
including page zoom. The card's side pick uses its measured height.
* fix(browser): bind annotation card to selected element
* fix(browser): keep document annotation card on selection
* fix(ci): point the main sync-spawn ratchet at the shared ratchet library (#28054)
#27756's script imported baseline helpers from check-focus-read-ratchet.mjs
after #27788 had moved them into ratchet-baseline.mjs, so it crashed on start
and failed every PR that merges main. Use the library's helpers, and record the
two daemon-process launches from #27769 that the broken check never saw.
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(browser): follow selected element outer-box size
* refactor(chat): give the start-refusal resumable table its own module (#28085)
#27946 and #26684 together put agent-session-failure-words.ts at 304 lines,
over max-lines, which fails lint and the pre-commit hook on every merge of
main. The refusal-code table is standalone with one reader, so it moves out.
Co-authored-by: Orca Worker <orca-worker@localhost>
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: Orca Worker <orca-worker@localhost>
* chore: upgrade terminal dependencies to the matching payload betas
* test: harden xterm payload contract, align mobile pins, fix serialize-at-ref across version bumps
- Cover foreign-payload replacement and the shared blank-cell reset in the
installed image payload contract (both mutants previously survived).
- Bump mobile's xterm, webgl and unicode11 pins to the root's betas.
- Resolve the ref's own serialize version and patch so the differential
fuzz baseline builds across a version bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step.
* Share any HTML file from the editor and the remote preview
HTML could only be shared as an artifact from a local file:// browser tab.
The editor offered Share only for Markdown, and the document preview that
renders HTML on SSH and paired hosts had no Share at all.
The editor now shares HTML files too, and the document preview's toolbar
gets the same Share button, reading the page from its owning host. Both use
one source key per file per host, so either surface finds the link the
other published.
Co-Authored-By: Claude <noreply@anthropic.com>
* Share every file under one key per host, and preview Share from its grant's host
The editor, the rendered preview and a local browser tab each built their own
share key, so one file could publish to several links: a Windows path spelled
C:/ in one place and C:\ in another, and the preview keyed from whichever host
the workspace resolved to at render time, not the host it was reading from.
One key function now serves every surface, with main's formats (local path,
the CLI's SSH key, ['editor', env, host, path] with the 'remote' fallback) and
Windows drive/UNC paths spelled with backslashes.
The preview's grant handle keeps the location it was minted for. Share reads
the page through that location and keys from it, so a later host change can't
move either; an SSH grant keeps its connection id, so its read fails closed
instead of reading a local file.
Publishing prepares the request once per click, before sign-in and the auth
retry, so a retry publishes what the user clicked Share on.
Co-Authored-By: Claude <noreply@anthropic.com>
* Pin preview Share key parity with the editor for server-proxied SSH workspaces
A preview of an SSH workspace a paired server proxies mints its grant on that
SSH target, so it already keys the file the same way the editor does; the new
test locks that in. Also corrects the SSH read comment: the connection id is
kept because dropping it would read the path on this machine.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(sidebar): stop workspace metadata icons brightening on hover
The link, notes and provenance icons on a sidebar workspace row only open
the details hover card; clicking them does nothing extra. The hover colour
made the link icon look clickable.
* chore: remove unrelated lockfile changes
* fix(native-chat): list not-sent rows at their journal places
The shared projection appended rows the host recorded but never delivered after every other row,
leaving the reader to sort them. A reader that draws the list as given showed an old not-sent
message under newer turns. Merge them in at their journal places, after the stop rows are built,
so the list is already in order. The desktop sorts by the same order, so what it draws is unchanged.
* refactor(native-chat): move the recorded-rejection line into shared words
The desktop's words for a message the host recorded and then rejected now live beside the other
rejection words, returning notice parts each client renders its own way, so the phone can say the
same thing. The desktop calls it with the same inputs; its words are unchanged.
* refactor(native-chat): one projection behaviour for rejected rows
Every reader now draws a message the host recorded and then rejected where it was sent, marked
not sent, so the rejectedInPlace option goes. The phone was its last off caller apart from the
host's conversation outline, which now leaves not-sent rows out itself; a test proves the outline
is the same as if the rejected message had never been sent.
* fix(mobile): show a not-sent message in place with the reason under it
The phone now draws a message the host recorded and then rejected where it was sent, as the desktop
does, with one small line under the bubble in the desktop's words. The line is built from the
journal the view already receives; when a start-failure or sign-in row already gives the reason,
it says only that the message was not sent. The load-earlier header moves to its own file to keep
the view under its line limit.
* fix(mobile): a send the host recorded is never handed back
When the send's own reply said the host recorded the message and then rejected it, the phone put
the text back in the composer with a banner, though the host and the desktop keep it in the chat.
A recorded send now settles like any other: its row draws it in place as not sent (or its kept
card holds it), and the composer stays empty. The banner's failure-fact plumbing, fed only by that
branch, goes with it; the transcript states the sign-in guidance once.
* fix(mobile): keep each structured send on the shared optimistic list
Once a not-sent row stays in the chat, matching a pending bubble by its text breaks: resending the
same words after a not-sent message never reached the count the bubble waited for, so a stray copy
stayed, and an acknowledgement lost on that resend showed a false "Delivery unconfirmed".
A structured send is now one record, kept by the structured session under the id it is sent with.
Three parts match the desktop's sender: the id, the drawing (the shared projection draws it at the
tail until the host's row under that id lands, in the same pass) and the settle rule (the host's own
record of it, a submission, a card, or a card's hand-off, drops it by the one rule the desktop now
shares). The rest stays the phone's own: the bubble shows once the host accepts the send, not from
the tap; a send whose answer was lost is held undrawn and says "Delivery unconfirmed" only if its
deadline passes first, instead of going back to the composer; records are kept whether or not a
chat view is open; and one send in flight is the composer's rule, not the sender's. Its photo is
kept under the row id it shares with the host's row, so it shows on the bubble, the landed row and
a not-sent row. The drafts' text matching, with its older queued-card matching, now serves only
terminal-backed chats, whose transcripts carry no ids.
* fix(native-chat): merge not-sent rows by the transcript order alone
The merge kept a rule for rows with no journal place that no caller reaches and that the desktop's
sort does not have; drop it, and say what merging after the stop rows actually keeps. A test pins
that the list the phone draws as given is already in the desktop's sorted order, with a moved stop
row, a retry run, a cut-turn notice, a held card and two not-sent rows.
* fix(mobile): draw the not-sent line as the desktop does
Red and capped to the bubble's width, like the desktop's line and the phone's own returned-card
caption, and only when there is a notice: the row had a second, unreachable source of words. A
send a Stop took back is no failure, so it no longer rebuilds the notices on every streaming frame.
* refactor(chat): answer transcript adoption and Session History from the agent registration
Claude and Codex's transcript importers and Session History ownership (row id,
resume-command grammar) move onto their runtime registrations as required-nullable
fields; every other agent writes null. agentSession.agents publishes the derived
transcriptAdoption/sessionHistory flags, and a client reads a host that omits them
(or lists nothing) as the old build's answer. Strict parity: only Claude and Codex
answer yes anywhere.
* refactor(chat): one Session History ownership index, binaries from row agents, fork offer from fork argv
Review round 1: the duplicate-adoption check and Session History ownership read one
index keyed by the registration's row id (an importer now requires sessionHistory);
resume-command binaries derive from rowAgents through the TUI agent config; the
Resume in New CLI offer asks getAgentForkArgv instead of naming Claude and Codex.
Strict parity: only Claude and Codex answer yes.
* feat(chat): Session History owns OpenCode, Grok, OMP and Pi native chats
An OpenCode, Grok, OMP or Pi native chat's conversation listed in Session
History as a plain terminal row, and resuming it ran the agent's CLI as a
second writer to the conversation the chat was still using. Each agent's
registration now fills `sessionHistory`, so those rows show as the chat
(its name, opening the chat) and Orca refuses a terminal resume of an owned
conversation, as it already did for Claude and Codex.
- OpenCode lists under `opencode` and `opencode2` (2.x) rows by its ses_ id;
`-s/--session`, `-c/--continue` resume, `--fork` does not.
- Grok: `-r/--resume/--load` and `-c`; a non-UUID value names a title, so it
is read as target-less; `--fork-session` does not resume.
- OMP: `-r/--resume/--session` take an id, any prefix of it, or the session
file's path; bare `--resume` and `-c` are target-less.
- Pi: rows are listed by the session file name's id while the chat holds the
file's path; `--session <id|prefix|path>`, `-c`, `-r` (picker).
- An optional `resumeTargetMatches` on the registration covers prefix and path
targets; it defaults to exact equality.
- Each agent now reads every token naming its own binary, so another agent's
name earlier in a command (`claude --model pi`, `cd ~/src/codex && claude
--resume X`) no longer hides a resume. For Claude and Codex this only ever
refuses more than before.
- A renamed OpenCode chat tab names its `opencode2` row too.
* fix(chat): show owned rows only to clients that open the chat, without a resume command
- The list and search projection now asks, per owning agent, whether the
requesting client opens that agent's chat. Current mobile renders only Claude
and Codex chats, so it no longer receives OpenCode/Grok/OMP/Pi rows owned by a
chat (a tap there opened an empty terminal and showed agent_session_conflict).
Claude and Codex keep their rule: any client that reads structured sessions.
- An owned search hit opens its chat, so it no longer carries a terminal resume
command (all agents; `orca` CLI search stopped printing one for a chat).
- OpenCode/Grok/OMP/Pi flags match case-sensitively, as their CLIs read them,
so `rg foo src/pi -C 3` is not `pi -c`. Claude and Codex still fold case.
- A test feeds every resume command Orca builds for an owned row of each agent
(Session History, live-tab argv; posix, PowerShell, cmd) into the guard.
* fix(chat): keep the resume guard linear and skip chat-tab lookups with no move
- Each agent binary mention in a command now reads its arguments only up to
that agent's next mention, which reads the rest, so every token is parsed
once per agent. A 200 KB text naming agents throughout took 9.4 s on the
host's main process; it now takes about 90 ms. Every refusal before still
holds: a later mention still reads its own resume.
- Right-clicking a native chat tab looks up its Session History row only when
the agent's CLI can fork a conversation, the one move a chat tab offers, so
OpenCode, Grok, OMP and Pi chat tabs run no list or rescan for nothing.
Claude and Codex are unchanged.
* fix(chat): read each shell command once in the resume guard
The guard tokenizes the text once and splits it into shell commands at
&&, ||, ;, |, & and newlines (a newline after a line-continuation mark stays
inside its command). In each command, each agent reads its arguments from its
first binary token to the command's end; a later token naming it is an
argument. A `claude` path inside a Claude command no longer ends that command,
so `--session-id <owned>` still cancels a fork after it, and a binary-named
path between a resume and its fork flag keeps the fork a fork. Binary names
are looked up once per token, which keeps a 16 MB send to about 2 s.
Against main's parser, a Claude/Codex command is now allowed only when main
refused it for a flag in a later shell command (`claude ; echo --resume X`).
* fix(chat): end a resume-guard command at a bare carriage return
A PTY's Enter, and many orca terminal send texts, end lines with \r. The guard
now treats \r and \r\n as line breaks like \n, including after a line
continuation mark, so a later line's resume is read as its own command.
* refactor(chat): drop the transcript-adoption and Session History wire flags
agentSession.agents no longer publishes transcriptAdoption or sessionHistory, so
there is no new field for older and newer peers to reconcile. Their only
consumers were renderer gates that could answer from what they already have:
"Resume in New Native Chat" eligibility goes back to main's rule (adopting a CLI
conversation is Claude/Codex-only, and the host still refuses any other agent from
its registration's transcriptImport). Removes the AgentSessionDeclaredCapabilities
split, the older-host decoder shim and the generic host-flag reader.
* refactor(chat): keep main's Claude/Codex rule for the CLI fork offer
"Resume in New CLI" and the chat-tab menu read the agent name again instead of
building a synthetic provider session to ask the fork builder, and the chat-tab
lookup loses its host Session History flag gate. Only Claude and Codex CLIs fork,
so only their chat tabs look up a row, exactly as on main: no new list requests.
* refactor(chat): one registration-aware ownership index, no registration union
- listStructuredProviderSessionOwnership reads each record's registration for its
Session History row id; the callback parameter and its one-function wrapper
module are gone (no import cycle: checked value and type imports).
- StructuredAgentCliConversations is a plain type: transcriptImport is nullable
and sessionHistory is required, since every registered agent now owns its rows.
The "importer needs history" union and its null checks go.
- A test pins that no two registrations claim the same Session History row agent,
since the owner lookup takes the first match.
* fix(chat): read resume commands into words as a shell does
Separators glued to a word (`opencode -s id; echo`, `cd /x&&claude -r id`) now end the
command, and quotes are removed inside a word, so `--session="id"` reads as the id. Applies
to every agent, Claude and Codex included. The acp-and-pi allowed() helper now asserts.
* fix(chat): count an agent's name only where a shell would run it
terminal.send checks agent prompts too, and agentPrompt does not keep the text from a shell (it
falls back to a plain PTY write), so prompt text like "Fix the Pi --resume flag" was refused.
The resume check now reads each command's program: its first word past VAR=x prefixes, a path,
and env/exec/command/sudo/npx/bunx/pnpm dlx. Every command Orca's builders produce, Claude and
Codex included and with a user's command override, is still refused.
* refactor(chat): compare resume ids in any case; Claude uses the shared option reader
The default target match now ignores case (ids are UUIDs or ses_ ids), so Grok's override,
which only repeated it, is gone. Claude folds its flags and reads them with
resumeInvocationFromOptions instead of repeating the `=` handling.
* perf(chat): read terminal text in one linear pass
A repeated regex group overflowed the stack on a 16 MB word. A sticky-regex scanner reads words,
quoted runs and separators in one pass; an unclosed quote is searched for once per quote kind.
* refactor(chat): read a command's resume only when its program is an agent
* fix(chat): read an agent run under nohup, time or wsl as the command's program
* chore: restore pnpm-lock.yaml
* fix(chat): keep the CLI's own title on OpenCode, Grok, OMP and Pi chat rows
Session History shows these agents' chat conversations under the title their CLI saved, as it does
for their terminal conversations; the chat tab's name and renames stay on the tab. Claude and Codex
rows keep the chat's name, as on main.
* feat(chat): Resume in New CLI for Grok and Pi chats
A Grok or Pi chat's Session History row and tab menu now offer "Resume in New CLI", like Claude and
Codex: the CLI opens a copy of the conversation (grok --resume <id> --fork-session, pi --fork <file>)
and the chat keeps its own. OpenCode's interactive CLI and OMP have no way to open a copy, so their
chats keep only Resume.
* fix(chat): offer Resume in New CLI only when the copy command can be built
A Pi chat found by search on a remote Orca server has no session file path, so its copy command
can't be built; the row no longer offers the button there.
* fix(chat): refuse pi --session-id of a conversation a chat owns
`pi --session-id <id>` reopens that session when it exists, so it is a second writer like --session.
* feat(settings): move Chat UI's chat settings from Experimental to the Chat page
* chore(i18n): sync catalogs for the moved chat settings
* fix(settings): hide the Behavior group when search matches only a switch the host lacks
* Keep Chat behavior search aligned with translations and host support
* Require Codex 0.136.0 for native chats
* Add host-owned Codex install and update actions
* Bind Codex checks and maintenance to the selected installation
* Resolve Windows updater cleanup through system helpers
* Use one child environment for Codex checks and launch
* Expire Codex readiness and reconcile current host jobs
* Resolve Codex checks against the child command search path
* Restrict global npm repair to the selected Codex install layout
* Pin the host updater directory through Windows supervision
* Synchronize Codex maintenance fixtures on readiness
* Unify Codex notices and recover version-refused chats
* Route Codex recovery to the operation that remains
* Preserve Codex setup execution errors in the job log
* Preserve UTF-8 in Codex setup logs
* Remove unused Codex host installation detector
* Require host administration for Codex maintenance
* Show a missing Codex explanation once
* test(native-chat): give scripted Codex runtimes their installation fact; keep the registry out of the relay
Runtime tests that drive a scripted Codex app-server still ran the new
version check against whatever codex was on the machine's PATH: refused
on CI (no codex), and on a dev machine it ran the real codex --version.
A scripted transport now must state the Codex it stands for
(readCodexInstallation), asserted at install, and the tests get it from
one scriptedCodexTransport fixture.
The relay's Codex maintenance path reached @orca/windows-registry through
resolvePathEnvKey's re-export in windows-environment-path; import it from
its registry-free module instead.
Also update the catalog-probe test for the shared child environment.
* refactor(native-chat): drop the Update Codex action; keep Install for a missing Codex
A too-old Codex now gets only the plain message ("Codex X is too old for
chats. Update to 0.136.0 or newer.") with no button and no job. The
existing rechecks (window focus, the <=30 s expiry of a saved result)
retry the refused chat once the user has updated Codex themselves.
A missing Codex keeps the Install Codex button: `npm install -g
@openai/codex` with the npm on the chat's PATH, as a host-run job with
its log, then a recheck and recovery. A configured Command that is
missing gets the message only, since a global npm install would not
land at that path.
Removed: the `codex update` command and its 0.126 floor, manual
instructions, npm layout and launcher identity detection, the action
descriptor (install|update) and its titles, the legacy capability, and
the SSH relay maintenance path (structured chat never runs over SSH or
WSL), so the relay preflight handler is back to main's.
* refactor(native-chat): keep Install Codex in Settings only
When Codex isn't installed, main already hides Codex from the New tab menu,
so the in-chat Install button was almost never reachable. Settings → Agents
already shows "Codex isn't installed." with Install Codex.
- Chats and the new-workspace composer now only state "Codex isn't
installed." (no button, no install job from the chat).
- The Install action, its busy spinner and the host-busy subscription move
into the Settings Codex row, the only place that starts the job.
- The store no longer re-checks every other workspace context when a job
finishes. A refused chat recovers through the same re-check a too-old
Codex uses (window focus, or the <=30 s expiry of a saved result).
- The chat recovery key no longer includes the install job id.
- Tests: Settings drives the install; a new test proves a chat refused for
a missing Codex recovers through focus after a Settings install, not
through the job; the expiry recovery test covers missing and too old.
* refactor(codex): check chat compatibility when acquiring the child
* Restore workspace submit guard and simplify Codex maintenance
* Keep only the Codex chat minimum version
* refactor: move provider-retry pieces into a sibling words file
Pure move of the six provider-retry translation pieces out of
agent-session-failure-words-text.ts, which sits at the 300-line limit.
No key or behaviour change.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(chat): name the agent when it refuses a message
"The provider did not accept this message" becomes "{{agent}} didn't
accept this message" ("The agent" when the name is unknown), in English
and every locale. The providerRejected failure kind is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>