Commit Graph
16002 Commits
Author SHA1 Message Date
6ff6b39e83 perf(terminal): cut renderer per-chunk classifier cost on coloured output (#38489)
* perf(terminal): stop allocating per SGR and rescanning ASCII output in renderer chunk classifiers

* fix(terminal): bound SGR delimiter scans to each parameter sequence

* test(terminal): pin background SGR classification for 38 colour skips and colon params

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-11-06 01:09:40 +00:00
OrcaWinandm4air f15e114aa8 perf(terminal): copy the retained transcript window once per output chunk (#38482)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-11-06 01:09:02 +00:00
OrcaWinandOrcaWin ab982d32b9 test: remove 137 low-value automation and account tests (#38755)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 17:05:57 -08:00
2b0400f16a perf(renderer): cut per-store-write selector fan-out and DOM blink churn on switches (#38505)
* perf(renderer): cut per-store-write selector fan-out and DOM blink churn on switches

- useChatProgramFacts: skip resolving the host context revision while inactive
  (hidden worktrees' tab bars ran two JSON.stringify per store write each).
- useTerminalTabActivity / useTabAgent: memoize per-tab selectors on this tab's
  own inputs so unrelated writes reuse the last result.
- Only suspend cursor blink for panes holding live WebGL: DOM blink is
  focus-gated CSS, and toggling the option rewrote xterm's stylesheet and
  invalidated style on every hide and reveal.

Refs #38413, #26785, #9715

* test(terminal): cover retained WebGL cursor suspension through production paths

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
2026-11-05 16:59:04 -08:00
OrcaWinandOrcaWin cbc2475d14 test: remove 286 low-value session history tests (#38754)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 16:58:57 -08:00
Neil cc8cdd480b End a remote pane whose session exited before its shell was ready (#38747)
* fix(terminal): end a remote pane whose host retired its surface

A remote session that exits before its shell is ready left the pane
"Reattaching terminal" forever: the host retires the pane surface and
publishes a close record for it, but the client only looked for a live
surface and kept polling for one to come back. The client now reads the
host's close record as the end of that pane and ends it with the exit
code the host recorded, both when opening the pane and when reattaching
after its stream ends. A live replacement the host publishes still wins.

Refs #38151

* fix(terminal): fence the retired-surface end to the attachment that asked

Refs #38151

* fix(terminal): keep a retired remote pane revivable by a host replacement

Refs #38151

* fix(terminal): judge a host replacement against the probed exit incarnation

Refs #38151

* fix(terminal): end a retired remote pane only while the host still confirms it

Refs #38151

* fix(terminal): keep an unconfirmed retirement's attach retry parkable

Refs #38151

* fix(terminal): keep watching a retired pane through absent-surface frames

Refs #38151
2026-11-05 16:54:42 -08:00
Kelvin Amoaba 6bd548ba5c feat(mobile): render math in chat and PR descriptions, with GitHub's line-count table (#38682)
* feat(mobile): render math in Markdown with desktop chat's delimiter rules

Move the price, citation, closer and display rules plus the KaTeX settings
to src/shared/markdown-math-rules and point desktop chat at them. Mobile's
markdown-it gets the same grammar, and pages typeset formulas with KaTeX,
whose fonts ship as page assets. Mermaid's engine leaves katex external so
both share one katex chunk.

* build(mobile): count KaTeX fonts in the page budget and keep math to one chunk

Only the licensed terminal font keeps its own allowance; KaTeX's fonts
count toward the budget like code. The typesetter imports katex itself,
sharing mermaid's chunk, so math adds one chunk instead of four.

* fix(mobile): draw the PR line-count table's +/− glyphs at GitHub's size

github.com draws \Huge at \Large (1.44em), so the line-count table's
glyphs sit inline at text height. Cap the larger size commands the same
way, and let inline math take its height from KaTeX's struts so rows keep
normal height and numbers stay on the text baseline.

* test(mobile): pin math delimiters to desktop chat's results

* refactor(mobile): drop type assertions from the math tree nodes

* chore(mobile): register the math mdast nodes beside <details>

* fix(mobile): read bracket math only in chat, keep math parsing linear

GitHub text (PRs, Tasks, files) reads $ and $$ only, so \[Bug\] stays a
bracket; chat rows opt into desktop chat's \( and \[ too. Remember failed
block scans and skip dollar-run tails so hostile input stays linear, leave
unfinished math out of remend like desktop, bound the formula cache by bytes,
retry a failed KaTeX load, and count symbol commands in table widths.

* fix(mobile): copy formulas as TeX and keep GitHub's unclosed $$ as text

Share desktop's selection-to-TeX reader so a copied selection carries each
formula once, as its source. An unclosed $$ in GitHub text stays a
paragraph, chat's math delimiters reach HTML-derived Markdown and summaries,
failed block scans only skip openers inside the lines they read, unfinished
math anywhere in the streaming tail skips remend, and table widths no
longer spread every command into Math.max.

* refactor(mobile): share the formula copy handler and cap sizes only in GitHub text

Desktop and mobile copy through one shared handler. GitHub surfaces draw
\Huge as github.com does while chat keeps desktop chat's sizes, PR and file
text skip bracket scanning entirely, and the dollar-run scan and token
alias live in one place.

* chore(mobile): stop tracking the generated KaTeX stylesheet

#38611 committed it by mistake; postinstall writes it and .gitignore now
lists it.

* fix(mobile): follow desktop chat's new dollar rule through the shared module

#38651 changed desktop chat's inline $ rule (shell variables and prices stay
text, a single $ can span lines, $5+2$ is math). Move those checks into
src/shared/markdown-math-rules, point desktop at them, and give mobile's
markdown-it rule the same behaviour.
2026-11-06 00:53:54 +00:00
Neil 4aeda96176 Move automation run history out of the signed SSH migration manifest (#38718)
* fix(ssh): move automation run history beside the migration manifest

A host whose automations kept a lot of run output overflowed the 768 KiB
signed migration manifest, so every Connect was refused and the host was
locked out. Run history now travels as a capability-gated
`automation-run-history` blob: the manifest signs only its sha256 and
size, the destination imports the runs atomically with the catalog at
commit, and a host without the capability is refused before anything is
sent instead of importing automations without their runs. Journals and
manifests signed with inline runs are still accepted. The oversize error
now names the largest section with an action that trims it.

Fixes #37051

* refactor(ssh): read migration capabilities from their own client module

Keeps orcad-migration-catalog-client.ts within its line budget.

Refs #37051
2026-11-05 16:50:32 -08:00
Neil 2d9d71f72f fix(terminal): give remote Windows panes the ConPTY resize model (#38715)
* fix(terminal): give remote Windows panes the ConPTY resize model

A pane's xterm took ConPTY's resize rule only when the PTY was local. A
paired server or SSH target on Windows runs every PTY, WSL shells included,
through its own ConPTY, but the client grew such panes the Unix way and
pulled history into the screen. ConPTY repaints only what it believes
changed, so stale TUI frames (e.g. Claude Code's inline prompt) stayed
stranded mid-screen after a resize.

Decide the model from the OS of the machine that owns the PTY: the paired
server's status hostPlatform or the SSH target's remotePlatform. The
renderer applies it at mount and again at PTY bind, because a restored pane
can mount before the server's status arrives. Main's copy of an SSH pane
follows the same rule.

* fix(terminal): resolve the PTY owner from the bound transport and apply a late OS once

A paired server's mirrored pane can be that server's SSH target, so the
bound transport's host names the owner. A remote owner's OS can arrive
after bind; the pane stays on the Unix model and watches the store until
it is known, then applies it once.

* fix(terminal): fall back to the pane's SSH owner and stop watching owners that can't be named

When a paired server's transport can't name its host yet, the pane's own
ssh: owner decides the OS instead of the server's. A pane with no
nameable remote owner keeps its creation-time model instead of watching
the store forever. Local-owner detection lives in one helper; an
undefined connection id still means unresolved, not local.

* fix(terminal): re-resolve a pane's resize model when its transport learns the PTY's host

A reattached mirror on a paired server binds before attach metadata says
the PTY belongs to the server's SSH target, so the pane took the
server's model and stopped looking. The remote transport now notifies
when its execution host is learned or changes, and the pane re-resolves
then.

* test(terminal): justify the partial RuntimeStatus fixture with a SAFETY disable
2026-11-05 16:49:34 -08:00
Neil c0b2df5d8d fix(ssh): leave an unlinked host disconnected, and name it when a project needs its server (#38746)
Forget and Stop removed the managed server but left the SSH host's transport up with nothing
serving it, so project setup offered the host and then failed with the raw connection id.
The unlink now hangs up the host's transport inside its lifecycle, so every surface offers
Connect (which sets a server up again) and a Connect queued behind the unlink still wins.
The setup error names the host and its toast offers Connect, and the forget/stop help says
the host is disconnected.

Fixes #37767
2026-11-05 16:44:40 -08:00
Jinwoo Hong a9ba59c944 Nest workspaces under parents on other machines (#38740)
* feat(lineage): store cross-store parent links beside same-store lineage (inert)

A workspace's parent can live in another Orca store. Save that link in the
child's store under a new top-level key, crossStoreLineageByChildKey, so older
builds' same-store cleanup never reads it. Nothing writes records yet.

- Types, load normalization (malformed entries dropped one by one), section label.
- Store get/set/remove by child; a child keeps at most one parent record.
- Every maintenance path acts by the child only: deletion and missing-worktree
  pruning, repo removal and deregistered-repo residue, identity migration,
  profile project transfer, WSL move projection and UNC drive conversion, and
  the orcad migration dormant payload (export, validation, import, subtraction).

* fix(lineage): gate cross-store links on host capability; one parent per child

- Migration stage/commit carrying crossStoreLineage requires the new
  orcad.migration-cross-store-lineage.v1 capability; older hosts are refused
  before staging instead of silently dropping the links. Empty arrays are omitted.
- Payload validation and destination prepare reject a child that has both a
  cross-store and a same-store parent, through one shared conflict check.
- Saved entries stay on disk as written; the map is Record<string, unknown> and
  readers go through validCrossStoreLineage. Re-keying moves unreadable entries unchanged.
- Tests: literal expectations; drop touched mock-call and work-count assertions.

* test(lineage): drop clone and save work-count assertions from project group removal test

* feat(lineage): resolve cross-store parents into the sidebar parent map (inert)

- One resolver: a saved record nests its child under the single visible
  row whose store reports the parent's installationId and whose instance
  matches; copied installs, unknown stores and stale instances stay standalone.
- getSidebarLineageParentsByIdentity merges resolved cross-store parents with
  same-store edges before cycle breaking; a valid same-store edge wins.
- Workspace delete collects descendants only from the parent's own store.
- Store identity: paired stores from their verified status hostDescriptor;
  this store from the published descriptor, else host-installation.json.
- Nothing passes a view or writes records yet, so the sidebar is unchanged.

* fix(lineage): import WorkspaceKey from workspace-scope after main move

* fix(lineage): resolve sidebar parents once over complete rows; keep unowned rows' delete rule

- resolveSidebarLineageParentIdentities resolves same-store edges and cross-store
  records once over every visible store's unfiltered rows, then drops cycles.
  Ancestor injection and nesting reuse that map, so a filtered-out twin still
  makes a parent ambiguous.
- Removed, retired or refusing paired servers leave the view: their records stay
  saved and their children standalone. Offline servers keep their last-known id.
- A row with no owner is unknown, not ours: it never resolves, and delete only
  excludes descendants whose store is known and differs from the parent's.

* fix(lineage): refuse UNC rekey collisions and transfers giving a child two parents

The UNC drive conversion rekeys cross-store child keys through the shared
collision merge, so a UNC key and its drive key with different records throw
instead of one silently overwriting the other. Profile project transfer runs the
shared child-key conflict check before writing the target.

* feat(lineage): write, carry and nest cross-machine parents

- Capability worktree.lineage.crossMachine on the child's store; the CLI
  refuses before any request when the server lacks it.
- worktree.set / worktree.create accept crossStoreParent
  { installationId, workspaceKey, instanceId } and write only
  crossStoreLineageByChildKey (refused when it names this store, or the
  child has no identity). Remove Parent clears both.
- worktree.crossStoreParentRef: the parent's own store names it with its
  installationId; reparent refuses a parent whose chain already reaches
  another store. Same-store Set Parent refuses the same.
- worktree.ps rows and lineage lists carry the records; same-store lineage
  fields stay null so older readers show the child standalone.
- The desktop sidebar resolves parents over its own and paired stores'
  records (PR 2's resolver) at the visible-worktree and row-plan call sites.
- CLI: active/current or an inferred terminal parent against a server is
  sent as a cross-store parent.

* fix(lineage): read worktree rows through the record selector; mark the test runtime cast

* fix(lineage): hydrate every store, fence refreshes, honest CLI fallback

- Each host's catalog hydration fetches its lineage snapshot, so this
  desktop's identity and records load whichever store is focused.
- Cross-store records join the per-child request-start fence: a stale
  lineage list can't restore a removed record or override a newer one.
- A failed inferred cross-store parent sends noParent instead of
  forwarding the selector a server could match to a same-store twin.
- Inferred cross-store creates record env-workspace/inferred capture.
- The writer states its trust contract; the chain rule runs at the mint.

* feat(lineage): cross-machine parents on the phone, pickers, chat resume and labels

- Phone: the list nests a child under a parent on another machine, from the
  records rows already carry and the store identities its host verified
  (status.get descriptor; relayed servers' installationId on the host list).
- Set Parent popover, context menu and the composer's parent picker offer
  parents in other visible stores. The parent's own store names it
  (worktree.crossStoreParentRef); the window checks it against that store's
  verified installationId and the child store's capability before any write.
  Remove Parent and Open parent cover cross-machine links.
- Native chat resume ancestors follow resolved cross-machine parents.
- Host sections keep a nested child under its parent and keep its own
  machine label.
- Main sidebar feed resolves cross-store parents (rows carry records; desktop
  facts and paired-host facts carry identities); paired web windows read
  their host's installationId; reveal without an occurrence plan resolves
  them; `worktree create --agent` names a parent on another machine.

* test(lineage): type the mint test's parent row through unknown

* test(lineage): give the cross-machine tests' partial-row casts their SAFETY reasons

* fix(cli): worktree set names a parent on another machine

* feat(lineage): name the machine of parent candidates on another machine

* fix(lineage): Set Parent and Remove Parent act on the clicked row's own host

* fix(lineage): phone resolves parents over every row before the repo filter

* fix(lineage): chat resume nests by host-qualified identity across machines

* fix(lineage): local create refuses a same-install parent before checkout and shares the RPC recorder

* fix(lineage): name parent machines without a focus read; keep create's probes synchronous

* fix(lineage): chat resume keys offers by their own machine; only cross-machine parents leave a section

* fix(lineage): name an older parent server before asking it for a cross-machine ref

* fix(lineage): an app-picked parent on another machine records as manual

* fix(lineage): chat resume keeps base grouping; only resolved cross-store parents move a chat

* revert(lineage): drop chat resume from cross-machine parents; it groups by machine as before

* refactor(lineage): CLI builds cross-machine parents; explicit only

- Drop worktree.crossStoreParentRef and its reparent chain walk: the CLI
  builds the ref from this desktop's installationId (worktree.lineageList
  now reports it) and the parent row it reads. Cross-machine chains are
  allowed; same-store cycle validation and same-install refusal stay.
- No inferred cross-machine parent: remove crossStoreParentInferred and
  the fallback states. Against a server, this machine's terminal, cwd and
  env context is never sent; the CLI prints how to nest explicitly.

* refactor(lineage): build cross-machine parent refs from verified ids; pickers hide only loops; one desktop install id source

* refactor(lineage): refuse moving a linked workspace instead of carrying the link

Orcad migration and profile project transfer now refuse, before any staging or
write, when a workspace in the move has a cross-store parent record (raw entries,
readable or not). Remove the carry machinery: the dormant crossStoreLineage
field, its runtime capability and destination gate, import/subtract/claim code,
transfer carry and the two-parent conflict checker. Also drop the unused
removeCrossStoreWorkspaceLineage.

* docs(lineage): describe cross-store parent refs without the removed mint RPC

* test(lineage): drop the RPC call-list assertion from the cross-store ref test

* fix(lineage): refuse linked moves before any write and for shared-repo children

- Migration refusal passes the child's own host stamp to the scope matcher and
  refuses an unstamped key whose shared repo id the move includes, so such a
  child can't move while its link stays behind.
- Profile transfer checks the read-only source snapshot before pending-move
  recovery or storage migration; the payload check stays as the final guard.

* fix(lineage): refuse a linked profile move before the profile lookup can restore the index

* fix(lineage): startup hydration of other stores loads only cross-machine links

* fix(lineage): list the child's own machine first in the parent picker

* fix(lineage): keep profile argument errors ahead of the linked-move preflight

* fix(worktrees): let the first listing replace a restored server workspace stub

A restored active server workspace starts as a session stub with no head.
If git status fills in its branch before the first listing lands, the
listing merge took that as a concurrent branch switch and kept the stub,
dropping the catalog row (instance id, lineage, provenance). The workspace
then showed standalone until the next listing.

* fix(worktrees): accept a headless row's listing only when it confirms git status

A stale listing could otherwise roll back a branch switch git status saw on
an empty-head row (e.g. an SSH fallback row) during the first connected listing.

* feat(lineage): CLI nests a create on another machine under the caller workspace

- No parent flag and no --no-parent against a server: the caller's workspace
  on this machine (env, else cwd) is sent as an inferred cross-store parent,
  the way --parent-worktree active builds it. The host records it as
  inferred from environment or cwd.
- A server without worktree.lineage.crossMachine, or a caller workspace that
  can't be linked, still creates unnested and prints one note.
- An inferred parent falls away for folder targets, as same-machine folder
  creates record none.

* fix(lineage): CLI cross-machine inference reads the caller terminal and suppresses fallbacks

- The caller terminal's workspace joins env and cwd as an inferred parent,
  with same-machine precedence: env and terminal must agree, cwd only alone.
- A fallback (old server, unlinkable or unresolved caller, ambiguous cwd)
  prints one note and sends noParent, so the server's own create-time
  inference can't nest what the CLI reported as not nested.

* feat(sidebar): drag a workspace onto one on another machine to nest it

Dropping a workspace on a workspace from another machine now sets the
parent through the same cross-machine path as Set Parent: same eligibility
(loops, older servers), same refusal messages. Drop targets carry host
identity so the child's own copy is the one written.

* refactor(lineage): this machine's Orca resolves the caller parent for a create on another machine

- New read-only worktree.resolveCallerParent runs the same-machine create
  resolver over the caller's env, terminal and cwd context, so stale
  contexts are skipped and only validated parents can conflict, and returns
  the cross-store ref or a reason.
- The CLI's own precedence and terminal lookup are removed; it keeps the
  capability gate, one note with noParent on any fallback, and silence with
  no Orca context.

* feat(orchestration): remote worker-start --worktree new-child nests under the coordinator's workspace

worker-start --on <server> --worktree new-child creates the server's worktree
under the coordinator's workspace on this machine, using the same verified
cross-machine parent ref as the CLI's caller auto-nest. A server without
cross-machine parents still starts the worker, top-level, with a warning in
the receipt. Guide, CLI help and docs describe it.

* fix(lineage): resolveCallerParent reads this store's identity without creating it

- Uses the published host descriptor's installationId, else the existing
  host-installation.json; never creates or repairs it. Missing identity
  returns a reason, so the CLI creates unnested with its note.
- Drops the read-only wording: parent resolution does the same catalog
  upkeep a same-machine create does.

* fix(sidebar): validate every cross-machine parent before a multi-select drop writes

A drop of several workspaces builds every cross-machine parent ref first, so an
older server's refusal leaves the whole selection unchanged. Drops the touched
preview-timing test cases.

* refactor(orchestration): build the remote new-child parent ref with the shared caller resolver

Orchestration still picks the caller workspace; resolveCallerParentForAnotherStore
verifies it and builds the ref, and both read the installationId the same way.
Drops the copy and mock-call tests.

* fix(lineage): narrow the inferred source with a guard instead of casts

* fix(lineage): a composer parent picked on another machine nests normally once the destination is that machine

The retained host-qualified pick is classified against the current destination:
same host gives the ordinary parent fields, another store the cross-machine ref.
Also corrects two stale comments.

* perf(lineage): read only the requested child's cross-store entry

* perf(lineage): request each store's lineage list once at startup

* perf(lineage): cap optional caller-parent inference at 3 s and skip a timed-out local Orca

* perf(lineage): return cached sidebar parents when selector inputs are unchanged

* fix(lineage): snapshot selector inputs without a type assertion

* Route startup lineage and the sidebar lineage memo through their owners
2026-11-05 19:42:19 -05:00
Neil 61803ba8ba fix(orcad): a supervised server does not start without its terminal daemon, and orcad defends its instance lock (#38709)
* fix(orcad): yield a lost instance lock and refuse non-durable terminals under a supervisor

Fixes #37605

* fix(orcad): treat an unstattable lock as unproven; scope supervised help to orcad

Fixes #37605

* fix(daemon): report fresh-terminal ownership from current spawn routing

Fixes #37605

* fix(daemon): narrow fresh-spawn routing check to degraded providers

Fixes #37605

* test(orcad): cover lock-loss yield and fix stale test paths after rebase

Fixes #37605
2026-11-05 16:41:49 -08:00
Neil 9cc0be5800 Let a paired host expand ~ in the paths its terminals print (#38711)
* fix(terminal): let a paired host expand ~ in the paths its terminals print

A paired-server pane expanded `~/…` on this client, using HOME from the
pane's startup env or a home guessed from the cwd. When the host's home
was not under /home/<user>, /Users/<user> or /root, the link never
appeared; when the guess was wrong, it named another file. The host now
receives the `~/…` text and answers with its own home's path, as it already
does for its local workspaces. WSL workspaces keep client-side expansion.

Refs #35968

* fix(terminal): keep a paired pane's launch HOME for its ~ links

Refs #35968

* fix(terminal): keep SSH homes client-expanded and open folder roots from paired ~ links

A server that proxies an SSH workspace refuses ~ paths, so paired panes
over one keep the client's expansion. A host-resolved ~ root that names a
folder workspace now activates through the workspace dispatcher.

Refs #35968

* fix(terminal): send a paired host one separator after ~

Hosts join what follows `~/` onto their home, so `~//work/file` became
the absolute `/work/file` once the client forwarded `~` paths as printed.
The client now collapses separators right after `~` before asking.

Refs #35968

* fix(terminal): link a paired ~ folder path only when it names a workspace

A home-relative path ending in a separator, such as ~/proj/src/, skipped the
folder-link filter so its host could expand it, but only a workspace root opens
from a click, so subfolders became links that did nothing. The host's answer
must now name a workspace root.

Refs #35968

* refactor(terminal): resolve a linked file's editor target in its own module

The editor workspace, host and relative path a linked file opens under now
come from resolveLinkedFileEditorTarget, keeping terminal-file-open-routing
within the 300-line limit. No behaviour change.

Refs #35968
2026-11-05 16:40:58 -08:00
Neil 6ee3ac76ff fix(plugins): ship plugin author typings in packaged builds (#38751)
electron-builder always appends `!**/*.{...,d.ts,...}` to the app `files`
patterns, so the resources/plugins/{declarative,types}/*.d.ts listed in
asarUnpack never reached the package and `orca plugin types` / `orca plugin
create` failed with ENOENT. Ship the typings via extraResources at
<Resources>/plugins (beside plugins/launch), resolve them there when the CLI
runs from app.asar.unpacked, and fail afterPack if any checkout typing is
missing from the packaged output.

Fixes #38542
2026-11-05 16:39:52 -08:00
OrcaWinandOrcaWin 7634d08f74 test: remove 419 low-value hook and search tests (#38748)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 16:35:58 -08:00
Neil 6077bbaf7a Download a granted host file in pages instead of whole (#38693)
* fix(files): download a granted host file in pages instead of whole

A paired host serves a file outside every workspace only through its
terminal grant, whose read is whole-file, text-only and size-capped, so
Shift-click and "Download & open with default app" on such a file opened
it in Orca with "too large" or "binary" errors instead of downloading.
Hosts now page a granted file through files.readTerminalArtifactChunk,
behind a capability, and downloads stream through it like workspace files.

Refs #35968

* fix(files): pin a granted file's ctime so a large download can't mix versions

Files above the digest cap are pinned by stat alone, and utimes can put a
rewritten file's mtime back. ctime moves on every write and can't be reset,
so a same-size in-place rewrite now makes the grant stale.

Refs #35968

* fix(terminal): let a newer link click win over a granted download's host check

Refs #35968

* fix(terminal): drop a superseded granted download whatever the host answers

Refs #35968

* test(files): wait out a coarse ctime tick before rewriting a granted file

On filesystems that stamp ctime in coarse ticks, a rewrite right after the
grant could keep the granted ctime and make the stale-page test flaky. The
test now rewrites until the ctime moves. Also corrects a comment that said a
grant can't be downloaded.

Refs #35968

* style(files): format the granted chunk capability constant

Refs #35968
2026-11-05 16:35:12 -08:00
Brennan Bensonandggbdpq ca971bba2e Clear a Claude permission wait when Claude ends the request itself (#38648)
* Clear a Claude permission wait when its tool batch ends

When Claude ends a permission request itself (prompt timeout, built-in
safety check, non-interactive denial), no Post* hook fires, so the
terminal kept showing "needs permission" for the rest of the turn while
Claude kept working.

Claude 2.1.118+ sends PostToolBatch once every call of a model step has
resolved, listing each call's tool_use_id. Install it (not for
OpenClaude), name each PermissionRequest's request from the matching
PreToolUse in the listener, and settle a held wait only when its own
batch lists that request, locally and over SSH/WSL relays (live and on
reconnect replay). Elicitation dialogs, unnamed waits, newer prompts and
finished turns are left alone. The offline hook spool skips the batch
event, which carries every call's output.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Name a permission from its open call and keep held prompts across sibling results

Parallel calls interleave their hooks, so a PermissionRequest is named
from the one open PreToolUse it matches rather than the latest one. A
subagent's sibling result no longer restates its wait over a held
prompt row, a subagent's settled request can't clear the lead's own
open prompt, and retention reads only the entries the history keeps.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Keep the pane waiting while any other permission request is open

The listener's single wait slot holds only the newest prompt, so a
batch now settles a wait only when no other open call still has a
pending permission request. This replaces the missing-saved-state
guess, which missed two concurrent subagents and stranded a subagent's
wait after a relay restart. A fresh session start, not compaction or a
child, retires open calls, and a batch's id list is read once.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Skip a spaced batch event in the offline spool and pin batch-read cost

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Settle a self-denied wait only while no other prompt is open

Orca keeps one wait per pane, so overlapping prompts can't be settled
one by one yet. A batch now leaves every held row alone while another
call still waits on the user: a permission request, or a question from
its PreToolUse. A stopped subagent's calls retire, and a call that waits
on the user is never evicted by the open-call bound. Overlapping prompts
keep today's behaviour and never show working while one is open.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Count open MCP dialogs and unmatched requests as waiting on the user

An Elicitation now waits in the open-call registry until its
ElicitationResult (matched by elicitation_id), and a PermissionRequest
that matches no observed call still records an entry that waits until
its owner stops or the turn ends. Either keeps a batch from clearing
the pane while that prompt is still on screen.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Keep tool calls apart from waits Orca cannot identify

An MCP dialog is now identified by its owner, server and elicitation id, so a result ends only that dialog. A dialog without an id, or a permission request whose call was never seen, is an unidentified wait that only a stop, turn end or fresh session retires. Only real tool calls name a request or settle by id, so an internal key never reaches the status row.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* End a call only from its own agent and keep lead prompts across task notifications

A tool result or batch now ends only a call opened by the same agent, so a reused id cannot end another agent's prompt. An injected task notification is not a new user turn, so it no longer retires the lead's open prompts.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

* Show the offline spool skips Claude tool batches

Co-authored-by: ggbdpq <ggbdpq@gmail.com>

---------

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
2026-11-05 16:31:58 -08:00
Brennan Benson d048aacd59 fix(sidebar): keep resize controls above workspace panes (#38705)
* fix(sidebar): keep seam controls above workspace panes

* test: cover sidebar hit targets over terminal content
2026-11-05 16:31:03 -08:00
Brennan Benson c7eae748d8 fix(tabs): a session's own name replaces a quick-command tab name (#38677)
* fix(tabs): a session's own name replaces a quick-command tab name

* fix(tabs): only a session's own name replaces a quick-command tab name

Session titles now say whether they are the session's own name (a rename
or a provider-generated name) or a stand-in such as the first prompt. A
quick-command tab keeps its label until the session has a name; a name
typed on the tab still wins. Titles from older hosts and saved tabs with
no kind keep the label; old saved titles are asked for once.

* fix(tabs): classify History-cached titles for session-id-only title requests

* fix(tabs): keep a cached session title when its transcript cannot be reread

* fix(tabs): keep a session title when its classification reread fails

* fix(tabs): keep the transcript for an unclassified cached session title

* test(tabs): keep the cached-title fixture's agent literal
2026-11-05 16:30:03 -08:00
Neil b468781c20 fix(claude): drop the local-command caveat when rewinding to a slash command (#38743)
Rewinding to a Claude local command (e.g. /usage) cut the fork at the
command row's parent, which is Claude's isMeta <local-command-caveat> row.
The fork then ended on the caveat, so the next prompt was read as the
local command and Claude replied "No response requested." Cut at the
caveat's parent instead.

Fixes #38686
2026-11-05 16:29:36 -08:00
Neil 47e8985117 fix(serve): keep orca serve running while it probes the login shell (#38515)
* fix(serve): keep `orca serve` running while it probes the login shell

`orca serve` started from an interactive terminal stopped itself ("[1]+
Stopped") before printing its pairing link. orcad inherits the terminal
and probes the login shell with `$SHELL -ilc`; an interactive bash 5 or
zsh takes the terminal's foreground job, so orcad's next terminal access
raised SIGTTOU/SIGTTIN. Give both shell probes (login environment and
PATH) a POSIX session of their own so they never hold a controlling
terminal. Windows keeps attached spawns, where `detached` would open a
console.

Refs #38347

* fix(serve): end a detached shell probe with its owner and on timeout

A probe in its own session no longer receives the terminal's Ctrl+C or
hangup, so a hung rc could outlive `orca serve`. Kill the probe's process
group by its own pid when it times out and when its owner exits while it
still runs; orcad and Electron turn INT/TERM into an exit, so a Ctrl+C
reaches it that way. No signal listeners: one would replace Electron's
graceful signal quit. The timeout now also ends what the rc started, not
just the shell.

Refs #38347
2026-11-05 16:28:20 -08:00
Brennan Benson 64ad230648 Open a terminal when splitting a Chat UI pane (#37984)
* Open a terminal when splitting a chat pane

* Clean empty terminal splits after remote creation settles

* Align retained menu test with terminal split labels

* Verify translated terminal split rejection messages

* Load remote regression dependencies in suite setup

* Keep split actions searchable by chat context and terminal terms

* Preserve later workspace selection when splitting into a terminal

* Use the existing group argument in terminal creation control

* Preserve existing split searches when opening terminals

* Rank eligible palette actions before checking split results

* Describe terminal splits accurately in the chat tour
2026-11-05 16:23:58 -08:00
89b51e2514 Include native chat tabs in Ctrl+PageUp/PageDown cycling (#37952)
* fix(tabs): include native chats in terminal shortcut cycling

Co-authored-by: Hrithik <hrithik@Hrithiks-MacBook-Pro.local>

Co-authored-by: Ignacio Correia <ignacio.r.correia@gmail.com>

* fix(tabs): keep cycling after selecting a hydrating terminal

* test: bound tab cycling work as tab counts grow

---------

Co-authored-by: Ignacio Correia <ignacio.r.correia@gmail.com>
2026-11-05 16:23:36 -08:00
Kelvin Amoaba 81e68744f2 fix(browser): Escape typed in a page exits element grab, find and the password list (#38741)
* fix(browser): Escape typed in a page exits grab, find and the password list

A focused page keeps its keys, so the renderer's Escape listeners for
element grab/annotate, the find bar and the saved-password list never
fired. Main now hands Escape from browser and HTML preview guests back
to the renderer for that page; during a grab pick the page doesn't
also get it, like C/S.

* test(browser): say why the grab Escape test waits out the toast

* refactor(browser): share one page-Escape subscription hook
2026-11-06 00:22:02 +00:00
Brennan Benson e1bc4e8618 Cmd/Ctrl+Enter on an empty chat composer steers the first queued message (#38732)
* fix(native-chat): Cmd/Ctrl+Enter on an empty composer steers the first queued message

* fix(native-chat): empty-composer Cmd/Ctrl+Enter steers only where the chord means steer

With Queue follow-ups off, Cmd/Ctrl+Enter queues the typed message, so a
second press on the now-empty composer would steer the card just queued.
2026-11-05 16:21:44 -08:00
OrcaWinandOrcaWin 0b27a777ea test: remove 220 low-value hook, timeline and Ruby tests (#38739)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 16:20:16 -08:00
OrcaWinandOrcaWin 9a255602d2 test: remove 161 low-value ACP tests (#38738)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 16:16:45 -08:00
Neil a0cea20878 fix(quick-open): resolve a folder twin's file list on the host the user selected (#38736)
Chat @ file search, Quick Open and the + menu looked a folder workspace up without a host, so a
same-id folder on two hosts resolved to nothing and listed no files. They now resolve the selected
copy, the same way skill lookup does.

Refs STA-13130
2026-11-05 16:16:15 -08:00
Jinwoo Hong 611feba7cc fix(terminal): press Enter once the composer shows the pasted prompt (#38401)
Press Enter once the composer shows exactly the pasted prompt, instead of waiting for a quiet screen that background work never gives.
2026-11-05 19:13:10 -05:00
Jinwoo Hong 0fe7a0beed fix(orchestration): hold mail behind a draft typed into OpenCode (#38451)
Hold orchestration mail behind a draft typed into an OpenCode composer (1.x and 2.x).
2026-11-05 19:09:51 -05:00
Jinwoo Hong 4fd7aea9d5 Keep a nested server workspace nested after the desktop restarts (#38378)
* fix(worktrees): let the first listing replace a restored server workspace stub

A restored active server workspace starts as a session stub with no head.
If git status fills in its branch before the first listing lands, the
listing merge took that as a concurrent branch switch and kept the stub,
dropping the catalog row (instance id, lineage, provenance). The workspace
then showed standalone until the next listing.

* fix(worktrees): accept a headless row's listing only when it confirms git status

A stale listing could otherwise roll back a branch switch git status saw on
an empty-head row (e.g. an SSH fallback row) during the first connected listing.
2026-11-05 19:05:24 -05:00
Brennan Bensonandyu.xia 369e23b084 fix(git): keep the agent-terminal credential guard scalar-only so env scrubs can't break git (#38716)
* fix(git): keep the agent-terminal credential guard scalar-only so env scrubs can't break git

Agents that scrub env names matching KEY/TOKEN/... (DeepSeek Harness) dropped
GIT_CONFIG_KEY_n but kept GIT_CONFIG_COUNT, so every git command in those
terminals failed with "missing config key GIT_CONFIG_KEY_0". GCM_INTERACTIVE
already carries credential.interactive and suppresses GCM's GUI, so the guard
now emits only scalar variables. Tracked in #26594.

Co-authored-by: yu.xia <yuxianice@163.com>

* chore(git): drop stale indexed-config comments and a dead WSL forward branch

---------

Co-authored-by: yu.xia <yuxianice@163.com>
2026-11-05 16:04:25 -08:00
OrcaWinandOrcaWin 27b1ed858c test: remove 184 low-value mobile, CLI and ACP tests (#38733)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 16:02:19 -08:00
NeilandPr1p 925cc80507 fix(orcad): only evict real version slots from the artifact cache, so the shared Node runtime survives upgrades (#38717)
* fix(orcad): keep the shared node runtime out of artifact-cache eviction

pruneOrcadArtifactCache treated every non-dot entry under a target
directory as a version slot, including runtimes/ — the Node runtime the
slots share across Orca versions. When an upgrade introduced a third
build, runtimes/ was usually the oldest entry by mtime and was removed,
after which orcad could not start (#38181). Skip the reserved directory.

* docs(orcad): document what the artifact prune keeps and what it must not evict

* fix(orcad): only evict names that parse as artifact version slots

Retention now shares the slot-name grammar with the materializer, so
runtimes/, staging dirs and any future sibling under a target are never
counted as a version slot. Installs that already lost runtimes/ re-place
the pinned Node on the next orca serve start.

Fixes #38181

---------

Co-authored-by: Pr1p <zwq19980411@gmail.com>
2026-11-05 15:57:37 -08:00
Neil 8f490f2fcd fix(browser): attach the desktop browser bridge to environment recipe SSH hosts (#38722)
* fix(browser): attach the desktop browser bridge to environment recipe SSH hosts

Recipe workspaces connect runtime-owned SSH targets that serve a managed
Orca server, but the bridge picked candidates from the SSH settings list,
which hides runtime-owned targets, and browser routing refused them too.
Pick bridge candidates from the managed-orcad claims, validate route
partitions by registration, and route deployment hosts whoever owns them.

Fixes #38603

Reported-by: Mehul K (@mek97)

* fix(browser): follow recipe host connections for the bridge and page routes

Owned SSH targets returned before the bridge rechecked, so a recipe host
connecting never attached it, and recipe browser panes had no published
SSH state to wait on. Recheck the bridge on every SSH state change, and
read a recipe target's connection from its server's state.

Fixes #38603

* fix(browser): settle a recipe browser route when its server is unreachable

Only a live dial waits; lost or never-made contact now lets the route
card offer Retry and browsing from this device.

Fixes #38603

* test: annotate hoisted fixtures instead of casting

Fixes #38603
2026-11-05 15:55:01 -08:00
OrcaWinandOrcaWin 5bc786c1a7 test: remove 133 low-value workflow, mobile and CLI tests (#38725)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 15:52:52 -08:00
Kelvin Amoaba 9b2427b958 fix(mobile): say why an agent set to Chat opened as a terminal (#38696)
* fix(mobile): say why a chat launch started a terminal instead

When the host falls back to a terminal for an agent the user set to Chat
(for example Antigravity without its chat runtime installed), show the
host's reason in the launch toast instead of opening a CLI silently.

* fix(mobile): skip the fallback notice for agents that have no chat

* fix(mobile): keep the launch notice toast up long enough to read

* refactor(agent-launch): own the chat-fallback rule in the launch contract
2026-11-05 23:48:01 +00:00
Neil 0f18e8a40c fix(browser): pin the workspace host for browser downloads into a server workspace (#38721)
The server-side download transfer store wrote through the runtime file
mutations without a host expectation, so every write was refused as
coming from an outdated client. Each transfer now captures the
workspace's host as it prepares the download directory, and passes that
expectation on every later mutation, so a host that moves mid-transfer
refuses the rest.

Fixes #38122
2026-11-05 15:42:22 -08:00
Brennan Benson 28f0d162c7 fix(chat): repair account setup and recover startup retries (#38389)
Prepare a missing owned Claude account home through its existing setup owner and retain useful startup failure guidance. Keep uncertain opening messages in their original chat for explicit Retry, accept exact late proof without duplicate messages, and persist one aggregate launch snapshot before notifying listeners.

Verified with independent code and performance review, Claude/Codex runtime recovery and upgrade QA, targeted renderer and persistence contracts, and required CI. Tracked in STA-13879.
2026-11-05 15:25:52 -08:00
Neil 71d4e9be16 fix(ai-vault-search): let a paired desktop resume session search hits (#38707)
A paired desktop (client kind 'runtime') was classified with phones as an
untrusted relay caller, so the host stripped resumeCommand and filePath
from search hits and the desktop stripped them again on receipt. The same
pairing already returns those fields from the session list, so Resume in
New Tab worked from the list but was disabled for the same search hit.

Classify by client kind once: phones keep the redacted view, paired
desktops get the same fields the session list gives them.

Fixes #38555
Reported by @ArtemMakaryev
2026-11-05 15:16:36 -08:00
b6de60ad0f fix(daemon): a main-process freeze no longer tears down the terminal daemon link (#38695)
* fix(daemon): a main-process freeze no longer tears down the terminal daemon link

On a Windows host where starting a process blocks for seconds, main spends most of its time
frozen inside synchronous spawns (git, mostly). After such a freeze Node runs due timers before
the socket completions that arrived meanwhile, so the 5 s write-settlement deadline expired ahead
of a write the daemon had already taken, and main tore the daemon connection down as dead. Every
session on that daemon then refused input until re-attached, and the reconnect's hello and connect
deadlines failed the same way. Only acknowledged input waits on that settlement, which is what a
paired desktop sends: its keystrokes were refused in a loop while phone and local typing went
through.

The settlement, hello and connect deadlines now count only the time the event loop could run: a
freeze is credited as a single tick, and a tick that ran late never expires a deadline itself, so
the I/O phase runs first. A daemon that stops answering while main runs is still condemned after
the same timeout.

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

* fix(daemon): freeze-tolerant connection-attempt and RPC deadlines

---------

Co-authored-by: registered2nd <39094401+registered2nd@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:14:31 -08:00
NeilandFede654 701098aebe fix(runtime): date title-only agent status at the PTY status transition (#38706)
* fix(runtime): stamp title-only agent status at the PTY status transition

Program activity moves a PTY's agent status without a new title, so a
title-only frame dated by the title clock alone could sort behind the
working row a paired client already holds. The client then dropped the
done as stale and kept the spinner. Title-only frames and shell-title
demotions now carry the later of the title time and the status
transition time.

Fixes #25136

Co-authored-by: Fede654 <buzondefede@gmail.com>

* fix(runtime): stamp the no-hook title fallback at the PTY status transition

The builder's fallback for a pane with no live hook dated its title-only
frame by the title clock alone, so it could sort behind a title-only
working the renew path had stamped at the status transition.

Fixes #25136

Co-authored-by: Fede654 <buzondefede@gmail.com>

* fix(runtime): never date a shell-title demotion behind the status it replaces

Fixes #25136

Co-authored-by: Fede654 <buzondefede@gmail.com>

* test(runtime): keep the title-only stamp tests beside the mobile status projection

---------

Co-authored-by: Fede654 <buzondefede@gmail.com>
2026-11-05 15:12:28 -08:00
6ead0dfb02 fix(runtime): re-attach every terminal a daemon reconnect detached (#38704)
* fix(remote): re-attach terminals a daemon reconnect detached, for remote views and input

When the host's main process loses and re-makes its daemon connection, the daemon drops main's
attachments and the adapter refuses input to every session until it is attached again. Only a pane
mounted in the host's own window re-attaches by itself; a session with no pane there stays silent
and refuses every keystroke from a paired desktop, a phone or `orca terminal send`, while
`terminal list` still calls it connected and writable. A client remount or restart did not help:
its new subscription never re-attached the session.

Main now notes each session the provider's endpoint loss names, re-attaches it once a remote view
shows it (at the loss or when a view subscribes later), and holds input for it until that attach
lands instead of refusing it.

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

* fix(runtime): main re-attaches every session an endpoint loss detached

* fix(runtime): held input honours cancellation and the session it was sent to

Fixes #38433

---------

Co-authored-by: registered2nd <39094401+registered2nd@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:10:11 -08:00
Kelvin Amoaba 69892601e2 feat(mobile): rewind a chat to an earlier message (#38344)
* feat(mobile): rewind a chat to an earlier message

Bring desktop's "Rewind to here" to phone chats. The rewind flow moves
into a shared React-injected hook so desktop and mobile share one set of
rules; desktop behaviour is unchanged. The host now lets phones call
agentSession.rewind behind agent-session.rewind.phone.v1, so a phone on
an older host offers nothing it would be refused.

* feat(mobile): put the message time first, then copy and rewind together
2026-11-05 23:06:47 +00:00
NeilandPr1p 4bd2f17e05 fix(managed-server): drop a stale update deferral once the host's activation moves (#38700)
* fix(managed-server): drop a cached update deferral once the host's active build moves

Status reads the host record every time but kept a local deferral until the
host's active version compared equal to or newer than the candidate. Two
builds of the same release differ only by content hash, which compareAppVersions
cannot order, so after the host moved the deferral could stay for days and
the UI kept reporting a pending update that described nothing.

Record the host's active version with the deferral; any change clears it.
This addresses the stale-cache part of the issue; the interrupted-transaction
retry and the supervised-update workflow are separate.

Refs #38400

* fix(managed-server): key a deferral to the host's activation, not just its version

A host that moves to another build and back between two status reads keeps
its version but gets a new activatedAt; the deferral it recorded no longer
describes that host. Compare active and activatedAt, the record identity the
activation code already uses for the same question.

Refs #38400

* fix(managed-server): drop a deferral whose candidate the host already runs

Refs #38400

---------

Co-authored-by: Pr1p <zwq19980411@gmail.com>
2026-11-05 15:05:14 -08:00
Kelvin AmoabaandNarry 1dd6a866f2 feat(omp): ask multiple-choice and batched questions in chat view (#38684)
* feat(omp): show OMP's ask questions in chat

Orca's ACP client never advertised form elicitation, so OMP started in
the chat view left out its ask tool and extension dialogs. The OMP
dialect now advertises forms and maps each elicitation onto the
existing question card: multi-question tabs, multi-select and Other.
PI_NO_PTY keeps `pty: true` bash from opening a TUI overlay ACP can't draw.

Co-authored-by: Narry <3047793+NarryG@users.noreply.github.com>

* fix(omp): tell OMP which ask questions past the fourth were never shown

* test(omp): assert behaviour, not note copy, in ask form tests

* fix(omp): let older clients answer single-question OMP forms

* refactor(acp): answer form elicitations in the generic request path

---------

Co-authored-by: Narry <3047793+NarryG@users.noreply.github.com>
2026-11-05 22:53:09 +00:00
Neil 791bbfbfde fix(remote-runtime): retire a shared-control socket that opens but never becomes ready (#38690)
The shared-control connection now bounds OPEN to ready with a 30s deadline
owned by its readiness timer. When it elapses, the generation closes with a
ready_timeout cause and reconnects through the existing backoff, which only
resets after 30s of stable readiness, so a slow host cannot cause a storm.

Fixes #37330
2026-11-05 14:49:54 -08:00
Kelvin Amoaba 63c874558e fix(native-chat): list OpenCode's own and custom slash commands as soon as the chat opens (#38698)
* fix(native-chat): list OpenCode 2 commands before the chat's first message

OpenCode chats had no at-rest command catalog, so the / menu showed only
Orca's own commands until the first prompt started the agent. Widen the
ACP at-rest catalog to every agent that lists models in a chat-folder
session: OpenCode 2.x opens a throwaway session in the chat's folder,
reads its available_commands_update and deletes it, as the 2.x model
probe already does. OpenCode 1.x cannot delete sessions and is skipped
without spawning.

* test: drop a no-op OpenCode launch-args assertion

* refactor(acp): share the release listing rule and session-delete policy with the model probe
2026-11-05 22:48:30 +00:00
Kelvin Amoaba 7ae45fb656 fix(terminal): ask before closing a tab when Orca can't tell if an agent is running (#38697) 2026-11-05 22:44:53 +00:00
Kelvin Amoaba c884428609 feat(mobile): copy, save and share an image from the full-screen image viewer (#38303)
* feat(mobile): copy, save and share an image from the chat image viewer

* refactor(mobile): pass image viewer actions in so diagrams skip them

* fix(mobile): name the image verb native.image.act and serialize image actions

* fix(mobile): queue image actions per page and skip SVG links

* refactor(mobile): share the cache-folder helper and table the viewer's image actions

* fix(mobile): keep the toast's live region mounted so screen readers hear it

* test(mobile): re-record agent history goldens for the always-mounted toast region

* test(mobile): import the declared routes in the moved routes test
2026-11-05 22:44:36 +00:00