Commit Graph
7581 Commits
Author SHA1 Message Date
Jinjing c5fef95a31 test(ssh): harden create-op request wait against CI flakes
The agent-session create test only budgeted 10 microtask turns for
pty.cancelDelivery after shutdown. Under loaded Node 26 CI shards that
async catch/rollback chain could miss the window. Use vi.waitFor so the
assertion yields real event-loop turns instead.
2026-07-30 16:19:37 -07:00
Jinjing e8f7552f41 perf: bound SSH startup reconnects and idle git polling
Orchestrate critical and background SSH reconnect batches with per-attempt and
per-batch budgets to prevent startup blockage. Handle transient git polling
failures gracefully: degraded scans retain previous state instead of throwing,
and chronically unreadable trees back off from active to idle cadence. Mirror
the push-channel hold in pull-side SSH state probes to prevent premature pane
remounting while relays deploy. Extract snapshot logic with degradation
tracking into a separate module.
2026-07-29 19:26:34 -07:00
Jinjing 50eb01fd4c perf: bound SSH startup reconnects and idle git polling
- Timeout-based slot release prevents hung SSH connects from starving
  later reconnect batches
- Parallelize post-startup state polling instead of serial per-target
  IPC to reduce startup latency
- Share SSH connection deduplication across panes to reuse in-flight
  startup connects
- Stop polling git-common when window is hidden and force scan on resume
  to reduce idle I/O
- Require git polling intervals as explicit parameters to prevent local
  default desync
2026-07-29 19:26:34 -07:00
Jinjing 998140acd9 perf: bound SSH startup reconnects and idle git polling 2026-07-29 19:26:34 -07:00
BingZandOrcaWin bd9653c26d fix(tabs): trust native OpenCode titles without hook signals (#11382)
* fix(tabs): trust native OpenCode titles

* test(tabs): cover native OpenCode identity authority

* fix(tabs): preserve sleeping provider identity

* fix(tabs): preserve completed hook authority

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 19:25:22 -07:00
Neil ef90f6099c fix(computer): supervise Linux and Windows desktop providers from main (#11468)
* fix(computer): supervise desktop providers from main

* fix(computer): remove unreachable provider timeout mapping

* test(computer): flush stale supervisor response
2026-07-29 19:14:10 -07:00
Yunqian Fanandfanyunqian.1 791577861b fix(project-host-setup): carry identity across hosts (#9413)
Allow setup when the selected project exists only on another host by carrying its validated provider identity with the request instead of reverse-parsing project IDs. Preserve host-qualified provider identity and reject mismatched payloads before linking.

Make linking atomic for local and runtime imports, including clone setup: roll back only newly registered repos and invalidate the same caches as canonical removal. Cover local, runtime, host-qualified identity, mismatch, clone rollback, and renderer routing paths.

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
2026-07-29 18:57:50 -07:00
Brennan Benson 3eddc467cf test(skills): pin both sides of the nested-skill prune boundary (#11462)
The payload prune had only its miss side covered, so the bound could be raised
or lowered by a refactor without anything failing. Both directions are now
pinned: a skill is found through 2 intermediate directories below a package and
missed at 3.

Raising the bound spends the entry budget on vendor payload — the cost that made
ordinary caches collapse and pin every skill amber (#10865). Missing a deeper
copy costs only a Details row, since a plugin-cache placement is not convergeable
by any update command. Recording the tradeoff on the constant so the next person
to touch it knows which direction is the safe one.

No behavior change.

Closes #11454
2026-07-29 18:44:01 -07:00
Brennan Benson 0fe7759c64 fix(sidebar): float setup script prompt (#11439) 2026-07-29 18:43:04 -07:00
Brennan Benson 64aa726301 fix(quick-open): support projects past 10k files (#11440) 2026-07-29 18:32:21 -07:00
Neil d0f341ad69 fix(computer-use): make modifier clicks interruption-safe (#11451)
* fix(computer-use): make modifier clicks interruption-safe

* fix(computer-use): pace modified Windows multiclicks

* fix(computer-use): address modifier safety review
2026-07-29 18:29:10 -07:00
Brennan Benson 5517bfcbd2 fix(native-chat): make the launch-draft mirror reachable (#11222)
* fix(native-chat): make the launch-draft mirror reachable

Seed the chat-composer copy of unsent launch context on every originating
draft path, then let those launches open in chat by default.

Three paths delivered a draft to the TUI without mirroring it into chat:
folder-workspace create, the local argv-prefill branch of launchAgentInNewTab,
and the web-host equivalent. The first was invisible; the other two were hidden
only because draft launches were forced into terminal view.

The view-mode decision now gates on the same predicate as seeding
(canMirrorLaunchDraftToNativeChat), so a draft can never open in chat with a
composer chat would refuse to fill.

* fix(native-chat): gate draft view mode on argv-prefill launches too

The draft view-mode gate read `startup.draftPrompt`, which only the
post-ready-paste delivery sets. An argv-prefill launch carries its draft
inside `launchCommand`, so the gate never saw one and the tab opened in
chat unconditionally — a multi-line draft was correctly not seeded yet
still opened chat, leaving an empty composer beside a filled TUI input.

Adds `launchDraftText` to the activation startup payload as a view-mode-only
field, deliberately distinct from `draftPrompt` so it cannot double-deliver
the draft through pty-connection's bracketed paste, and sets it at all four
originating producers.

* fix(native-chat): reconcile backend draft launch tabs
2026-07-29 18:28:17 -07:00
Sebastian 8c5b02547e fix(main): prevent claude login hang on Windows due to inherited handles (#11407) 2026-07-29 18:24:00 -07:00
Neil eb58e00c19 fix(terminal): activate fresh OSC links on first click (#11453) 2026-07-29 18:22:52 -07:00
Jinjing cbe8635f46 fix(worktrees): prevent deletion from blocking Orca (#11233)
* fix(worktrees): prevent deletion from blocking Orca

* test(worktrees): loosen async history-delete event-loop bound for CI

The main-thread safety check failed on a loaded runner when a single
timer gap hit ~48ms under the prior 30ms threshold. Keep the bound well
below a recursive sync-rm stall without treating CI jitter as a block.

* test(worktrees): measure history-delete critical path, not timer gaps

setInterval gaps during async rm of thousands of files still flake under
CI scheduling. deleteWorktreeHistoryDir is sync and must only rename, so
assert that critical-path wall time stays well below a recursive walk.

* fix(worktrees): prevent deletion from blocking Orca

Add timeout-based draining of watcher closes so SSH round-trip delays don't
indefinitely block the worktree removal path. Also: order durable temp-file
sweeps ahead of writes to reclaim orphans before accumulation, skip own-process
temps to avoid deleting live writes, swallow persistence errors so disk failures
don't cascade to query callers, and measure history-deletion progress by loop
turns rather than timer gaps to detect blocking on CI runners.

* fix(worktrees): prevent deletion from blocking Orca

Worktree deletion can now proceed even if filesystem watchers or history cleanup operations hang, preventing Orca from freezing. Changes:

- Fence install slots with tokens instead of counters so removals can abandon wedged installs without corrupting later removals
- Timeout-bound watcher unsubscribe operations with a shared drain budget
- Move JSON serialization of large usage caches from queue-time to write-time to avoid blocking main thread
- Async tombstone + schedule history tree deletion instead of blocking recursive rmSync during GC, preventing main-thread stalls ~10s after startup

* Extract usage cache writer into reusable durable snapshot class

Consolidates serialized durable-write and generation-veto logic from
three usage stores into UsageCacheSnapshotWriter. Eliminates duplication,
centralizes multi-MB JSON serialization on the main thread via write-queue
serialization, and vetoes superseded snapshots to avoid wasted rewrites.

* fix(worktrees): prevent deletion from blocking Orca

Worktree deletion used to recursively delete large session trees (hundreds
of MB) on the critical path, stalling the event loop. Instead, rename trees
into a `.pending-delete` tombstone queue and reclaim them asynchronously
off the removal's critical path.

Extracted host tree removal into a reusable helper (`removeHostTree`) that
centralizes Windows retry logic. Added usage-cache flush on quit to prevent
data loss when scans complete right before shutdown. Improved watcher
removal deadline management with reserved tail slices for the final
unsubscribe, and added retry logic for tombstone removals that fail once
under transient Windows locking.

* fix(history): retry failed session tree removals

Tombstoned session trees whose removal fails transiently (e.g., EBUSY
under Windows AV) are now re-queued in-process with bounded exponential
backoff instead of sitting until the next HistoryManager construction.
Prevents a single stuck tree from blocking the entire Orca process.
2026-07-29 18:21:26 -07:00
JinjingandOrca 4e99602ac8 Add search to kanban view (#11244)
* feat: add search to workspace kanban board

Search filters workspace cards by display name, branch, repo, and comment. Lanes show match counts (e.g., "2 / 5") when filtered and reset to full counts when cleared. Drag-drop indices are mapped from rendered cards to the full lane so manual-order math is correct even when hidden. Query clears when the board closes to prevent stale filters on reopen. Includes keyboard shortcuts (Escape to clear), live region announcements for matches, and i18n support.

* feat: add search to workspace kanban board

Adds a search field to filter the kanban board by workspace name. Range selections now index rendered cards only, preventing silent selection of hidden items when filtering. Selection badges count only the visible cards that drag/context-menu actions will move. Lane totals distinguish between empty-by-definition and filtered-away cards. Drop operations commit against the full lane while displaying filtered indices. Whitespace-only queries don't show match counts, since they don't narrow the board.

* fix(kanban-search): let the board search field own Escape

The board's Escape handler is a capture-phase listener on document, so it
runs before React's handlers and the search field's stopPropagation could
never reach it — pressing Escape to clear a query dismissed the whole board
instead, and the reopen reset then discarded the query too.

useWorkspaceBoardPanel now defers Escape to editable targets inside the
board sheet, and the field handles both outcomes itself: clear when it has
text, close the board when it does not.

Also: keep focus in the field when the clear button unmounts itself,
reserve counter width from the rendered text so three-digit counts cannot
overlap typed text, and align the icon centering, X size, and placeholder
with the sibling search fields.

Co-authored-by: Orca <help@stably.ai>

* perf(kanban-search): defer the filter and stabilize its derived identities

Clearing a query re-mounts every hidden card, so it costs roughly what
opening the board costs. The input stays controlled and undebounced, but
the filter now reads a deferred query so React can interrupt that work and
the caret stays responsive.

The match set also keeps its identity when the matched ids are unchanged.
Board worktree identities churn on agent-status ticks, and a fresh Set on
every tick cascaded new identities through the lane views, the rendered
selection, and every memoized card.

Also harden the lane full-id channel: the identity guard in
resolveFullLaneDropIndex compares membership rather than length, so a stale
lane of equal size no longer skips translation; serialization declines ids
containing the newline delimiter instead of inventing phantom lane members;
the sidebar drop path scans lane cards once instead of twice; and the
unfiltered full-id fallback is no longer offsetParent-filtered, restoring
the pre-branch notion of lane membership.

Adds coverage for the stale-equal-length lane, the full-id round trip,
regex metacharacters and non-ASCII queries, and the over-bound query at the
drawer level.

Co-authored-by: Orca <help@stably.ai>

* fix(kanban-search): leave mid-composition Escape to the IME

Escape during an IME composition cancels the in-progress reading. The
search field was clearing the query behind it instead, matching the
isComposing guard other keyboard handlers in the app already use.

Co-authored-by: Orca <help@stably.ai>

* fix(kanban-search): stop a hidden anchor from collapsing a shift-click

A query can hide the selection anchor while leaving the rest of the
selection on screen. updateWorktreeSelection reads an anchor missing from
visibleIds as "no anchor" and replaces the selection with the clicked card,
so shift-clicking dropped the still-visible cards too. Re-anchor onto the
first still-rendered selected card, and carry hidden selections through a
range so the query cannot silently discard them. A plain click still clears
everything.

Also, in the drop-index translation:
- a lane filtered down to nothing now appends rather than always prepending
  (an empty rendered lane reports index 0 for every pointer position, so the
  old branch could only prepend, disagreeing with the document-drop path)
- an unresolvable rendered id falls back toward the end of the lane its
  branch was aiming at, instead of sending every head drop to the bottom
- the full-id channel uses NUL, the one character no path can contain, so
  serialization can no longer be defeated by a newline in a repo path.
  Dropping the channel was the wrong fallback: under a query the reader
  would scan the DOM and see only the matched cards.

Tests now build the channel through its own serializer rather than
hardcoding the delimiter.

Co-authored-by: Orca <help@stably.ai>

* fix(kanban-search): explain a query discarded for length

Past the palette byte bound the query is dropped and the board stays
unfiltered, which looks identical to a query that matched everything — full
field, untouched board, no counter. The field now marks itself invalid,
shows a "Too long" badge carrying the full reason, and announces it.

Whitespace-only text stays silent: it is also non-filtering, but self-
evidently so.

Co-authored-by: Orca <help@stably.ai>

* fix(kanban-search): derive the too-long badge from the deferred query

The badge describes the board, so reading the live query made it flip a
frame before the filter it is describing.

Co-authored-by: Orca <help@stably.ai>

* fix(kanban-search): let a range replace a hidden selection like every other gesture

Carrying hidden cards through a shift-click made it the only replace-shaped
gesture that did so — a plain click and a non-additive marquee both drop
them. It also left the user unable to narrow a selection: shift-clicking the
two visible matches silently re-added the six hidden ones, and the badge
counts only rendered cards, so nothing disclosed it. Re-anchoring onto the
first still-rendered selected card, which is what actually fixed the
collapse, is kept.

Also state the Escape contract where a reader will look: SheetContent now
declines Radix's dismiss explicitly instead of depending on
handleSheetOpenChange quietly dropping the request, and the overlay reserve
is capped so a wide counter in a narrow drawer cannot squeeze the typed text
to nothing. The reserve is exported and tested directly — happy-dom cannot
parse min(), so it could not be read back off a style.

Co-authored-by: Orca <help@stably.ai>

* fix(kanban-search): stop mutating match-set ref during render

React Doctor blocks ref writes during render; keep match-set identity
stable with setState-during-render so discarded renders cannot leak it.

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-29 18:12:27 -07:00
Neil 48184b9e21 fix(computer): supervise macOS helper from main (#11441)
Move native macOS helper process ownership into Electron main while preserving the sidecar as the authenticated socket peer. Add fixed lifecycle IPC, bounded claim and release handling, confirmed-exit tracking, sidecar and helper force-kill escalation, cleanup across failure paths, and focused lifecycle coverage.
2026-07-29 18:08:55 -07:00
hanjoonchoeandBrennan Benson f4e46383df feat(mobile): add session.tabs.list handler to mock server (#9293)
* feat(mobile): add session.tabs.list handler to mock server

The mock WebSocket server had no handler for session.tabs.list, so the
session screen of a paired dev client hung on 'Loading tabs' forever —
the terminal pane, live input, and command input could never be
exercised against the mock. Respond with a single ready terminal tab
wired to the existing term-1 fixture so the whole session surface works
offline.

* fix(mobile): complete the session.tabs.list mock contract

The new mock response omitted four non-optional fields of
RuntimeMobileSessionTabsResult: publicationEpoch and activeGroupId on the
result, and parentTabId and leafId on the terminal tab. Nothing caught it —
the object literal had no type annotation, and MobileSessionTabsStreamHealth
is generic over both result and tab. A shape-incomplete mock yields
untrustworthy repros for exactly the bugs it gets used for (session tabs,
split panes, pane-to-tab attribution).

Fill the fields with host-realistic values: a per-process publisher epoch, a
layout UUID leaf id, and the `${parentTabId}::${leafId}` surface id
mobileTerminalSurfaceId actually emits. Pin the shape with an explicit return
type so a future required field fails typecheck instead of silently drifting.

Move the fixture into its own module: inlining it pushed
mock-server-rpc-handlers.ts to 317 lines against a 300-line max-lines cap,
which broke `pnpm lint` on the parent commit. It registers through the file's
existing delegation chain, after the native-chat scenario so MOCK_NATIVE_CHAT=1
keeps ownership of the method.

Co-authored-by: Hanjoon Choe <hanjoonchoe@gmail.com>

* test(mobile): pin session tabs mock fidelity

Normalize the selector-backed worktree ID like the real runtime and cover the complete terminal surface response so future contract drift fails the mobile suite.

* fix(mobile): share terminal.list worktree resolution with session tabs

Main added `terminalListWorktreeId`, which the rebased session-tabs fixture
duplicated with a different no-selector fallback — `terminal.list` resolved to
the active fake worktree while `session.tabs.list` returned a literal 'mock',
so a session repro saw two different worktree ids for one screen.

* test(mobile): cover the bare session-tabs worktree selector

Answers the review note that only the `id:`-prefixed path was exercised.

* fix(mobile): make the mock publication epoch unique per process

Date.now() can repeat across a sub-millisecond restart, so the epoch did not
actually guarantee the fresh-publisher identity its comment claimed.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-29 18:01:38 -07:00
Neil 270c5ad3fa Set selected create-worktree agent as default (#11443)
* feat(new-workspace): set selected agent as default

* fix(agent-picker): guard empty default action
2026-07-29 17:46:39 -07:00
Neil 78b8a37aed fix(cli): keep automated worktree creation in background (#11445) 2026-07-29 17:45:57 -07:00
Brennan Benson 5e00a30e4e Decouple feature copy from locale parity (#8512)
* Decouple feature copy from locale parity

* Fix undeclared dynamic localization key check

* Fix localization code owner
2026-07-29 17:44:41 -07:00
Brennan Benson 32926bc831 fix(dashboard): remove per-worktree status dot from agent cards (#11437) 2026-07-29 17:28:43 -07:00
Brennan Benson b339fe0346 Fix Node 26 test gate and happy-dom storage (#11434)
* ci: test PR shards on Node 26

* test: isolate happy-dom storage from Node globals
2026-07-29 17:11:16 -07:00
Jinjing 8d4e975ff7 fix(new-workspace): stop UI flashing when typing ahead of search (#11436)
* fix(new-workspace): stop UI flashing when typing ahead of search

Hold branch results while queries settle, show the spinner only on
initial load, use stable cmdk values, and guard selections against
stale rows. This prevents the highlight from jumping around when
typing faster than the debounced search settles.

* fix(new-workspace): keep dropdown visible while typing within settled qu

Hold the last search results while the user extends or trims the query,
only hiding them when the query diverges completely. This prevents the
dropdown from flashing empty between debounced keystrokes and removes the
guard that made provider rows unselectable during typing.

* fix(new-workspace): align held provider results with live typing

Cap prefix hold by length delta, hide GitHub/GitLab/Linear rows when the
field is cleared ahead of debounce, and re-sync the cmdk arm when search
settles so the highlight cannot lag the resolved selection.
2026-07-29 17:10:58 -07:00
Brennan Benson 93dfe68d73 fix(settings): reject malformed navigation targets (#11433)
* fix(settings): reject malformed navigation targets

* fix(settings): allow setup guide navigation
2026-07-29 17:08:40 -07:00
Jinjing 4c65b42ee2 fix(sidebar): move project header grab cursor to title surface only (#11435)
* fix(sidebar): move project header grab cursor to title surface only

Prevent grab cursor appearing over action buttons (…, +, chevron) which
should show cursor-pointer, not the reorder hand.

- Grab cursor scoped to icon + label surface only
- Row retains data-repo-header-drag-handle for indent/padding drag targets
- Actions excluded via [data-repo-header-actions] selector
- Add lockstep test to keep action selectors synchronized

* fix(sidebar): share project header action selector across drag contracts

Address Greptile feedback: drop the format-sensitive regex lockstep parse and
import one shared REPO_HEADER_ACTION_SELECTOR for repo and group headers.
2026-07-29 17:03:58 -07:00
JinjingandOrcaWin 5f7807497e feat(ssh): bound relay PTY output end to end (#11005)
* docs: design SSH relay PTY backpressure

* fix(ssh): bound relay frame decoding

* fix(relay): bound PTY output publication

* fix(ssh): bound PTY model admission

* fix(ssh): settle closed model admissions

* feat(ssh): negotiate bounded PTY consumer sessions

* fix(ssh): fence exit on renderer settlement

* feat(ssh): track PTY source credit end to end

* fix(ssh): recover bounded PTY output across reconnect

* feat(ssh): complete relay PTY output backpressure

* fix(ssh): close final PTY source credit races

* docs(ssh): record final backpressure validation

* feat(ssh): complete relay PTY source-credit lifecycle

* test(ssh): complete provider notification fixture

* fix(ssh): preserve terminal source credit across rotation

* fix(ssh): fail closed on recovery cancellation

* fix(ssh): prioritize mux control writes after drain

* fix(ssh): retire canceled relay restore deliveries

* fix(ssh): order exit cancellation cleanup

* fix(ssh): gate provisional source activation

* test(ssh): register mux drain-priority coverage

* fix(ssh): type stale owner recovery mismatches

* fix(ssh): close projection replacement races

* fix(relay): contain streaming edge failures

* fix(ssh): secure relay endpoint credentials

* docs(ssh): reconcile final backpressure lifecycle

* fix(ssh): bound main IPC output lifecycle

* fix(ssh): close recovery ownership gaps

* docs(ssh): record exact artifact validation

* fix(ssh): reject reclaimed snapshot replacements

* fix(ssh): fence model admission across reconnect

* fix(ssh): contain migration failure per PTY

* docs(ssh): record final exact-head validation

* test(ssh): align deploy fixtures with credential publication

* feat(ssh): add per-target bounded output setting

* fix(ssh): close source recovery review gaps

* fix(ssh): latch source credit environment override

* feat(ssh): make PTY source credit the default

* docs(ssh): record always-on relay validation

* docs(ssh): bind validation to current main

* test(ssh): grant source credit in IPC fixture

* test(ssh): grant source credit in fake relay

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 17:03:15 -07:00
Jinjing c676b6aa3b docs: update mobile APK link to 0.0.36 (#11438) 2026-07-29 17:02:13 -07:00
Brennan Benson 0678fd8a0d fix(macos): acknowledge TCC notice after close (#11412)
* fix(macos): acknowledge TCC notice on close

* fix(macos): require fresh TCC detection
2026-07-29 16:54:27 -07:00
Neil 4f536ed601 fix(computer): close helper session review gaps (#11428)
* fix(computer): close helper session review gaps

* perf(computer): stop released session registration retries
2026-07-29 16:38:11 -07:00
Brennan Benson 24a2accc3c fix(hibernation): reap restored subagent rows with no live agent process (#11219)
* fix(hibernation): reap restored subagent rows with no live agent process

A pane whose Claude session had a subagent in flight can be locked out of
agent hibernation for good. A PTY that dies while Orca is down never runs
the teardown that clears pane state, so hydrate rebuilds a subagent roster
that nothing can retire: the existing reap needs the parent to emit a
complete `background_tasks` inventory, and a parent that went idle before
the restart never emits one. The restored row keeps gating the pane
'working', and hibernation only accepts 'done'.

Observed locally: six panes parked at SubagentStop in state 'working' for
17 to 145 hours, each still holding a working child row.

Adds a second reap path. Hydrate seeds are marked `restoredFromSnapshot`,
cleared by any live lifecycle event or an id-exact running inventory entry.
One post-restore sweep drops the rows still unconfirmed when the pane's PTY
is absent from the live local inventory, then re-derives the child-gated
'working' to 'done'.

The scan is local-only by construction: panes with a relay connection id are
skipped and SSH-scoped PTY ids resolve as live, since a remote agent runs on
the far host and could never appear in a local listing. An unreadable
inventory is not evidence that anything exited, so it is a no-op. Panes that
have reported to this runtime are left alone.

`stateStartedAt` and `stateHistory` are untouched, so a draft typed while
the pane was working still blocks hibernation.

* fix(hibernation): prove local ownership before restored reap

* fix(hibernation): require authoritative restored PTY absence

* fix(hibernation): probe restored PTY liveness authoritatively

* fix(hibernation): restart idle window after restored reap

* fix(hibernation): type restored reconciliation timing

* fix(hibernation): respect worktree host ownership

* fix(hibernation): preserve same-id restored PTY rebinds

* fix(hibernation): fence batched restored PTY probes
2026-07-29 16:30:44 -07:00
Neil f56c37b470 fix(skills): stop rescanning on unrelated store writes and bound the discovery cache (#7670)
Two fixes to installed-skill discovery in the renderer.

**Stops one discovery IPC per unrelated store write.** `refresh`'s `useCallback` depended on the `discoveryTarget` *object*, which callers rebuild inside a store-backed `useMemo`. Any unrelated store write handed the hook a fresh identity, recreated `refresh`, and re-fired `useEffect(() => void refresh(false), [refresh])`. On a cache hit that is only extra renders — but on a *rejecting* scan nothing is cached and the pending entry is cleared, so it becomes one discovery IPC per store write, indefinitely. That is the remote-runtime-unreachable and SSH case. Measured 6 scans where 1 was correct.

This dep also exists on `main`, so this repairs a pre-existing bug rather than only one introduced here. Fixed with a render-phase `useState` latch keyed on the already-present `discoveryTargetKey` string — deliberately not a `useMemo` (React documents those as discardable, so a discarded one silently restores the regression while the test stays green) and not a ref (react-doctor correctly flags a render-phase ref mutation).

**Bounds the discovery cache.** The module-level maps were unbounded, cleared only wholesale by `notifyInstalledAgentSkillsChanged`. Now a 256-entry LRU keyed by `getRuntimeScopedSkillDiscoveryKey`, with a `discoveryGeneration` guard and a read/peek split so only the unforced cache-serving path promotes recency.

This matters more after #6887 than before it. Local keys are bounded by project count (~47 KB/entry, ~240 KB for 5 repos), but once remote scans key on `runtime:<environmentId>`, ephemeral VMs mint `orca-${randomUUID()}` per start (`ephemeral-vm-runtime-service.ts` + `ephemeral-vm-recipe-runner.ts`), so every VM start creates a permanently-retained entry — ~18.8 KB each, ~1.8 MB after ~100 starts, and it does not stop.

Scoped down during review: an `executionHostId` change touching 8 production files was removed as a second feature (host-awareness landed in #6887 instead), taking this from 17 files to 6. New tests pin that the cap keys off the runtime-scoped key, that two environments never share an entry, and that one environment collapses to a single entry across differing client target shapes.

Known gap, not addressed here: cache entries are not evicted when a runtime environment is removed, so a removed environment's skill list is retained until LRU pressure or an install notification clears it. That needs a store subscription to `runtimeEnvironments` removals and is filed separately.

Co-authored-by: nwparker <4138956+nwparker@users.noreply.github.com>
2026-07-29 16:30:07 -07:00
Kevin BravoandBrennan Benson 0d4baf2a63 fix(settings): guide Windows skill setup when npx is missing (#10453)
* fix(settings): guide Windows skill setup when npx is missing

* fix(settings): resolve npx by PATHEXT and keep the preflight off POSIX shells

Review fixes on the Windows npx preflight:

- Probe and run bare `npx` instead of pinning `npx.cmd`. cmd.exe resolves both
  through PATHEXT, so shims that ship `npx.exe` (Volta) no longer get told
  "npx was not found" on a machine where npx works.
- Force the skill terminal to PowerShell when the configured Windows shell is
  POSIX-family. Git Bash rewrites the leading `/d /s /c` arguments as MSYS
  paths, which would break a command that runs fine there today.
- Drop the "restart Orca" advice. Every new PTY merges the persisted Windows
  PATH, so a new setup terminal already picks up a fresh Node install.
- Fall back to the plain command on the two newly routed call sites when the
  project runtime is repair-required, matching the eight existing callers.
  Those sites otherwise emitted a wsl.exe command for a missing distro.

Adds coverage for the Git Bash override and for cmd.exe block safety.

* fix(settings): apply the skill-terminal shell override on every wrapped path

The Windows npx wrapper is emitted whenever buildSkillCommandForRuntime falls
back to the local host, but three paths never consulted the shell override, so
a Git Bash user still got the cmd.exe string pasted into MSYS:

- useActiveProjectSkillRuntime returned an empty result whenever no local
  project runtime resolved (no repo yet, or an SSH/remote repo) while the
  command builder kept wrapping. It now resolves the override for that same
  host fallback, so the wrap gate and the shell gate agree.
- The Linear setup prompt carried a third copy of the override that was still
  on the old wsl.exe-only check. It now delegates, as does the settings copy,
  leaving one implementation.
- MobileEmulatorAgentControlRow built wrapped commands but passed no override.

Also drops the unreachable WSL guard inside the wrapper; the only caller is
already on the non-WSL branch.

* fix(settings): keep the npx preflight off remote runtimes and finish the emulator row

- Skill setup terminals spawn on the focused runtime environment, so a Windows
  client was handing a cmd.exe command to a remote Linux host where the plain
  npx command used to run. Skip the wrapper whenever a runtime environment is
  focused.
- MobileEmulatorAgentControlRow was left half routed: it took the project
  runtime's shell override while still building a Windows host command, so a
  WSL project got the cmd.exe wrapper inside a WSL shell. Build its commands
  from the same runtime, matching MobileEmulatorAgentSetupGuideSteps.

* fix(settings): match the terminal router when skipping the npx preflight

- The remote check used settings.activeRuntimeEnvironmentId, but the setup
  terminal routes through getSingleFocusedRuntimeEnvironmentId, which keeps the
  terminal local unless exactly one saved environment is focused. Users with
  two or more environments lost the preflight while still running locally.
- LinearAgentSkillPane memoized its commands on the project runtime alone. The
  built command now also depends on the focused runtime environment, and
  Settings panes stay mounted, so the memo could serve a stale Windows command
  after a runtime switch. Compute it inline like every sibling pane.

* fix(settings): keep emulator skill setup installing where detection looks

The mobile emulator surfaces detect the Orca CLI skill with a local-host scan
that takes no discovery target, so building their commands from the project
runtime made a WSL project install into the distro while detection kept
scanning Windows: the panel stayed "Not installed" with no way out. Build host
commands there again, as the surrounding comment already documented, and keep
only the terminal shell override those surfaces actually needed.

Also narrow the remote check back to the focused environment id. Matching the
terminal router exactly meant reading runtimeEnvironments, which nothing on
these surfaces subscribes to, so adding or removing an environment mid-session
could leave a stale decision. The focused id alone over-skips instead, which
degrades to the previous behavior rather than sending cmd.exe to a remote host.

* fix(feature-tips): keep the npx preflight on the repair-required fallback

installDisabledReason is only ever set on Windows, so dropping the whole
command builder on that branch stripped the npx preflight exactly where it is
needed. This terminal auto-pastes with no install gate, so that fallback put
the bare npx command straight back in front of the user #10438 describes. Drop
to the host runtime instead, which still avoids the missing WSL distro.

Pins the two call-site invariants that were unguarded: the emulator surfaces
must build host commands because their detection scans the host only, and the
Linear prompt's shell override must cover POSIX-family Windows shells.

* fix(settings): reach the npx preflight from the ephemeral VMs pane too

This pane was the only one of eight requiring a resolved project runtime
before building its command, so a Windows user with no repo added yet — the
state issue #10438 was reported from — got the bare npx command and the same
dead end. An absent runtime already resolves to the local host, which is what
the sibling panes rely on.

Also pins the repair-required host fallback added in the previous commit; the
existing assertions all passed against the old form.

* test(settings): pin the ephemeral VMs pane to the host-resolving command

Reverting the previous commit left the whole suite green, and this is a PR
where several regressions came from an earlier fix, so guard it the same way
the emulator call sites are guarded.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-29 16:27:47 -07:00
Neil 0349cb6bdb fix(computer): reap mac helper after client loss (#11425) 2026-07-29 16:17:30 -07:00
8f36cd9baf fix(skills): read installed skills from the connected remote runtime (#6887)
* fix(skills): read installed skills from the connected remote runtime

The "Not installed" badge stayed on in Settings even after a skill was
installed against a remote `orca serve`. Skill discovery always ran through
the local `skills:discover` IPC, so it scanned the client's home dir while
the install (and the skill files) landed on the server. The skills browser
had the same blind spot.

Route discovery to the runtime that owns it, mirroring git/hooks/terminal:
local IPC by default, remote runtime RPC (`skills.discover`) when an Orca
runtime environment is active. The discovery cache is now scoped per runtime
so local and remote results never collide.

Extract the discovery cache/transport into a store-free module
(`installed-agent-skill-discovery.ts`) and a shared runtime-target hook, so
the React hook stays under `max-lines` and store slices can import the
change-notifier without pulling the app store into a circular import.

Independent of the terminal selector fix (#6816); addresses the still-broken
install-status half of #6789.

* fix(skills): runtime-agnostic scan-error toast + docstrings

Address review on #6887:
- The skills-scan error toast said "Could not scan local skills", but
  discovery can now target a remote runtime; drop "local" (source + locales).
- Add short JSDoc to the discovery helpers and skill hooks to clear the
  docstring-coverage gate.

* fix(skills): route discovery through the active runtime and scope its cache

Rebuilt on the repo's standard runtime-client pattern (getActiveRuntimeTarget +
callRuntimeRpc) instead of a bespoke transport, and keeps the renderer discovery
cache keyed per runtime so a remote result never leaks into the local host's
badge after switching environments.

* fix(skills): keep the discovery cache store-free to break the import cycle

Reading the active runtime from the app store inside the hook module closed a
cycle (store -> repos slice -> hook -> store) that broke module init in three
suites. The cache/transport moves to a store-free module the repos slice can
import; only the hook itself touches the store.

* fix(skills): scan the host the install actually lands on

Reviewer round 1 found the badge could scan a different machine than the Install
button writes to: skill install terminals route through
getSingleFocusedRuntimeEnvironmentId, which declines to guess an owner while
several runtimes are saved, so a two-environment user installed locally while
discovery scanned the remote and the badge never flipped. Discovery now resolves
through that same resolver, and holds a loading state until settings and the
runtime catalog have hydrated instead of flashing 'Not installed'.

Also drops the unreachable cwd/worktreeId forwarding (no caller can produce it)
and retires three SkillsPage strings that a remote scan makes false.

* fix(skills): close the round-2 review findings

- SkillsPage had no generation guard, so a slow local scan could land after a
  newer remote scan and silently redisplay the client's skills.
- The round-2 selector took a useShallow object including runtimeEnvironments,
  whose identity churns on every status refresh; that re-fired every consumer's
  scan. Select the resolved id instead.
- A failed runtime-environment catalog read never set the hydrated flag, so
  discovery would have spun for the whole session with no retry affordance.
  An unreadable catalog is settled, which is what terminal routing assumes.
- Remote cache keys no longer fragment on a client-side target the remote call
  discards, which was issuing the same RPC once per target shape.
- Cover each hydration conjunct separately; the combined test covered neither.
- First tests for SkillsPage, which had none.

* fix(skills): settle the runtime catalog without loosening host routing

Round 3 set runtimeEnvironmentCatalogHydrated on a failed catalog read so skill
discovery would stop waiting. That flag also gates fail-closed host routing
(worktree-operation-route mayBeLegacyLocal), so flipping it on failure would have
routed ownerless legacy worktrees — including removals — to the local host off a
stale empty list. Add a separate 'settled' flag for surfaces that only need to
stop waiting, and leave 'hydrated' meaning what its doc says.

fetchSettings now probes the catalog even when the settings read fails, so a
rejected settings.get cannot strand every skill badge on a spinner.

* test(skills): pin the settings-failure runtime catalog probe

The only hunk in the review no mutation could kill.

---------

Co-authored-by: vladmesh <vladmesh@gmail.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-29 16:12:38 -07:00
fe6f929c6e fix(terminal): reconcile cross-platform IME composition lifecycle (#11293)
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: JeongUk Park <jeongph.dev@gmail.com>
2026-07-29 16:12:20 -07:00
Dhilip Subramanian 6c3b2cfb39 fix(skills): preserve symlinked agent coverage (#8329)
Settings → Orchestration → Agent coverage decided whether each detected agent had the orchestration skill by matching a skill's single `rootPath` against a hand-maintained table of path segments. Skill discovery dedups by canonical file path, so when one provider home is a symlink onto another agent's skills directory — which `npx skills add --global` does — the two collapse into one row and the absorbed roots survive only in `rootPaths`, which the old matcher ignored. With `~/.grok/skills` symlinked onto `~/.claude/skills`, Claude read Ready and Grok read Missing even though Grok loads the same files.

Now classifies every entry in `rootPaths` and resolves each root through the `SkillDiscoverySource` the scanner already returns, accepting it when `sourceKind` is not `repo` and `owner` is either the agent's owner or `null`. Deletes `ORCHESTRATION_SKILL_LOCATIONS` and `ORCHESTRATION_SKILL_LOCATION_IDS_BY_AGENT` — a renderer-side mirror of `buildSkillDiscoverySources` that had to be hand-edited for every new provider root, which is what caused this class of bug.

No discovery roots change here; those landed in #8510.

Fixed during review:
- Restored the required `{ kind: 'local' }` argument to `useDetectedAgents`. Dropping it failed typecheck (TS2554) and pinned `detectedIds` to null, leaving the widget on "Checking installed agents and skill paths…" forever. It also reverted the remote-host scoping from #9790.
- Stopped a duplicate repo root from shadowing an owning home root. `SkillDiscoverySource.path` is not unique: when the scan cwd is the home directory, `~/.claude/skills` is scanned as both a home root and a repo root, and a path-keyed Map is last-write-wins — so every agent read Missing while the panel above said Installed. Guaranteed in Settings on a WSL project.
- Carried OMP coverage in the owner-based shape so #6422's coverage-table hunks could be dropped on merge.

Behavior note: an orchestration skill inside an enabled Claude plugin now counts for Claude. That is a deliberate consequence of trusting the scanner's ownership data, matches how the Codex plugin cache was already treated, and is pinned by a test.

Refs #8256

Co-authored-by: sdhilip200 <49802211+sdhilip200@users.noreply.github.com>
2026-07-29 16:11:53 -07:00
Brennan Benson 493f403ef4 test(skills): execute the plugin-cache entry limit instead of reasoning about it (#11255)
* test(skills): execute the plugin-cache entry limit instead of reasoning about it

The entry bound that ended a plugin-cache scan was never reached by any test.
Every existing `entry-limit` case builds a synthetic issue object at the display
layer, so both emission sites in scanKnownPluginSkillCandidates — the dirent read
loop and the declared-skill-root resolve — were verified by reading the code. The
real bound is 16,384 dirents, and the declared-root guard only fires when the count
lands in a one-entry window below it, which is why neither was ever fixtured.

Makes the entry bound injectable the way the candidate bound already was, folding
both into a `PluginSkillScanBounds` bag so a fourth positional number is not needed,
and adds:

- a case that truncates the dirent read and asserts the scan drops both real skills
  and reports `entry-limit` at the root;
- a case that truncates while resolving declared roots, asserting the declared root
  that exists was still walked so the guard under test is the resolve one;
- an inventorySkillFreshness case at the production 16,384 bound asserting a
  truncated scan produces zero fabricated placements — the #10918 regression.

No bound value or scan behavior changes.

Refs #10918.

* docs(skills): state the declared-root guard's real window in the bounds comment

* test(skills): pin entry-limit to the scan root, not the crossing directory

Both entry-budget fixtures crossed the bound in the same directory they named,
so swapping recordIssue(rootPath) for recordIssue(directory) at either guard
passed the whole suite — the dialog would surface a nested path and no test
would say so. Cross the budget below the root instead.

* test(skills): assert the declared-root guard's threshold, not just its firing

The declared-root entry guard's ±1 boundary was left unasserted on the claim
that it is unkillable: admitting one more declared root was said to always cost
a dirent the read-loop guard then catches identically. It does not. A declared
root that does not exist reads no dirent, and when it is the last one the loop
simply ends — so the scan completes and reports nothing.

Restates both budgets against the fixture's exact entry count: one short of it,
where only the last root's resolve can cross, and exactly at it, where nothing
should be reported. `>` -> `>=` and `>` -> `> max + 1` now each fail a test.

* test(skills): assert the entry bound stops the walk, not just what it reports

Dropping `limitReached = true` at the dirent guard survived every case: the
already-read entries of the crossing directory are still descended, and the
scan reports a depth-limit for a path it never reached. A nine-level chain
whose deepest directory sits at the depth bound and crosses the budget on the
second of its two children kills that, with no symlink and no Windows skip.

* test(skills): derive the walk-stop fixture from the depth bound it depends on

The walk-stop case detects a dropped `limitReached` only because its chain is
exactly MAXIMUM_PLUGIN_SCAN_DEPTH deep, so the entries already read when the
entry bound trips are rejected on depth if the walk keeps going. That coupling
was a hardcoded 9. Raising the depth bound left the test passing while it
stopped killing the mutant it is the only cover for — verified by A/B: at
depth 12 the hardcoded fixture lets the mutant survive, the derived one does
not. Other tests in the file fail loudly on that change, which is exactly what
makes the silent one dangerous.

Exports the bound the way the file already exports its four siblings for the
same reason. No behavior change.
2026-07-29 16:07:07 -07:00
Brennan Benson 5c8013abaa fix(settings): reserve skill badges for attention (#11413)
* fix(settings): reserve skill badges for attention

* test(settings): cover hidden checking badge
2026-07-29 15:56:30 -07:00
Brennan Benson b21f978c6d fix(codex): restore five-hour usage window (#11415)
* fix(codex): restore five-hour usage window

* fix(codex): reuse backend reset credit metadata
2026-07-29 15:53:42 -07:00
caioribeiroclw-pixelandBrennan Benson 827207784d Add OMP skill discovery source (#6422)
* Add OMP skill discovery source

* review: scope OMP discovery to the shared provider and cover orchestration

- Drop the `SkillProvider` widening: `~/.omp/agent/skills` is a provider home
  like `~/.pi/agent/skills`, so it carries `agent-skills` and identifies OMP
  through the source `owner`. That keeps every `Record<SkillProvider, ...>`
  (including the Skills page label map) at a zero diff.
- Add the `omp-home` orchestration coverage location and map OMP to it, so the
  Agent coverage panel stops reporting a real OMP install as missing.

* review: guard the Pi/OMP matcher anchors

Both roots end in agent/skills, so dropping either leading segment let one
agent's install mark the other with a green suite. Also corrects the OMP
owner assertion's rationale: no OMP native-chat profile exists, so owner
matters because a null owner leaks OMP-only skills into other pickers.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-29 15:40:25 -07:00
Neil 3f37e32e72 perf(main): move hang watchdog into a worker thread (#11344)
Keep main-thread hang detection independent of the blocked Electron event loop while reducing watchdog memory from 47.1 MiB to 11.5 MiB. Preserve marker, recovery, and telemetry behavior with a bundled worker-thread entry.
2026-07-29 15:39:34 -07:00
OrcaWinandOrcaWin dde72f85de fix(windows): separate updater from orchestration migration (#11405)
* fix(windows): separate updater from orchestration migration

* fix(terminal): attest adopted reveal identity

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 15:23:06 -07:00
Brennan Benson 857f28554b fix(mobile): keep the provider session id alive while an agent sits idle (#11260)
Mobile Chat UI subscribes to an agent transcript by providerSession.id, so
losing that id blanks the chat: use-mobile-native-chat-session clears the
message list and then returns without subscribing when sessionId is null.

Two places dropped the id on a status ping that carried no session metadata,
both while the agent was idle at its prompt — exactly when mobile reads it:

- The renderer store refused to carry the id across `done`. A completed turn
  does not end the provider session (the TUI stays alive and resumable), and
  OSC 9999 repaints plus reconnect snapshot replays re-deliver a metadata-less
  `done` onto an already-done row, so retention has to cover done -> done.
- The main-process OSC ingest overwrote the cached row without the id. The OSC
  wire payload has no providerSession field, so an OSC observation is never
  evidence the session ended. Dropping it there erased the id from persisted
  rows (lost across restart) and from headless `orca serve`, which serves those
  rows to mobile directly rather than through the renderer store.

Both keep the turn boundary: a new turn after `done` still starts clean, so a
reused pane cannot inherit a finished session.

Closes #10630
2026-07-29 12:36:26 -07:00
Brennan Benson 7477a01f3c fix(mobile): stop a mobile reveal from minting a duplicate terminal tab (#11259)
Revealing a live PTY from a paired phone passes the tab id baked into
its env, but the create-terminal handler only recognised ownership via
tab.ptyId and the live ptyIdsByTabId map. With the worktree closed on
the desktop no pane is mounted, so ptyIdsByTabId is empty and tab.ptyId
holds at most one leaf's PTY -- a split pane's second PTY lives only in
the persisted layout. Ownership missed, and createTab ran with an id
that already existed; it mints a fresh uuid on collision, so a second
tab appeared on the same session (#10486).

Resolve ownership through the persisted layout as well. Every binding
consulted is an exact match on the ptyId, so all of them outrank the
reveal's tab id hint: that hint is written once when the PTY spawns and
is never rewritten, so it goes stale as soon as a pane is dragged to
another tab. A mounted pane outranks a recorded one, same-tier
conflicts stay ambiguous so the mobile mount planner keeps failing
closed, and the hint is what resolves a reveal that nothing records
yet -- which is also what keeps paneKey hook attribution intact.
2026-07-29 12:32:54 -07:00
Brennan Benson c74fb3c71b feat(browser): let Shift invert link routing instead of always forcing the system browser (#10991)
* feat(browser): let Shift invert link routing instead of always forcing the system browser

Shift+Cmd/Ctrl-click has always meant "open in the system browser", which is a
no-op when that is already where links go. Users who keep Link Routing off have
had no gesture to pull a single link into Orca's built-in browser.

Adds "Hold Shift to open in ___", a nested toggle under Link Routing that makes
the modifier open a link the opposite way from the setting. It ships off, so the
one-way escape hatch is unchanged for every existing user.

The title and description name the destination the modifier actually reaches and
flip with the parent setting, since "the opposite" is meaningless on its own.

- openHttpLink gains modifierHeld; resolveModifierRouting owns the decision so
  every surface (terminal URLs, OSC 8, xterm web links, markdown preview) routes
  identically. forceSystemBrowser stays for callers that must bypass settings.
- The terminal hover hint names Orca when the modifier would open there, and is
  re-resolved per hover so toggling applies without recreating panes.
- Link Routing's own copy drops "always uses your system browser", which the new
  toggle can falsify; the nested row states the live destination instead.

* fix(browser): route the Checks panel hosted-review link through the shared modifier

The "Open on GitHub" button had its own Shift+Cmd/Ctrl escape hatch that passed
forceSystemBrowser directly, so it kept the old one-way behavior while every
other surface honored the invert setting. Route it through modifierHeld like
the terminal and markdown paths.

Also wraps the modifier row's description in translate(); the title in the same
file was localized but the description returned raw English (caught in review).

* fix(browser): make link-routing modifier copy true in every state

Review follow-ups on the Shift-inverts-routing change.

- The nested row promised "⇧⌘+click opens one in Orca" in the present tense
  while its own toggle was off, so the out-of-box state described behavior the
  user did not have. Phrase it as enabled-state copy, matching sibling rows.
- The parent Link Routing description gained "opens a link the other way",
  which is false in the default state and contradicts the child row when
  inverting is on. No fixed sentence there is true in every state, so the child
  row — which knows the live destination — now owns the claim. That leaves
  getBrowserLinkRoutingShortcutLabel unused, so drop it.
- The rich markdown editor still forced the system browser while the preview of
  the same file honored the modifier, so one link routed two ways depending on
  which view it was clicked in.
- A remote runtime pins every link to the system browser, so the hover hint
  could promise Orca for a click that lands elsewhere. Gate the hint on the same
  condition openHttpLink uses.
- Index both modifier titles: the search entry is built with openLinksInApp
  false, so the row was unfindable by the title it renders when routing is on.
- Drop the ariaLabel that shared no words with the visible label (WCAG 2.5.3)
  and add the four missing settings-search keyword keys to all five catalogs.

* test(editor): pin the rich markdown Shift+click routing hop

The editor half of the modifier fix had no coverage — reverting it to
forceSystemBrowser left the suite green while the same link routed one way in
the markdown preview and the other way in the rich editor. Also pins that a
non-local source owner survives the hop, since that is what keeps an SSH file's
links out of Orca's browser.

* test(editor): cover the Ctrl chord for rich markdown link routing

This file is the only test of handleRichMarkdownEditorClick, and it exercised
metaKey alone, so the isMac branch of modKey had no coverage off macOS. Also
stop claiming the source-owner case proves SSH links stay out of Orca — it
proves the owner survives the hop; http-link-routing.test.ts enforces the rest.

* style: trim review comments to the one-line house rule

Both explained the change adequately in two lines; the extra lines were worked
examples, not information.

* fix(browser): keep Link Routing copy unchanged until inverting is enabled

Removing the "⇧⌘-click always uses your system browser" sentence outright
reworded the row for every user on upgrade, including everyone who never turns
the opt-in on. Restore it verbatim in the default state and only hand the chord
sentence to the nested row once inverting makes "always" untrue.

* revert(editor): keep rich markdown Shift+click on the system browser

Per Brennan: the editor's Shift path hands the link to the client OS and should
not follow the invert setting — the preview opening in Orca is the intended
divergence, not a bug. Restores main's call exactly; the test now pins the
divergence so a future consistency pass cannot erase it silently.

* fix(browser): surface the inverted modifier on the hosted-review link

The hosted-review click path now passes modifierHeld, so with inverting on and
Link Routing off the chord opens in Orca — but the hint stayed gated on
openLinksInApp, hiding a live gesture. Resolve the destination instead of a
boolean. Default-off output is unchanged.

* test(browser): pin the inert modifier hint when links already open in Orca

* refactor(terminal): require the pane link hint so dropping the wiring fails to build

The optional option fell back to a duplicated copy of the legacy hint string, so deleting the hook wiring reverted the tooltip silently with every test green.

* fix(browser): trim the runtime id before hiding the hosted-review modifier hint

openHttpLink and terminalUrlOpenHintOptionsFor both trim, so a blank runtime id hid the hint while the click still reached Orca.
2026-07-29 12:30:48 -07:00
Brennan Benson caca5d1c96 fix(mobile): route external mouse/trackpad wheel through the terminal scroll router (#11247)
The mobile terminal WebView only handled touch. Wheel events fell through to
xterm, which either scrolls its own hidden viewport or — in the alternate
screen — emits cursor keys via onData, and the mobile onData bridge forwards
those to sendMobileTerminalQueryReply, which drops anything that is not a
query-reply grammar. Net effect: an external mouse or trackpad scrolls nothing
inside the terminal, and nothing reaches the PTY.

Attach a wheel handler on the terminal surface that reuses the touch path's
router: alternate-screen and mouse-aware TUIs get bounded cursor keys / wheel
reports through the existing validated terminal-input gate, and the normal
buffer gets the same coalesced scrollback scroll as a swipe.

Refs #6863, #8818
2026-07-29 12:27:18 -07:00
OrcaWinandOrcaWin 363e478909 fix(orchestration): preserve active workers across updates (#11271)
* fix(orchestration): preserve active workers across updates

* test(ssh): model absent legacy adoption

* test(orchestration): align compatibility contracts

* fix(windows): escape updater PowerShell booleans

* fix(windows): restore stock uninstall process check

* fix(orchestration): keep recovery off renderer startup barrier

* fix(orchestration): harden legacy recovery migration

* fix(orchestration): close recovery review gaps

* fix(orchestration): complete legacy worker cutover recovery

* fix(orchestration): preserve legacy workers across updates

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 11:31:35 -07:00
NeilandOrca 4543bb6826 fix(activity): bound Activity portal readiness oscillation (React #185) (#11326)
* fix(activity): bound Activity portal readiness oscillation (React #185)

React error #185 is "Maximum update depth exceeded" -- an unbounded render loop,
not setState-after-unmount as the crash reports superficially suggest. Verified in
react-dom@19.2.7's production bundle: getRootForUpdatedFiber throws
Error(formatProdErrorMessage(185)) when 50 < nestedUpdateCount. That frame is the
top of all 14 crash stacks across shipped 1.4.156 / 1.4.158 / 1.4.159.

nestedUpdateCount and rootWithNestedUpdates are module-level globals keyed on the
ROOT, not on a component. One runaway loop saturates the counter and the throw
lands on whichever fiber calls setState next, so SortableTab (terminal.workbench),
the Radix Presence/MenuPortal (page.settings) and the sidebar.worktrees button are
innocent bystanders with misleading component stacks -- four boundaries, one bug.

The driver is Activity's portal readiness pass. When 'ready' is unreachable while
'unavailable' stays reachable, updateReadiness flips loading<->unavailable from
inside useLayoutEffect -- React's sync lane, the lane that increments
nestedUpdateCount -- so it saturates and throws rather than merely being slow. A
prior investigation captured this live on Windows via CDP at 239 renders in 847ms.

Bound the oscillation: after ACTIVITY_PORTAL_READINESS_MAX_FLIPS consecutive
loading<->unavailable transitions the latch parks on 'unavailable' until a real
'ready' arrives. The latch rewrites only the hook's output, never the DOM probe,
so a terminal that churns during a slow attach and then genuinely comes up still
releases it. Because the counter is global per root, this also protects every
innocent bystander in the app -- most of the user-visible value.

Extract reconcileActivityPortalThreads and resolveActivityPortalSwap so the portal
loop is testable against the real collaborators. Note the reconciliation keeps its
worktree+tab comparison deliberately: Terminal mounts one TerminalPane per
(worktree, tab) and routes it by worktree+tab, so staging a same-tab pane would
leave the staged slot empty and its readiness stuck on 'loading' forever. Same-tab
switches swap in place via isolatedPaneKey instead.

Co-authored-by: Orca <help@stably.ai>

* fix(activity): coalesce portal readiness updates

* test(activity): tighten readiness regression coverage

---------

Co-authored-by: Orca <help@stably.ai>
mobile-android-v0.0.36
2026-07-29 02:36:54 -07:00
github-actions[bot] ef55429f3d release: v1.4.162-rc.0 v1.4.162-rc.0 2026-07-29 08:50:00 +00:00