Commit Graph
5250 Commits
Author SHA1 Message Date
Brennan Benson adda205e32 docs(settings): disclose structured chat platform limits 2026-08-26 17:04:21 -07:00
Brennan Benson 5fb357c9db Merge remote-tracking branch 'origin/main' into tmp/native-chat-recovery-main-integration
# Conflicts:
#	src/main/runtime/orca-runtime.ts
#	src/main/startup/desktop-startup-ordering.test.ts
#	src/shared/child-process/__fixtures__/child-process-import-allowlist.txt
2026-08-26 17:04:09 -07:00
Brennan Benson 9135b6f004 feat(orchestration): surface nested worker depth and propagate it across hosts (#16669)
* feat(orchestration): surface nested worker depth and propagate it across hosts

Builds on the depth enforcement in the previous commit, which shipped with the
setting reachable only by editing settings.json and with workers never told they
could nest.

Adds the Settings -> Agents control (a 1/2/3 select rather than a free-form
number, which bounds the value without inventing a numeric input primitive). The
key stays absent from the SettingsUpdate RPC schema, matching agentSkillSharingEnabled:
settings.update is reachable from the CLI, so an RPC-writable depth would let a
worker raise its own cap.

Adds a SUB-DISPATCH block to the dispatch preamble, emitted only when the worker
actually has budget left. A worker told it "usually cannot" delegate still tries and
then reports the refusal as a blocker, so the section is omitted entirely rather
than softened.

Propagates depth to federated worker hosts. Previously the home side computed and
stored a depth the remote host never received, so a remote attachment always read
as depth 1. That is correct at the default cap and wrong as soon as the cap is
raised — precisely when someone starts relying on nesting. The field is optional,
so an older Run home simply omits it and the attachment's NOT NULL DEFAULT 1 keeps
the fail-closed behaviour. Enforcement still runs on the executing host against
that host's own cap, consistent with the SSH execution boundary.

* fix(orchestration): close nested depth readiness gaps

* fix(settings): defer nested depth translations

* fix(orchestration): drop federated depth keys that main already landed

The enforcement PR's review pass added the same federated depth propagation
before it merged, so replaying this branch onto main produced duplicate object
keys. Keep main's versions -- its schema entry validates an integer >= 1 rather
than any finite number.

* fix(settings): label nested worker depth select

* fix(settings): move nested depth to orchestration

* fix(settings): refine nested depth placement
2026-08-26 16:16:05 -07:00
Neil 0096e47850 fix(windows): keep windows-process-tree gyp paths absolute under pnpm (#16688)
* fix(windows): keep windows-process-tree gyp paths absolute under pnpm

Hourly Windows builds have failed since #16598 at
`build-windows-process-tree-relay-addon`: `require('node-addon-api').targets`
is cwd-relative, so node-gyp evaluates it from the pnpm store realpath and
then loads it from the `node_modules` symlink. That resolves
`node_addon_api.gyp` outside the repo.

Use `require.resolve` for an absolute path, matching the node-pty patch.

* i18n: keep ja skill-filter labels on the catalog's Agent brand

#16682 merged with a failing localization catalog: ja used エージェント
in three new skill-filter strings, and repair-locale-catalog rewrites
those to Agent. Match the rest of ja.json so static analysis can pass.
2026-08-26 16:15:03 -07:00
Neil 64c992cd56 fix(memory): report the Windows number that predicts paging, not just resident pages (#16211) (#16589)
* fix(memory): report Windows commit charge, not just working set (#16211)

On Windows the per-process figure was working set — resident pages only.
An agent whose pages Windows has trimmed to the pagefile shrinks its
working set while still holding the commit that pushes the host into
paging, so Resource Manager and `orca diagnostics memory` understated an
owned tree by 10-40x (9 codex.exe: 1.4 GB working set, 13.4 GB private)
and could not warn before the host was already thrashing.

Add committed private bytes as a second, separately-labelled quantity
rather than redefining the existing one:

- CIM sweep gains one property (PageFileUsage, UInt32 KB); the typeperf
  fallback gains one counter (\Process(*)\Private Bytes). Both ride the
  sweep that already runs.
- MemorySnapshot gains optional `privateMemory` per app/worktree/session
  plus `processCommitMetric` and `totalPrivateMemory`. Rule 1 additive
  optional fields: old clients ignore them, and absence reads as "not
  measured", never as zero — Unix hosts and older hosts send nothing.
- `totalMemory` and `processMemoryMetric` keep their exact meaning, so
  the "shared pages may repeat" copy stays true; the working-set copy now
  also says paged-out memory is not counted.
- Resource Manager shows "Σ Private" beside "Σ WS", and tints the badge
  yellow/red once tracked commit passes 60/80% of physical RAM — the same
  thresholds `usageTextColorClass` already uses for host usage. Tint and
  tooltip only; no toast, and the badge number is unchanged.

The parsers move to windows-process-sample-parsing.ts and the Windows
sweep tests to their own file to stay under max-lines.

Not migrating the collector to windows-process-table.ts: the native
snapshot exposes no commit figure and no CPU times, and truncates
WorkingSetSize through a DWORD. Documented in the enumeration reference.

* fix(memory): derive the typeperf field cap from the counter list

The fallback parser's 8192-field cap was sized for three `\Process(*)`
counters. Adding `Private Bytes` cut the parsable process count from ~2730
to ~2047, and overrun is a blackout (`parseTypeperfCsvLine` returns `[]`, so
the whole sweep reports nothing) rather than a truncation. The counter list
now lives beside the decoder that reads those names back out of the PDH
header, and the cap is derived from it.

Also collapses the four spellings of "omit privateMemory when unmeasured"
in collector.ts onto one `commitField` helper, drops the unread parameter
and the never-rendered `columnLabel` from `getResourceCommitMetricCopy`,
folds `getCommitPressurePercent` into the only function that called it, and
reverts unrelated Prettier churn in the Windows enumeration doc.

The commit tint's doc comment no longer claims to predict host paging: it
measures Orca's own share of physical RAM. Host commit charge / commit
limit stays a follow-up (#16211).
2026-08-26 15:43:02 -07:00
Jinjing 614d2d4a28 fix(cmd+j): always enable See more for soft preview hints (#16661)
The leading preview section now shows an actionable 'See more' button
even when all rows fit within the hard cap, letting users expand and
browse more tabs without scrolling past the worktrees section.
2026-08-26 15:33:11 -07:00
Brennan Benson 81f89a705c fix(terminal): bound WebGL context-loss retries on tab reveal (#16338)
* fix(terminal): retry bounded WebGL recovery on tab reveal

* test(terminal): cover reveal repaint and pruned diagnostics

* fix(terminal): make WebGL diagnostics pure and cover reveal refusal

* fix(terminal): correct WebGL retry comments
2026-08-26 15:23:07 -07:00
Brennan Benson d97853c3fe fix(native-chat): reload outbox on session switch 2026-08-26 15:07:42 -07:00
OrcaWinandOrcaWin 7d5c7aa9c3 i18n: Make skill install dialogs and errors translatable (#16682)
Extract hardcoded error messages and status labels from skill
installation components into the i18n system. Supports localized
UI for install flows in English, Spanish, Japanese, Korean, Chinese.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-26 15:04:51 -07:00
Neil d1a11b3299 fix(pty): match the echo shapes a real tty actually produces (#16542)
Reply echo suppression modelled two echo shapes from the spec rather than from
a tty. Captured under node-pty against real bash, at a readline prompt and
under `read`:

  - Readline mangles CSI replies, not just OSC: `ESC [ ?` becomes BEL and the
    residue echoes. The projection was gated on an OSC introducer, so a private
    DSR echo was never matched at a readline prompt. This is the reachable one:
    a mode-2031 theme push (`CSI ?997;1n`) left latched by an exited TUI paints
    `997;1n` on a bash prompt (#9993's scenario).
  - ECHOCTL carets EVERY control, not just ESC. A BEL-terminated OSC reply
    echoes as `^G`, but the needle kept a literal BEL — a string no tty
    produces. Hardening only: every in-tree OSC reply is ST-terminated
    (terminal-osc-color-reply.ts:112, xterm's own reply), so the changed byte
    is unreachable except from a foreign or older emulator.

Why this is not the CSI projection #13160 review dropped: that one was the
identity (`replaceAll('\x1b]', …)` is a no-op on a CSI reply), so it was
ESC-led and 500ms-held bare-ESC tails away from the query parser. This one is
BEL-led. The rule is now asserted for every shape rather than implied by the
gate: holdPartial iff the needle does not start with ESC.

The readline branch is keyed on the private-DSR grammar with a non-empty
parameter list, plus a floor on needle length. The containment grammar admits
`CSI ? n`, and `answerLiveQueryReply` takes client-supplied bytes on the relay
path, so a peer could otherwise arm a two-byte `BEL n` needle and delete the
first bell-then-`n` in ordinary output. #61c65151129 proved this system can eat
real output when a needle outlives its budget; a length floor is cheap.

Live coverage: pty-reply-echo-shapes.node-pty.test.ts writes a reply to a real
bash master and feeds back what it echoes, so a shell or libc change fails the
suite instead of silently disarming suppression. Registered in the
shell-contracts lane. The transcript tests and the caretEcho helpers that
encoded the same ESC-only assumption are corrected alongside.

Suppression is display-only. This does not change what reaches the child's
stdin — the reply is written to the master either way, in call order.
2026-08-26 14:36:45 -07:00
Brennan Benson a624e7cd5d test(agent-status): inventory legacy pane identity surfaces (#16575)
* feat(agent-status): measure identity evidence before migrating any consumer

PR 1 of the identity migration. It changes no displayed or routed identity — it only measures.

Why measure first: the hierarchy shipped in #16148/#16157 has zero consumers, while ~31 sites still
derive identity independently. Every migration decision after this is currently a guess, including
the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No
Claude status shown", and this design trades toward showing nothing when uncertain, so the blank
rate has to be a number before any surface moves.

- `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers
  stop each inventing their own ladder.
- `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host /
  wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of
  which sources were present and whether the resolver returned null or ambiguous. No titles,
  prompts, paths, handles, or agent text.
- `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper
  gains a new production caller, so the surface cannot grow while the migration runs.

Three review findings are encoded rather than deferred: launch stays above run-key-less completed
hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized
before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read
as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host
observes the distro wrapper rather than the agent inside it.

The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay
cohorts collected from real use, and that review is the gate on PR 3 — not this PR.

* test(agent-status): keep identity migration inventory-only

* test(agent-status): reuse reliable source scanner

* test(agent-status): bound inventory scan work

* test(agent-status): avoid inventory path false negatives

* test(agent-status): refresh identity inventory after base repair

* test(agent-status): correct inventory classifications

* test(agent-status): correct action boundary inventory

* test(agent-status): pin inventory occurrence counts

* test(agent-status): fail closed on scanner desync
2026-08-26 14:33:02 -07:00
Brennan Benson 7008d55fa0 Merge remote-tracking branch 'origin/main' into brennanb2025/native-chat-current-main 2026-08-26 14:32:11 -07:00
Jinjing 87ede54eb8 Show all automation destination hosts, disable ineligible ones (#16665)
* Show all automation destination hosts, disable ineligible ones

Previously, filtering to only eligible hosts hid all connected hosts
on pre-host-scoping Orca servers. Now all offered hosts appear in the
picker; ineligible ones are disabled with a message naming which
servers need updating to support them.

* Show all automation destination hosts, keep create available when all ar

- Gate the create button on what the picker offers, not on readiness: with every
  offered host ineligible (e.g. all pre-host-scoping servers), the dialog is
  where the repair is stated, so the button must still open it.
- Fix StrictMode double-mount lifecycle: disposed controller revived by effect,
  unsubscribe before dispose to prevent event leaks under simulated unmount.
- Validate create destination early, before hooks load and trust prompt, so the
  user never answers a trust dialog for a destination that would reject.
- Dedupe capability probes per authority incarnation: concurrent callers share
  one in-flight status.get; confirmed capabilities never re-probed.
- Drop cache payload at retirement so revived hosts refetch instead of showing
  stale rows.

* Add TTL-based capability probe caching and fix automation dialog target

Extract capability probing to a separate module with improved caching strategy: confirmations now expire after 60 seconds and the cache is bounded to 32 entries, enabling in-place runtime replacements to invalidate old confirmations. For uncaptured automation owners, resolve the dialog target to the same host the save addresses rather than relying on ambient context, preventing stale host references. Optionally await external managers after mutations to ensure row re-reads reflect recent writes.

* Fix automation tests after rebase

* Refactor capability probe to fence fencing checks from in-flight probes

Fencing checks need fresh probes since in-flight probes may predate
in-place runtime replacements. Extract shared probe deduplication
into `sharedCapabilityProbe()` and unconditional probe start into
`startCapabilityProbe()`, then route based on cache preference.
2026-08-26 14:31:32 -07:00
Brennan Benson f6eb09d000 test(native-chat): cover retry after unconfirmed send 2026-08-26 14:31:32 -07:00
Brennan Benson f6ee87c02f Merge remote-tracking branch 'origin/main' into brennanb2025/native-chat-current-main 2026-08-26 14:23:36 -07:00
Jinjing aab6464a6a feat(editor): implement Shift+Tab to unindent lists and code blocks (#16677)
Add keyboard handlers for Shift+Tab that unindent code block lines and
lift nested list items. Properly handles mixed bullet/task nesting by
retyping items to match their enclosing list. Provides symmetric control
over indentation to complement Tab's indent behavior.
2026-08-26 14:09:04 -07:00
Jinjing fda213a4e8 Improve automations table layout and column sizing (#16667)
* Improve automations table layout and column sizing

- Wrap table containers with min-width constraint for horizontal scrolling
- Adjust grid column widths for better visual balance
- Simplify automation draft building with helper function
- Remove unused validation checks and imports

* Make automation list first column sticky

- Keep automation name visible when scrolling horizontally
- Adjust header z-index to layer above sticky cells

* Remove unused canCreateAutomation prop from test
2026-08-26 14:03:46 -07:00
Brennan Benson cbefe2feca fix(native-chat): retire hosted rows on structured tab activation 2026-08-26 13:59:17 -07:00
Brennan Benson 803d16b4ea Merge remote-tracking branch 'origin/main' into brennanb2025/native-chat-current-main
# Conflicts:
#	src/main/index.ts
2026-08-26 13:56:48 -07:00
Brennan Benson c8baf4eaaa fix(native-chat): close stale turns and retry rejected sends 2026-08-26 13:41:24 -07:00
Brennan Benson c1dab11f49 fix(native-chat): keep chat tabs visible through terminal closes and empty-worktree launches
Two proven blockers in the native Codex tab contract:

closeTerminalTab pre-empted the canonical unified close. With one terminal
left it deactivated the worktree on a terminal/editor/browser-only check,
blanking a workspace that still held a renderable agent-session tab; with
two or more it pre-picked a successor from terminal entities only,
re-stamping the group active before closeUnifiedTab's MRU/neighbor repair
could land on the chat tab. Successor choice now defers to the unified
contract whenever the terminal has a unified row, and deactivation is
gated on the unified renderable count (matching leaveWorktreeIfEmpty),
with the legacy pre-pick kept only for terminals without a unified row.

A structured session created on an empty worktree was published into the
host's headless group while preserveLocalLayout froze the local layout,
leaving the tab in store but permanently off screen. A preserveLocalLayout
owner now always takes client-owned placement — repairing a rendered
leaf whose group record is missing, or materializing a rendered group on a
truly empty worktree — and applies the client-derived layout repair while
still rejecting host-authored layout.

Regression tests drive the real store through closeTerminalTab (git
worktree and folder workspace) and the real snapshot applier for the
empty-worktree adoption states; all fail without the fixes.
2026-08-26 13:24:59 -07:00
Brennan Benson 256f23c7a0 fix(automations): validate create destination projects 2026-08-26 12:58:41 -07:00
Brennan Benson 588eec68b4 fix(native-chat): stop rendering a tool result whose call is outside the window (#15653)
* fix(native-chat): stop rendering a tool result whose call is outside the window

A tool result carries no call id, so it can only be attributed to a tool
call loaded alongside it. Both chat views read a windowed transcript tail
(mobile 40 messages, desktop 300), and the window regularly opens between
an assistant's `tool_use` record and the user-role record that answers it.
Claude also re-emits already-answered `tool_result` records at a `/compact`
boundary, long after their call scrolled out of the window.

`foldToolMessages` had no rule for those: with no assistant predecessor in
the output they were pushed through as standalone messages and rendered as
a bare, unowned block of raw tool output with no tool name — reading as a
message from nowhere mid-conversation. Sampling real Claude transcripts,
176 of 400 sessions (44%) produced one in a mobile-sized first page.

Drop a result no loaded call can own, before folding. It is not lost: it
comes back attached to its call as soon as the owning turn pages in.

* fix(native-chat): scope tool result attribution to folded turns

* fix(native-chat): preserve harness-attributed tool results

* fix(native-chat): keep interruption boundaries
2026-08-26 12:38:59 -07:00
Brennan Benson d9c77c5830 fix(automations): restore main checks 2026-08-26 12:21:48 -07:00
hwantage b755629f37 feat(i18n): add Korean translations for CLI-created workspace labels (#16212)
- Localize filter toggle labels in SidebarFilter and SidebarWorkspaceFilterSection.
- Localize card detail descriptions in WorktreeCardCliDetailSection.
- Localize meta badge accessibility label in WorktreeCardMetaBadges.
- Resolves English fallback for CLI-created workspace UI under Korean locale.
2026-08-26 11:47:06 -07:00
Brennan Benson 290b4b3b4a feat(native-chat): port structured Codex sessions from restructure-recovery
Rebuilds the desktop structured native-chat implementation from
brennanb2025/native-chat-restructure-recovery (tip 4e31c08db3) on top of
current main as a single commit, scoped to the local Codex path.

Ported:
- Structured agent-session core: durable record store + single-writer lease,
  canonical journal, agent-session wire host/attach/eviction/subscribers,
  `agentSession.*` RPC surface (registered via ALL_RPC_METHODS; host-side
  mobile allowlist included for wire compat), pty write gate, transcript
  additions, and the Codex app-server adapter/launch resolution.
- Renderer: NativeChatStructuredSession view/composer stack, structured
  launch path with the single-flight guard, local structured session tabs
  sync, activation gate + structured inventory (read-only
  `agentSession.handoffStatus` probe), agent-session tabs in the tab strip,
  AI-vault structured session activation, and the settings pane with the
  parent Experimental Chat UI toggle plus the nested "Use updated structured
  native chat" toggle. New sessions require both flags, agent codex, no
  prompt, and a local non-WSL, non-Windows-host execution host
  (structured-native-chat-availability).
- Fixes 72c013cea6 (verified Codex launch recovery), 8ddbaf5e3d (defer
  native terminal view switching affordances), and 4e31c08db3 (release the
  launch gate after a visibility retry) with their regression tests,
  including the third-launch-after-retry guard case.
- Cross-version agent-session wire test + CI lane, packaging entries
  (proper-lockfile, agent-tooling asar excludes), and the wire-compat doc
  section.

Deliberately not ported: mobile/ changes, the Claude structured runtime
(only the claude-transcript-branch-proof and claude-structured-owner-identity
leaf modules remain, backing the kept TUI-recovery arms), the terminal↔chat
adoption/handoff flow (`agentSession.adoptTerminal`/`requestHandoff`, the
handoff request engine, TUI adoption machinery, orca-runtime adoption
methods), renderer switching affordances and their dead leftovers, the
hook/subagent-status refactor cluster, and unrelated branch changes. The
crash-during-acquisition recovery path (restart handoff adjudication,
restore/reverse re-acquire, lease schema handoff keys) is kept because every
plain direct launch depends on it; a trimmed handoff coordinator exposes
only status/restore/close.

Branch edits that targeted files main has since split (ipc/pty.ts,
worktrees.ts, rpc/methods/terminal.ts, useIpcEvents, pty-connection,
store/slices/terminals.ts, runtime-types, web preload) were re-applied to
the split modules, preserving main's newer logic (Windows CIM fallback,
browser tab close rework, cold-restore resume flow, dispatcher threading).

Known seam: the mobile clipboard image-provenance CONSUMER gate ships
(agentSession.send refuses unproven mobile image refs with
agent_session_image_untrusted) but the producer hunk in
rpc/methods/clipboard.ts stays with the unported mobile cluster, so mobile
image sends into structured chat fail closed until that side ports.
2026-08-26 10:47:56 -07:00
Jinjing cda2280d63 Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo

Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.

* Filter automation create projects by destination host

Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.

* Add runtime storage authority support for automations

- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata

* Replace child_process.execFile with runProcess for external automations

- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)

* Unify desktop automation CRUD onto the local runtime RPC surface

The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).

The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.

External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).

* Remove automation ghost SSH tombstone scanning

This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.

* Refuse orphan automations at dispatch time, not migration time

Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.

* Show all automations in flat table with unified filter menu

- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components

* Add automation owner fencing and destination validation

- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers

* Route automation recovery actions to the origin host

When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.

* Remove external manager scope limitation notices

Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.

* Persist only store-derived automation contexts, not client-perspective o

Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
2026-08-26 09:50:12 -07:00
Neil 885106baee fix(terminal): stop routing unowned workspaces to the focused runtime (#16584)
`createWebRuntimeSessionTerminalResult` collapsed an explicit
`environmentId: null` ("I resolved ownership and nobody remote owns this")
into "caller said nothing", then fell back to
`settings.activeRuntimeEnvironmentId`. The tab-strip "+" shell rows and the
guest-focus Ctrl+T relay both pass that explicit null, so a local workspace's
new terminal was created against whatever remote runtime happened to be
focused, which answered `selector_not_found` for a worktree id it had never
seen.

The same call selects the runtime as the workspace's execution host before
the create, and the error path never handed that selection back — leaving the
workspace latched to the runtime that just refused it, so every later
owner-routed action (the next Ctrl+T included) silently followed the latch
until a workspace switch reset it.

Fixes #16444
2026-08-26 04:37:17 -07:00
Neil 691759540a fix(terminal): blur suspended panes on the dispose branch too (#16592)
suspendPaneRendering blurred panes only on the WebGL-retention branch; the
dispose branch — taken by every pane past MAX_RETAINED_HIDDEN_WEBGL_CONTEXTS=6
— did not. Make it unconditional so both branches leave a suspended pane in the
same state.

No measured cost is being fixed, and the earlier cursor-blink-timer rationale
was wrong. Measured on Windows 11 against the shipped @xterm/xterm
6.1.0-beta.287 + @xterm/addon-webgl 0.20.0-beta.286, N=12 panes: display:none
and inert each make Chromium fire a real blur on the pane's helper textarea,
which pauses the WebGL blink interval on its own, and disposeWebgl() disposes
the blink manager regardless. Hidden panes measured 0 interval fires and 0 rAF
fires over 8s with and without the explicit blur. Focus is also a document-wide
singleton, so "one timer per hidden pane" was never possible.

Kept as defence in depth for opacity:0 without inert — TerminalOverlaySlot's
startup probe inside an active worktree — the one hide mode that keeps focus.
2026-08-26 03:12:03 -07:00
Neilandsanshengai e2cb797506 perf(sleep): park idle agents in the worktree you are working in (#16591)
The planner skipped the entire activeWorktreeId, so the tree a user actually
works in never parked anything — exactly where a 16 GB Windows host
accumulates its idle Codex/Grok panes and starts hard-paging (#16211).

The two guards that remain are the correct granularity and already existed:
foregroundTerminalTabIds covers the tab on screen, and the
foregroundTerminalLastSeenAtByTabId floor in getEligiblePane holds any tab
left inside the idle window.

Test lever taken from @sanshengai's #16214, which found this first: pinning
the existing sibling-tab regression to activeWorktreeId means it fails against
the pre-fix planner. A standalone background-worktree case does not, because
the fixture's active worktree is a different one — that is why the first cut of
this change shipped a vacuous test.

#16214 changed only the planner suite; the same one-line change also breaks
agent-hibernation-coordinator's two revalidation tests, which used
activeWorktreeId as their eligibility lever. Those now flip
setForegroundTerminalTabIds instead, which is the property they were written
to prove.

Co-authored-by: sanshengai <sanshengai@users.noreply.github.com>
2026-08-26 03:11:29 -07:00
Neil 87f5c6cd03 perf(terminal): stop rebuilding parked-watcher keys on every overlay render (#16596)
* perf(terminal): stop rebuilding parked-watcher keys on every overlay render

Every mounted worktree's TerminalPaneOverlayLayer rebuilt its parked-watcher
synchronization key from scratch on every render: JSON.stringify of the whole
split-tree root per tab, then a second JSON.stringify pass that re-escaped that
already-serialized string. Two app-global subscriptions in the cold-parking
hook (pendingStartupByTabId, sleepingAgentSessionsByPaneKey) made any write for
any tab in any worktree trigger that render everywhere at once, so the cost
scaled with mounted worktree count.

- Memoize the store-derived half of the reconciliation key on the already
  shallow-stable selector output. The captured-pane half still recomputes per
  render because that registry mutates outside React.
- Replace the outer JSON.stringify of already-serialized fragments with a
  length-prefixed join, which is injective for arbitrary fragments and does no
  escaping pass.
- Narrow both global subscriptions to worktree-scoped, value-comparable keys.

Measured on a 12-worktree x 4-tab x 4-leaf-split model: 9.5 us -> 1.3 us of key
work per worktree render (7.3x), before counting the renders the narrowed
subscriptions now avoid entirely.

Key semantics are unchanged: no hash is introduced, only memoization of an
identical serialization and an injective replacement for the outer pass.

* refactor(terminal): narrow the park subscriptions with useShallow, not string keys

Review follow-up. zustand's `shallow` already compares Sets and plain objects
structurally and order-insensitively, so the encode-to-string / parse-back pair
each subscription carried was doing by hand what `useShallow` does for free.

- Restore the Set-returning `selectSleepingRecordParkExemptTabIds` and subscribe
  through `useShallow`. Drops the NUL separator, the `.sort()` that existed only
  to keep insertion order out of the key, the O(k^2) `includes` dedup, the parse
  helper and the caller's `useMemo` — and removes the ordering invariant that
  was enforced by a comment alone.
- Same for the pending-startup presence hook: `useShallow` over the presence
  record, keeping the frozen empty singleton for the zero-allocation steady
  state.
- Drop the `useMemo` around the reconciliation selector. `useShallow` returns a
  fresh closure every render regardless, so the memo bought nothing and its WHY
  comment described behaviour zustand 5 does not have. The memo that is the real
  fix here, `reconciliationStoreInputsKey`, is untouched.

Adds a narrowing case for a sleeping record this worktree can never resume,
which pins both the blocked-record exemption and the narrowing itself; it fails
against the pre-narrowing code (2 renders, expected 0).

Net -25 lines of production code.
2026-08-26 03:08:11 -07:00
Neil 1fafccb26b fix(settings): use Workspace Directory for the Create-project default path (#14767) (#16583)
* fix(settings): use Workspace Directory for the Create-project default path

`repos:getDefaultCreateProjectParent` hardcoded `join(homedir(), 'orca',
'projects')` and never consulted the settings store, so Settings -> General ->
Workspace Directory had no effect on the Location field of "Create new project".
Users had to retype the path every time, or fake it with an NTFS junction.

Resolve the parent from the store instead, through the same rule the rest of the
app uses for a host preference: `host override ?? client default`, i.e.
`getEffectiveHostSetting(settings, LOCAL_EXECUTION_HOST_ID,
'defaultWorktreeLocation', settings.workspaceDir)`. This handler only ever
answers for the local host, and a local-host override previously could not win
either.

A seeded value is not a user choice. `workspaceDir` is never blank -- new
installs seed it with `~/orca/workspaces` -- so treating any non-blank value as
configured would silently relocate every existing user's new projects into the
worktree root. Worktrees nest at `<workspaceDir>/<repoName>/<branch>`, so such a
project would then host its own worktrees inside its own working tree. Compare
against `getDefaultWorkspaceDir(homedir())` (now exported) via
`normalizeRuntimePathForComparison`, and keep `~/orca/projects` for blank,
whitespace-only, and untouched-default values.

Also scope the `~/orca/projects` shorthand in `formatCreateProjectParentSummary`
to the fallback path itself. Otherwise a user with Workspace Directory set to
`J:\PROJECTS` saw the summary line claim `~/orca/projects` while the field held
`J:\PROJECTS`.

Fixes #14767

* fix(settings): keep configured orca/projects paths verbatim in the create summary

The collapsed Location summary used a tail match on orca/projects, so a
configured directory like /data/orca/projects rendered as ~/orca/projects.
Scope the shorthand to usual home layouts and pin the lookalike cases.
2026-08-26 02:36:38 -07:00
Neil a27c691fdd fix(terminal): stop detached exit observers pinning evicted panes' xterm buffers (#16551) 2026-08-26 02:05:58 -07:00
Neil 2f5f5ce23c fix(ui): restore the light-mode dropdown shadow (#16570) 2026-08-25 23:43:28 -07:00
Jinwoo Hong 3c5c908451 fix(automations): scope project refs to destination host (#16552) 2026-08-25 23:32:21 -07:00
Neil f72dcb908e fix(contextual-tours): stop measuring 60 times a second while nothing moves (#16453) 2026-08-25 22:38:33 -07:00
Jinjing 933345d347 Clarify upstream divergence stats for rebased branches (#16358)
* Clarify upstream divergence stats for rebased branches

When a branch is rebased, it still tracks the pre-rebase upstream
while comparing against the new base. Move upstream arrows to the
head line to prevent them being confused with compare-base counts.

* Show upstream divergence stats independent of compare base

Measure HEAD against upstream regardless of compare-base state,
so divergence indicators stay visible even when comparison is
missing, loading, or failed. Also use cross-platform temp paths
in tests.

* Show commit counts against compare base, not upstream

Upstream divergence (↑/↓ against tracking branch) was confusing for
rebased branches — the counts appeared beside the base ref but measured
against the upstream branch. Show only the compare base count instead,
on the line that names it.

* Report branch divergence in both directions

Rebased branches are typically ahead AND behind their base; a single count
hides this case. Use symmetric range with --left-right --count to capture
both directions efficiently, then expose commitsBehind in the UI alongside
commitsAhead.

* Use semantic names for i18n keys and template variables

Rename hash-based translation keys to descriptive identifiers and replace generic value0/value1 placeholders with semantic variable names like `count` and `ref`. Improves code maintainability and makes translation strings self-documenting.
2026-08-25 22:19:04 -07:00
Jinjing 07b82340f3 Route terminal file links to sibling workspace tabs (#16544)
* fix: route terminal file links to sibling workspace tabs

Detect when a clicked file is already open in a sibling workspace and route
to that existing tab instead of creating a duplicate. Reorganizes workspace
activation to dispatch by both worktree id and execution host, allowing the
same worktree name across different remotes to be disambiguated and routed
correctly.

* test: validate terminal file link opens in correct sibling worktree

Enhance test to check both file path and active worktree ID, ensuring
the linked file opens in the intended sibling workspace.
2026-08-25 22:17:59 -07:00
Jinjing 6a3bd2a1b8 fix: keep tab-cycle shortcuts in sync with rendered group order (#16549)
Tab-cycle shortcuts (Ctrl+Tab) were getting out of sync with what the
TabBar actually renders. When a tab hydrated into the strip before
group.tabOrder was updated, it fell out of the cycle until a click.

Align keyboard cycling to use the same reconcileTabOrder pass the
TabBar uses, so the cycle always walks what the user sees. Fixes STA-3475,
particularly in remote servers where hydration timing diverges from
local.
2026-08-25 22:10:29 -07:00
Neil b57b812e72 fix(ssh): recover initial state hydration
Hydrate SSH connection states independently of best-effort tombstone labels, with bounded fanout and regression coverage.
2026-08-25 21:52:52 -07:00
Jinjing 5479bd9159 refactor(task-page): split task page into focused modules (#15163)
* rm unused files

* rm unused files

* fix(task-page): clean readiness lint findings

* Add GitLab IPC timeout wrapper and improve error handling

- Extract GitLab timeout logic into reusable `withGitLabIpcTimeout` wrapper to protect all GitLab API calls from hanging indefinitely
- Apply timeout protection to all GitLab list and fetch operations
- Add error handling for GitHub and Linear issue creation operations
- Fix event bubbling in GitHub work item row to prevent nested button clicks from opening detail page
- Remove unused `usePRReviewCellState` hook
- Consolidate redundant imports

* refactor(task-page): extract components and improve provider handling

- Add glab timeout handling (30s) to prevent IPC thread blocking
- Extract GitHub assignee/review components to dedicated files
- Improve GitLab work item row keying (repoId:id) and keyboard event handling
- Add context-aware error handling for Jira creation failures
- Refactor GitHubAssigneeAvatar to use shared GitHubUserAvatar component

* Add timeout support and error handling for GitLab operations

- Admission control times out queued work after 30s to prevent
  indefinite queueing behind saturated operations
- Mutation errors now display to users via toast instead of failing
  silently

* Consolidate workspace attachment labeling into unified utility

Extract common label-generation logic from GitHub and Linear
work-item components into a single getWorktreeAttachmentLabel
function, removing duplication across attachment types.

* Improve TaskPage accessibility, i18n coverage, and error handling

- Add missing aria-labels, roles, and semantic attributes for improved screen reader support
- Extract hardcoded UI strings into i18n system with translate() calls
- Add error handling and proper abort signal support for async operations
- Use locale-aware date formatting throughout
- Fix pagination disabled state and reviewer suggestion merging logic
- Improve async state management with proper refs and effects
- Add Textarea component import for Jira dialog

* Improve TaskPage accessibility and i18n key naming

- Add DialogTitle/Description with i18n to Linear issue dialog
- Use useId to improve aria-labelledby in GitHub selectors
- Replace hash-based i18n keys with semantic names
- Use Object.hasOwn instead of `in` for safer filter checks
- Fix PR review cell to clear input only on success

* Add missing dependencies to TaskPage hooks and useCallback/useEffect arr

Fixes exhaustive-deps warnings by adding missing setters, refs, and computed
values to dependency arrays. Refactors GitHub and Linear issue state handling
to compute values from pageData where available, with fallback to local state.
Moves imperative ref updates into useEffect to properly track dependencies.

* Fix TaskPage ref timing and null repo selection state

Treat null newIssueRepoId as a valid selection, and use useLayoutEffect to synchronize the provider context ref before paint rather than after.

* Extract Linear issue dialog components and fix popover scroll styling

- Consolidate scroll styling: apply popover-scroll-content and scrollbar-sleek classes to PopoverContent wrappers
- Remove redundant max-h-60 overflow-y-auto styles from inner picker divs
- Fix GitHub new issue repo selection to explicitly target first selected repo on fresh mount
- Correct CacheEntry import paths from store/slices/github to store/github/cache-model
- Update tests to reference extracted dialog components instead of TaskPage.tsx

* Improve GitHub task page i18n and fix issue creation edge cases

- Add i18n support to GitHub work item aria-labels (draft PR, PR, issue)
- Optimize work item row by extracting repeated source context call
- Add safety check to prevent opening detail page when issue URL is missing
- Fix dependency reference in detail opener hook
- Extend GitLab job trace timeouts (60s backend, 65s frontend) for slow logs

* Increase GitLab job trace fetch timeouts

Job traces can outlive the runner's 30-second default timeout.
Extend fetch operations to allow 60–65 seconds to complete.

* Verify sourceContext variable extraction in github row test

Update expectations to check that sourceContext is assigned to a
variable rather than called inline, matching the refactored component
implementation.
2026-08-25 21:28:21 -07:00
Brennan Benson 290f192d84 fix(updater): surface and degrade renderer shutdown checkpoint failures (STA-5505) (#16497)
* fix(updater): surface and degrade renderer shutdown checkpoint failures

The in-app updater could refuse to install with 'Renderer shutdown
checkpoint was not completed.' while the actual persist() error was
swallowed unlogged, leaving users stranded on old builds (STA-5505).

- report the swallowed persist error: console, crash breadcrumb, and a
  cross-world DOM attribute so the thrown error (and the Update Error
  dialog) names the underlying cause
- stop failing the checkpoint on sleeping-agent quit-capture errors; the
  periodic capture bounds the loss to one minute
- extend the existing durable-session degradation to full-session staging
  failures during an intentional restart, preserving the dirty-draft guard

* fix(quit): degrade and surface checkpoint-vetoed app quits (#15352)

Cmd+Q walked the same shutdown checkpoint as the updater: a persist()
throw preventDefault()ed the synthetic beforeunload and
confirmNativeWindowClose returned silently — quit accepted, nothing
logged, SIGKILL the only exit.

- run the quit checkpoint inside a window-close scope so full-session
  staging failures degrade to the durable tier for app-level closes too
  (dirty editor drafts still hard-block)
- when the checkpoint still vetoes the quit, toast the published failure
  reason instead of dying silently

* fix(updater): retry-then-degrade staging and honest capture-loss accounting

Review findings on the first pass:
- a first full-session staging failure now stays a visible, retryable
  error; only a repeat failure degrades to durable-only staging, so a
  transient IPC failure keeps its retry instead of silently dropping
  just-captured scrollback
- the sleeping-capture comment no longer overstates periodic coverage
  (periodic mode skips done panes and never stamps quit origin); the
  swallowed failure records a crash breadcrumb
- pin the exact degradable-shutdown gate expression in the source-shape
  test so rewiring it cannot pass silently

* fix(updater): arm the staging-retry flag only for degradable shutdowns

An unrelated unload's staging failure must not burn the visible first
retry of a later restart or quit.

* fix(updater): isolate shutdown checkpoint retries

Reset full-session staging retry state when a shutdown attempt is abandoned, and route Terminal-less closes through the same scoped synthetic checkpoint as mounted workspaces. Keep arbitrary thrown-value diagnostics non-throwing and localize the quit failure toast.

* fix(updater): preserve checkpoint retry lifecycle

* fix(updater): preserve empty checkpoint failure reason
2026-08-25 21:14:20 -07:00
Jinjing 5e5457983a Add clickable See more button to palette section overflow hints (#16533)
* feat: Add clickable See more button to palette section hints

Allow incremental expansion of capped sections (worktrees, tabs, projects) by clicking "See more" to reveal 20 additional entries per section. Replaces static "X more" messages with interactive expansion that resets when the query changes.

* Make soft preview See more non-clickable when no rows are hidden

- The soft preview hint's expand button is only actionable when rows are
  hidden beyond the hard cap (leadingHardOverflowCount > 0)
- When all rows already render, expanding would only reshuffle already-visible
  content without revealing anything new
- Pass undefined as the handler to prevent the click behavior in this case
- Add test case to verify the button doesn't appear when all rows fit
2026-08-25 20:23:30 -07:00
Neil 91a500712c fix(crash-reporting): see the renderer memory the heap counters never report (#16449)
* fix(crash-reporting): see the renderer memory the heap counters never report

Windows renderer crash 36048e26 arrived with 618MB of private renderer memory
and a `renderer_memory` breadcrumb reporting a 150MB V8 heap. Both numbers were
right: xterm scrollback lives in `Uint32Array` backing stores and glyph atlases
live in GPU transfer buffers, and neither is counted by `usedHeapSize`,
`mallocedMemory`, or Blink's allocator.

That made the report unanalyzable. `renderer_memory_highwater` is the crumb
carrying the subsystem census that names what grew, and it is armed on
`usedHeapSize / heapSizeLimit`. At 150MB of a 4192MB limit that ratio is 3.6% —
nowhere near the 60% mark — so the census never reached a single one of these
reports.

Measured on Windows (6 worktrees x 4 terminal tabs, 8000 lines each, this app
at 4218d505): filling 24 mounted panes moved the renderer working set from
210MB to 656MB while `usedJSHeapSize` stayed at 43MB for the whole run.

Sample the renderer's own OS footprint through `process.getProcessMemoryInfo()`
(available in the sandboxed preload) and:

- report `privateMB`, `residentMB`, and `outsideHeapMB` — the footprint minus
  everything V8 and Blink admit to holding — on every `renderer_memory` crumb;
- arm the highwater census on private-footprint marks (600MB / 1000MB) as well
  as the heap ratio, so growth outside the JS heap now carries the pane and
  store census that names it.

The footprint read is async, so a sample annotates with the previous read and
refreshes in the background: one interval of staleness is irrelevant to a
footprint trend, and awaiting it would make every sample reentrant. A shell
without the bridge, or a runtime that withholds the read, keeps sampling
exactly as before.

Retained-breadcrumb keys now distinguish the two threshold ladders; keying only
on `thresholdPct` collapsed every footprint crumb onto one slot.

crash-diagnostics.ts split at the max-lines budget: memory sampling moves to
renderer-memory-sampling.ts and the shared payload shaping to
crash-breadcrumb-data.ts.

* fix(crash-reporting): retain all renderer memory marks
2026-08-25 18:38:42 -07:00
Brennan Benson 630b71730b fix(sleep): restore agent auto-hibernation for non-Pi agents (#16430)
* fix(sleep): let non-Pi agents hibernate again, and stop repaints resetting the idle clock

Auto-hibernation could never fire for claude, codex, gemini, opencode, grok, or
any other resumable TUI agent — only pi/omp/prime-agent.

#10238 broadened the `origin: 'live'` resume anchor so every resumable agent
keeps its `--resume` handle when a turn ends. The planner rejects any pane that
already has a sleeping record, and its exemption was still Pi-only. Since the
planner's eligibility conditions are the same conditions that write the anchor,
that rejection covered every otherwise-eligible non-Pi pane.

- Split the conflated predicate. `isLiveResumeAnchorForCompletedAgent` answers
  "is this record just this pane's own live anchor?" with no vendor gate; the
  Pi-gated wrapper keeps today's exact semantics for the manual-sleep and quit
  capture call sites; `isAutomaticHibernationAllowed` carries the
  `automaticResumeBlockedBy` fence on its own.
- Fence automatic hibernation. A fenced worker must not be auto-relaunched, and
  the capture does not copy the flag — so hibernating one would erase it. Checked
  in the planner and again inside the shutdown action against freshest state,
  re-evaluated after the synchronous capture callback that could itself fence it.
- Anchor the idle clock on `stateStartedAt`, not `updatedAt`. Same-state
  repaints (OSC 9999, reconnect replays) advance `updatedAt`, restarting the
  30-minute countdown and invalidating the two-tick confirmation.
- Floor that anchor on PTY-binding age and a boundary-resolution stamp, so a
  wake or app restart still gets a full idle window instead of sleeping the
  whole backlog on the ancient timing main replays. The boundary stamp is
  written synchronously where the flag clears; sampling it per tick would miss
  a boundary written and cleared between two samples.
- Signature drops `updatedAt` and gains agent kind plus full resume identity,
  which is the change detection `updatedAt` was providing by accident.

Splits the planner into planner / pane-eligibility / snapshot to stay under the
file length limit.

* fix(sleep): drain pane teardowns sequentially

`runAgentHibernationTick` launched every confirmed shutdown unawaited, so a backlog
fanned all of them out at once. Each shutdown re-runs a full runtime-liveness sweep
(one `terminal.list` per runtime-owned worktree, 10s timeout) and then a
`terminal.stopExact` (15s timeout) — so ~100 overdue panes meant ~100 concurrent
sweeps plus ~100 concurrent stops plus interleaved persistence writes. On an SSH
runtime that is hundreds of near-simultaneous RPCs at the relay.

The fanout predates this branch, but auto-hibernation could not fire for non-Pi
agents, so it never ran at scale. Restoring eligibility is what exposes it.

Awaiting each teardown also makes `tickInFlight` real: it was cleared in the
`finally` as soon as the promises were launched, so it never covered the drains it
was meant to guard. Each candidate still re-validates against a fresh plan at its
own turn, so a slow drain cannot act on stale confirmation, and per-candidate
failures are already caught so one stuck teardown cannot abort the rest.

* perf(sleep): scope hibernation rechecks to pane owner
2026-08-25 18:22:53 -07:00
Jinwoo Hong c8567eb16e fix(sidebar): preserve hidden rows in manual order (#16488) 2026-08-25 16:46:42 -07:00
Brennan Benson efa3b972c2 fix(native-chat): prevent duplicate mobile prompt echoes (#15656) 2026-08-25 15:47:40 -07:00
Brennan Benson 29f1f4e545 test(perf): warm palette matcher before timing
Warm the palette matcher before measuring steady-state p95 performance.
2026-08-25 15:42:13 -07:00
Jinwoo HongandJinwoo-H a9781a4118 STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai>
2026-08-25 15:36:51 -07:00
Brennan Benson 98bdd653ab fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript

A brand-new agent session can take minutes to write its first JSONL line,
and one that is never prompted never writes it at all. The host emitted no
stream frame until the file resolved, so every native-chat client sat on a
bare spinner with the composer enabled but the transcript blank -- forever,
in the never-prompted case.

The resolve poll now reports the transcript as pending after a short grace,
and both host handlers emit a `pending: true` snapshot. It is deliberately
not a plain empty snapshot: an empty window sold as a settled read would
capture over retained history and unblock consumers that require a
trustworthy transcript (the launch-draft adoption would re-offer a prompt
the agent may already have taken).

Clients render it as the "start a chat" empty state while keeping the read
unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on
desktop, which also stops the seed loop expiring into an error card for a
session that is simply new. New optional field only, so older clients
ignore it and still stop spinning.

* fix(native-chat): negotiate pending transcript frames
2026-08-25 15:06:47 -07:00