`getScopedRecord` returned an existing record without re-inserting it, so the
per-tab record map evicted by insertion order rather than recency. A long-lived
active tab is the oldest key, so crossing the 32-scope cap silently dropped its
tracked model and reset the pill to "Model". Desktop's scope cache does
delete-then-set for exactly this reason.
Also moves the shared session-option tests to src/shared so the root suite runs
them (they only exercised src/shared logic the Electron renderer consumes, but
sat under mobile/ where only mobile's vitest project sees them), and restores
two "why" comments dropped while extracting the shared modules.
Piece A — shared slash catalog + send classification:
- Mobile composer now serves getVerifiedNativeChatCommands from the shared
catalog (agent-aware, with description rows) instead of a hardcoded
provider-agnostic list that advertised commands Claude does not have.
- classifyNativeChatSend moves to src/shared/native-chat-slash-commands.ts
(renderer re-exports keep desktop import paths stable); mobile's send seam
now gates optimistic echoes on it, so slash sends no longer create a
'Queued' bubble that no transcript echo can ever retire, and the
ack-lost hold only arms for chat sends.
Piece B — mobile model/session-option pickers:
- New per-tab session-option tracking (state/commands/labels modules) ported
from the desktop live flow, reading the shared agent-session-option
catalog for Claude AND Codex.
- Composer pill row (model + options) opening an inline choice card in the
proven Ask-card pattern; applies use catalog modelApply semantics
(/model <value> via the existing send path), Codex-style agent-picker
entries dispatch the picker command and flip the tab to the terminal view.
- Current model seeds from the hook-reported provider model when derivable;
typed /model-style commands update tracked state (recordOutgoingCommand
parity); dispatched values render as sent-not-confirmed.
* fix(ai-vault): support session scanning in SSH worktrees
Add relay-native aiVault.listSessions scanning that discovers agent
sessions on SSH hosts. Includes fallback to filesystem crawl for
legacy relays, full cancellation support, result validation, and
scan coalescing to reduce redundant work.
* fix(ai-vault): scan sessions in SSH worktrees with coordinated cancellat
- Extract batching logic to `mapRemoteScanBatches` for reuse and proper cancellation checkpoints
- Move `AiVaultScanCoordinator` from relay to main to handle concurrent same-key requests with individual cancellation signals
- Report scope path truncation consistently across relay and SSH fallback paths
- Gracefully degrade relay handler on unsupported platforms instead of aborting startup
- Refactor issue display to separate blocking errors, scope notices, and skipped transcript counts
* fix(ai-vault): stabilize SSH session scan CI
Swallow async WSL relay stdin EPIPE so the live hook-relay shard no longer
fails after all tests pass. Merge main, resolve scan/relay conflicts, and
align cancellation/host-issue reporting with IPC expectations.
* fix(ai-vault): harden session scan cancellation, relay timeouts, and preemption
Thread the abort signal through every scan and parse path so superseded or
cancelled scans stop promptly instead of parsing every remaining transcript
for a caller that already left. Replace the fragile message-text relay
timeout check with a typed error code so unrelated errors carrying the
phrase "timed out after" no longer suppress the filesystem fallback. Fix
scan coordinator preemption so a forced Refresh in one window no longer
re-enters as a spurious cancellation in another. Add a host-leg cache for
the all-hosts view and cap filesystem concurrency so a single slow remote
home cannot stall the whole merge.
Co-authored-by: Orca <help@stably.ai>
* fix(ai-vault): use stable React keys for scan issue banners
Drop array-index keys so react-doctor/no-array-index-as-key passes.
Uniqueness comes from host, kind, agent, path, and message.
* fix(ai-vault): SSH session scanning with configurable depth limits
Implement depth-aware caching and proper scan boundaries to make SSH session
scanning reliable in worktrees. Users can now select between faster (250
sessions) and comprehensive (unlimited) history scans. The scanner:
- Deduplicates scans across relay, host leg, runtime, and renderer layers
- Reuses larger scans to serve smaller depth requests
- Properly bounds in-scope discovery per-limit
- Fixes timeout enforcement when SSH providers ignore abort signals
* Move sessionLimit ref update to useLayoutEffect
Keep render pure for React Doctor by deferring ref updates to
a layout effect, which still executes before render-dependent
effects that consume the ref.
* fix(adhoc): stamp version prefix from main, not the feature branch
Adhoc builds check out arbitrary refs whose package.json often lags
version bumps (e.g. 1.4.165-rc.0 while main is 1.4.168-rc.1). Hourly
always builds main so it already tracks the product line; adhoc now
resolves the base version from origin/main (or ORCA_ADHOC_BASE_VERSION)
so branch builds share that prefix.
* Revert "fix(adhoc): stamp version prefix from main, not the feature branch"
This reverts commit a26a18eb3fd83f7e7d2db9a6a7c3e02e0f79089a.
* fix(ai-vault): fix scoped backfill and coordinator race conditions
Resolve race where the last waiter leaving could abort an already-settled scan (add `settled` flag). Redesign scoped session backfill to keep searching through newer files until the scope reaches its requested session quota instead of stopping at the candidate limit; out-of-scope files no longer consume the scope budget. Centralize scan limit normalization and fix error classification for cancelled scans using the proper helper instead of checking Error.name. Disambiguate cache keys using JSON and add cancellation check after scope discovery phase.
---------
Co-authored-by: Orca <help@stably.ai>
* Post fixing replies after launching PR comment resolution agents
Orca now automatically replies to selected comments after launching an
agent to resolve them. Review-thread comments get nested replies; review
summaries and issue comments get top-level @-replies. Payloads are
snapshotted at queue time and posted only after prompt delivery
succeeds. Bounded concurrency (4) prevents SSH slowdown.
* Batch conversation replies instead of posting per-comment
Combine N unresolvable comments into a single PR conversation reply to avoid timeline spam. Review threads still get one nested reply each. Refactor helper functions to pr-comment-fixing-reply-body.ts and change replyAsConversation(comment, body) → replyAsConversation(body).
* Handle bot account mentions in PR conversation replies
Use buildPRCommentConversationReplyBody to properly handle GitHub App
bot accounts, which don't resolve as @-mentions. Enforce delivery semantics
with type-safe PendingPRCommentAiAck payloads and improve error handling
around agent launch to prevent stranded state.
* Fix React Doctor ref-mutated-during-render errors in ChecksPanel
Move comment-resolution payload ref clears and latest-callback mirrors
into effects so render stays pure for the static analysis gate.
* Optimize worktree parent picker: conditional mount, virtualization, lazy
- Mount WorktreeParentPickerPopover only when open to avoid hundreds of
unmounted instances subscribing to lineage and worktree store updates.
- Virtualize the candidate list and resolve activity statuses only for
visible rows, eliminating redundant status subscriptions.
- Extract filtering, placement calculations, and row rendering into
separate modules for testability and clarity.
* Memoize worktree parent picker search handler
Wrap search state update in useCallback to stabilize the handler
across re-renders. Reduces unnecessary effect runs and enables better
memoization of child components.
* Optimize worktree parent picker: defer unmount, memoize IDs
- Defer unmount until exit animation completes (200ms) to prevent premature teardown
- Memoize visibleWorktreeIds to prevent status hook from rebuilding its selector on every render
createExpandCollapseActions minted five fresh closures per TerminalPane
render; four are deps of useTerminalKeyboardShortcuts, so its seven
window listeners tore down and re-registered on every render and all
effect-owned IME chord/modifier state silently reset. Memoize the
actions via useExpandCollapseActions and pin registration stability
with a render-churn regression test.
Fixes#12269
The cell stamps invite expiry at exactly now+10min from its own clock while
the desktop rejected anything past now+10min from the local clock with zero
tolerance, so any cell clock ahead of the machine by more than network
transit made every Relay pairing code fail with an opaque toast. Same
defect class as the host-proof freshness incident; same 30s leeway.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
The runtime RPC WebSocket listener bound to 0.0.0.0:6769 at startup, so a
desktop with no paired device was reachable from the whole LAN before the
user opted in. Default the bind to 127.0.0.1 and widen to all interfaces
only on an explicit opt-in:
- createMobilePairingOffer / getRuntimePairingUrl widen (ensureNetworkExposure)
before advertising a LAN endpoint; the rebind reuses the resolved port so an
already-issued offer stays valid, and concurrent offers share one rebind.
- orca serve and E2E set exposeNetworkByDefault to bind wide at startup.
- A previously-connected device (lastSeenAt > 0) rebinds wide at startup so
reconnect after restart keeps working; a pending/never-connected offer does
not persist exposure across a restart.
The advertised pairing endpoint still resolves to a concrete interface address,
never the 0.0.0.0 bind host.
* fix(sidebar): route local folder adds to the intended host, not the global runtime
Adding a local folder while connected to a remote runtime failed with
"<path> was checked on <host>, but that host did not report a usable folder"
because addRepoPath decides local-vs-remote purely from the global
settings.activeRuntimeEnvironmentId when no explicit host is passed.
Two local-add flows relied on that global fallback and got misrouted:
- useAddRepoLocalFolderFlow (native picker / drag-drop): the Add Project
host selector can display "Local" (selectedRuntimeEnvironmentId = null, so
the guard passes) while the global still points at an unavailable runtime.
Native-picked/dropped paths are always local, so force local routing.
- AddProjectFromFolderDialog ("Add folder as project" on a subfolder): a
subfolder lives on the active repo's host, so carry that host through the
modal data and route by it — local for local projects, the owning runtime
for runtime projects — instead of the globally-active runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sidebar): route runtime server-path adds by selected runtime, not global
The Add Project "server path" step (reached only when a runtime host is
selected) called addRepoPath(path, kind) with no explicit host, so it
inherited the global settings.activeRuntimeEnvironmentId. When that global
diverged from the dialog's selected runtime, the add was misrouted off the
host the user picked — the same root cause as #9541, opposite direction.
Route the server-path add by the dialog's selected runtime explicitly.
Co-locate selectedRuntimeEnvironmentId in useAddRepoHostSelection next to
selectedSshTargetId so the dialog reads it from one place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sidebar): route the pre-add git server-path scan by the selected runtime
For kind === 'git', scanNestedRepos runs before addRepoPath and can
early-exit the flow into the nested-repo review, but it still routed by the
global active runtime — so it could scan the wrong host even after the add
itself was correctly routed to the selected runtime (CodeRabbit).
- scanNestedRepos accepts an optional runtimeEnvironmentId in its controls;
when present it routes by that host, else falls back to the global
(existing callers unchanged).
- useAddRepoServerPathFlow passes the selected runtime into the scan and
derives runtimeKind/streaming support from it instead of the global-reading
getNestedRepoRuntimeKind(null), so telemetry and the nested review target
the same host as the add.
Adds renderer- and store-level regression tests covering scan routing to the
selected runtime, the null-override local case, and the nested-review handoff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sidebar): make nested-scan cancellation route by the scan's owning host
Follow-up to CodeRabbit review of the scan-routing change:
1. scanNestedRepos treated `{ runtimeEnvironmentId: undefined }` as an explicit
local override via the `in` check. Only null or a string is now explicit;
undefined falls back to the global (matches getAddRepoPathRouteSettings).
2. scanNestedRepos gained a routing override but cancelNestedRepoScan still
routed by the global — an asymmetric contract where an override-routed scan
could be un-cancellable if the global diverged mid-scan. cancelNestedRepoScan
now takes the same override, and useAddRepoNestedReviewState remembers each
scan's owning host by scanId (set when the scan is registered) so both stop
and reset cancel on the host the scan actually ran on. The local folder flow
routes its scan explicitly local so scan, cancel, and add all agree.
Adds store-level regression tests (explicit override wins, undefined falls back
to global, cancel routes by override) and a new useAddRepoNestedReviewState test
covering cancel-by-owning-runtime for stop and reset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sidebar): keep subfolder adds on their owning host
* fix(onboarding): keep completion on captured host
* chore(review): drop unreachable onboarding recovery
* fix(sidebar): preserve paired runtime checkout ownership
* fix(runtime): index paired worktrees by logical owner
* fix: fail closed on worktree owner alias collisions
* docs(sidebar): clarify host-routing intent flagged in review
Two Greptile P2 notes, addressed as comments (no behavior change):
- project-added-default-checkout.ts: the runtime branch's `hostId === executionHostId`
is NOT unreachable — a colliding repo id can carry a runtime-qualified hostId with
no runtimeOwnerEnvironmentId (see the "repo IDs collide" test). Documented why the
comparison is reachable and load-bearing rather than replacing it.
- AddProjectFromFolderDialog.tsx: note that omitting the runtimeEnvironmentId spread
intentionally signals local (NonGitFolderDialog coerces absence to null).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(activity): control portal readiness observer delivery
---------
Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* test(runtime): prove mobile session publication rebuilds every worktree
buildMobileSessionTabSnapshots consults its per-worktree cache after building
the content, so a republish saves the fanout but none of the work. With 300
worktrees, an unchanged republish still does 601 units of per-worktree work,
and a single changed worktree does 602.
Publication is keyed on agentStatusByPaneKey/agentStatusEpoch, so this runs on
every agent status tick. On a multi-client runtime host with 381 worktrees this
allocated ~350 MB/min and rode the renderer into repeated 4 GB OOMs.
Tests are marked it.fails so the branch stays green; drop .fails when the build
loop skips worktrees whose inputs are unchanged.
* refactor(runtime): make mobile session snapshot inputs explicit per worktree
Every per-worktree builder in buildMobileSessionTabSnapshots took the whole
AppState, so a worktree's real input set was the transitive closure of seven
helpers and could not be memoized safely. Introduce MobileSessionWorktreeInputs
— built once per worktree — and thread it through the group projection and the
terminal/markdown/file/browser tab builders so the compiler proves the input
set. Tab- and pane-keyed slices are narrowed to this worktree's tab ids, file
ids, browser workspace/page ids, and pane keys; agent statuses are bucketed per
worktree once per publication via a tab-id index.
No behavior change. Dropping AppState from the projection path also removes the
second per-worktree read of browserTabsByWorktree, so the publication-cost
counter falls from 601 to 1 per publication and its two cases now pass.
* fix(runtime): skip unchanged worktrees before building mobile session content
buildMobileSessionTabSnapshots consulted its per-worktree cache only after
building that worktree's three Maps, group projection, and full tab array, so
the cache suppressed the fanout but none of the computation. Every agent-status
tick therefore rebuilt every worktree, which drove sustained 4 GB renderer
working sets on a host holding 381 worktrees.
Cache MobileSessionWorktreeInputs alongside each snapshot and reuse the snapshot
when every input field is reference-equal, before any intermediate structure is
allocated. Worktrees with a mounted TerminalPane always rebuild: their live
DOM/PaneManager state is invisible to store references. Absent per-worktree
slices now resolve to shared empty values so an empty worktree can compare equal
to its last publication. jsonContentEquals stays as the backstop on the rebuild
path for inputs that churn by reference without changing output.
With 300 worktrees, per-worktree content builds go from 300 to 0 on an unchanged
republish and from 300 to 1 when one worktree changes.
* test(runtime): cover agent status publication cost
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Bound exclusive host navigation to a generation-aware latest-wins
single-flight so bulk open and switch fan-out stay responsive on large
remote fleets. Add freeze repro harnesses and navigated settlement.
Backspacing away an entire Pinyin preedit ended the composition with
empty data, no textarea residue, and no input/keypress events — yet
_sendPendingComposition fell back to the last non-empty
compositionupdate data and typed its first character into the PTY.
Only trust that fallback when observed input evidence corroborates it;
a composition with no evidence in any channel was cancelled.
Fixes the macOS Pinyin regression from #11293 (stray letter left after
deleting a preedit); same fix covers IBus/fcitx Backspace cancellation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A single slot per code let a rapid second Enter press go unguarded: the
first release found a mismatched timeStamp, dropped the only entry, and
the second release then synthesized the Shift+Enter this guard exists to
prevent. Track one entry per press and drain exactly one per physical
release, so every press stays guarded until its own release. Auto-repeat
keydowns do not stack an entry, since the whole run ends in one release,
and the list is bounded so a press whose release never arrives cannot
grow it without end.
Refs #11878
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On Windows, the Enter-keyup synthesis path inferred the modified-Enter
chord from release-time modifier state. A plain committing Enter
(Process/229, no modifiers) followed by a rolled-over Shift for the next
doubled consonant made the keyup report shiftKey=true and synthesized a
Shift+Enter the user never chorded; a directly-sent Shift+Enter could
likewise send a second newline from its keyup once the next composition
started. Record observed Enter keydowns (code -> timeStamp) and let the
keyup synthesis run only for presses whose keydown the IME swallowed
entirely; a balancing keyup that copies the keydown timeStamp keeps the
evidence for the later physical release.
Refs #11878
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(terminal): expand variables in Windows PATH
* fix(terminal): preserve expanded Windows PATH at spawn
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): surface worktree catalog failures instead of showing 0 worktrees (STA-3123)
A connected host whose worktree.ps request fails now shows an explicit
catalog-failure state (with the RPC error code) on the host page, and
'Worktree list unavailable' on the home host card, instead of silently
rendering as a healthy host with zero workspaces.
* fix(mobile): mark cached worktree catalogs unavailable
* fix(onboarding): run skill setup in the configured Windows runtime (#12103)
Onboarding was the one skill-setup surface that did not route its install
command through the resolved runtime. Settings, the feature-wall panels and
the Linear prompt all wrap theirs as `wsl.exe -d <distro> -- sh -c ...` and
pass a matching shell override; onboarding spawned a bare terminal and handed
it the raw `npx skills add ...`. With Node inside WSL, npx is not on the
Windows PATH, so the install failed.
The runtime resolver had a second gap behind that: it only consulted
per-project settings, and onboarding runs before any project exists. With no
project it returned undefined and fell through to the Windows host, ignoring
a global WSL default entirely. `getLocalAgentPreflightContext` already had a
no-project fallback for PATH detection; the skill-install path had none.
- extract that fallback as `getGlobalWindowsExecutionRuntimeContext` and
rewire the existing agent-preflight branch through it so the two cannot drift
- adopt it in `useActiveProjectSkillRuntime` when no project is active. WSL
only: a windows-host default already matches the old no-project behavior,
and resolving it would hand skill discovery a target where it had none,
re-triggering scans for every host-default user
- build the onboarding terminal's command for the runtime and pass its shell
override
- register the CLI in WSL rather than on the host, so `orca` lands on the PATH
the install actually runs on, and wrap the copied command to match
* fix(onboarding): keep skill setup runtime consistent
* test(onboarding): satisfy runtime settings contract
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(terminal): route remote-runtime link clicks to the system browser
Terminal link clicks classified ownership from the global
activeRuntimeEnvironmentId, which is null when runtimes are bound per
workspace, so a link clicked in a remote-hosted pane opened a local-only
Orca browser tab and never reached the host. Thread each pane's resolved
runtimeEnvironmentId into openHttpLink as sourceOwner across the OSC 8,
WebLinksAddon, and click-fallback paths.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): route link clicks based on pane ownership, not global sta
Clicking links on remote-hosted panes was routing based on global runtime state, causing unexpected reconnections. Now link routing decisions (where to open: Orca vs system browser) are based on the actual pane's owner — local, SSH connection, remote runtime, or unknown — regardless of whether any runtime is globally active. This ensures a local pane can route to Orca while another pane's remote runtime is active, and a remote pane always routes to the system browser.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(browser): bound retained webview guests across worktree switches
* fix(browser): merge duplicate imports flagged by import/no-duplicates
* fix(browser): evict guests without unmounting the worktree surface
Unmounting the surface disposed every parked terminal byte watcher for the
evicted worktree (bell/title/agent-completion facts dropped for good after
the 15s handoff TTL) and skipped the force-park scrollback capture remote
panes rely on. Destroy the guests only: hidden slots mount no BrowserPane,
so nothing resurrects a destroyed guest before the next visit, and no slot
ever unmount-detaches a live guest (STA-3228). Terminal-state vetoes and
the eviction re-render bump are no longer needed; the only veto left is a
guest an automation/mobile controller is actively driving.
* fix(browser): veto eviction for downloading pages; keep zoom; add kill switch
Main cancels a page's active downloads when its guest unregisters (tab-close
semantics), so eviction now vetoes worktrees with a page still writing a
download, tracked app-wide because download state was pane-local and hidden
panes are unmounted. Eviction is not a user close: re-remember explicit zoom
past the destroy-path forget so a revisit reasserts the user's zoom instead
of resetting same-host siblings through Chromium's partition-wide HostZoomMap.
browserGuestWorktreeRetentionBudget mirrors the terminal budget kill switch.
* feat(crash-reports): add byte attribution to renderer memory highwater breadcrumbs
Entry counts stay flat when a slice grows by value weight (97b9e86d leaked
~700MB while its biggest slice grew by 4 entries), so highwater breadcrumbs
now carry sampled per-slice KB estimates for the store plus a live pane
census (managers, panes, estimated scrollback KB) — the dominant heap cost
the store census cannot see.
* fix(crash-reports): bound renderer OOM profiling
* fix(crash-reports): total raw store estimate bytes
A mobile New Tab -> Codex create resolves the launch command and hands it
to the renderer, but when the renderer's startup queue is lost (the #7587
stall class) the pane spawns a plain shell and the create still settles
ready via PTY adoption - silently binding the phone to a bare terminal
forever, since the ready status also disables the #7837 activation-time
materialize recovery.
Record the resolved launch command on the pending create and, at every
renderer-backed settle point, deliver it to the adopted PTY when no spawn
command was recorded for it. Spawn commands are noted per PTY by both
spawn IPC handlers, so a missing record on the locally registered live
PTY proves the launch never ran; delivery types the command exactly like
the create would have, and the note prevents double delivery.
Fixes STA-3214