Commit Graph
9944 Commits
Author SHA1 Message Date
Jinjing 4ff96df2b5 Auto e2e tests autofix scheduled ci 1h run 32 20260902T0700 (#18227)
* Fix flaky e2e tests with improved locators and synchronization

Add explicit waits, use more robust element selectors, and simplify test
setup to reduce race conditions. Replace file-based fixtures with
programmatic browser creation, use parent-scoped locators for menu
interactions, and poll for stable state before assertions.

* Add E2E failure triage report for run 33564563164

- Reconciles 14 failed tests against job logs and trace artifacts
- Categorizes failures: 8 product bugs, 2 flaky tests, 4 test updates
- Documents test-maintenance fixes and diagnostic findings
- Files 8 Linear issues with owners and fresh recurrence evidence
- Provides next actions for product owners and repository maintenance

* rm artifact notes

* Refactor browser creation E2E test to use UI interactions

- Click through menu instead of manipulating internal store state
- Use Playwright's locator and toBeVisible() assertion patterns

* Record E2E browser creation pageId before barrier check

Move createdPageId assignment before the barrier arm/fire checks. This
ensures the pageId is recorded unconditionally when tracking is enabled,
allowing tests to distinguish between creations rejected before the host
attempt vs those that failed after creation.

* Remove browser page reclamation assertion from restart test

Simplifies test by removing page ID tracking and poll checking
if pages persist after paired runtime restart.
2026-09-02 14:05:29 -07:00
Neil d8ca420cdd perf(remote): apply the no-evidence inspection cadence to remote panes (#18146)
A visible remote/SSH terminal that has never run an agent inspected its
execution host every ~2s forever for a strictly negative answer — ~30
RPC round trips per minute per pane, each a network hop plus a host-side
foreground process scan.

The `no-evidence` 15s cadence tier exists to bound exactly that volume,
but `isProcessInspectionCostly` gated it on local Windows only and
explicitly excluded remote-execution-host PTYs — the most expensive
inspection shape in the codebase.

Extract the predicate to `agent-process-inspection-cost.ts` and treat a
remote-execution-host PTY as costly on every client platform. The local
branch (Windows costly, POSIX cheap) is byte-identical.

Client-side timer choice only: no wire change, no new field, no opcode.
Activity (output/title/hook) re-arms the 2s cadence, agent evidence
returns the tier to active/idle, and the `unavailable` branch and its
error backoff are untouched.
2026-09-02 14:03:46 -07:00
Neil ef4e9c40ab fix(terminal): replay paired-runtime snapshots at the host's grid (#18132)
* fix(terminal): replay paired-runtime snapshots at the host's grid

A paired remote pane parsed the host's authoritative terminal image at
whatever grid its own xterm happened to have. The host dimensions every
snapshot it publishes, but only the REQUESTED snapshot path ever read
`cols`/`rows` back — both PUSH paths (initial subscribe and server
recovery) dropped them, so `onSnapshot` handed the transport an image
with no grid and the drain wrote it as-is.

Serialized frames are grid-relative: rows are newline-fed and the frame
ends in an absolute CUP. Parsed at a different grid they re-wrap and
clip, and because an alternate-screen TUI has no scrollback the rows
scrolled off the top are gone. An idle agent never repaints, so the pane
stays wrong until the next byte arrives — which for a finished Claude
Code session is never.

Carry the grid the host already publishes through the multiplexer and
transport, then reuse the choreography the reattach payload already
follows: resize to the source grid, replay, fit back to the pane, and
push the resulting grid to the PTY. A host that publishes no dimensions
reads as unknown and keeps today's behaviour, so no wire change and no
capability negotiation is involved.

* fix(terminal): keep the source-grid fit correct under mobile fit overrides

Two follow-ups on the source-grid replay:

- A mobile fit override skipped the post-replay fit entirely, stranding
  the pane at the host's replay grid. Fit without the PTY grid push
  instead, matching applyMainBufferSnapshot.
- Reset the source-grid flag when a drain is scheduled: a transaction
  whose restore was skipped never runs afterRestore, and the stale flag
  would fit a later drain that never left the pane's own grid.

* perf(terminal): clear the replay buffer before the source-grid resize

The drain resized xterm to the host's serialization grid and only then
wrote the clearing `2J`/`3J`/`H`. `clearBeforeReplay` is true for every
pushed remote snapshot, so a column change reflowed a full scrollback
that the next sequence discarded microseconds later — on the recovery
push that lands under output flood, when the renderer is already loaded.

The clear is grid-independent, so running it first is equivalent: the
resize then operates on an empty buffer. Verified identical end state
(content, cursor, buffer type, baseY) across cols-change, rows-change,
alt-screen, no-scrollback and equal-grid shapes. Interleaved 25-run
medians on a 10k-line scrollback: 6.19ms -> 2.48ms on the normal buffer,
unchanged on the alternate screen (where `3J` cannot free the normal
buffer's history, so the reflow is paid either way).
2026-09-02 14:03:21 -07:00
Neil 0886db2b90 refactor(process-table): extract the correlation indexes into their own module (#18246)
`src/shared/process-table-snapshot.ts` is 308 code lines against the 300
cap for `**/*.ts`, so `static analysis` is red on `main` and every open PR
inherits it.

Neither PR that grew the file crossed the cap alone. #18151 took it to 427
raw lines; #18166 added ~35 more. #18166's branch predated #18151, so the
head CI linted was 428 raw lines and passed, while the squash onto main is
463 -> 308 code lines. The gate lints the PR head, not the merge result, so
nothing linted the sum until it was on main.

Pure move, no behaviour change: the generic index machinery
(ProcessIdentityRow, ProcessTableIndexOf, buildProcessTableIndex,
collectDescendantsFromIndex, lookupProcessTableIndex, getProcessTableIndex
and its WeakMap) moves to process-table-index.ts. `ProcessTableIndex` and
`scoreForegroundCandidateRow` stay behind because they need
`ProcessTableRow`, which keeps the new module free of any import back and
so introduces no cycle.
2026-09-02 14:01:24 -07:00
Brennan BensonandMerge Sim 623d58e386 fix(native-chat): show pasted images while they save, and make them previewable (#18118)
* fix(native-chat): show pasted images while they save, and make them previewable

Pasting an image into the native chat composer showed nothing until the
clipboard image finished being written to disk, and the resulting chip could
never render the image at all.

Preview was blocked by path authorization, not by rendering. Clipboard pastes
are written to the OS temp dir, which sits outside every allowed root, so the
composer's own `fs:readFile` of the file Orca had just written was denied.
`saveClipboardImageBufferAsTempFile` now authorizes the path it writes, the
same way other Orca-produced external files are handled.

The delay is the macOS paste route: Cmd+V is intercepted in main and delivered
through the app-menu paste channel, which has no clipboard blob in hand, so the
composer only learned an image existed after the save round-trip. A new
`clipboard:readImageThumbnail` probe reads the clipboard in memory and returns a
downscaled preview; it runs alongside the save rather than before it, so text
paste gains no latency. The DOM-paste route needs no probe — it mints a blob URL
from the clipboard file on the same tick.

Attachments now carry `pending` and `previewUrl`: the chip appears immediately
with the real image dimmed under a spinner, then settles in place on the saved
path. Send is blocked while anything is pending, because a pending chip has no
agent-readable path yet. Pending chips are kept out of the pane attachment cache
so a mid-save unmount cannot strand one, and blob previews are revoked on
remove/clear. SSH pastes now carry their connectionId onto the chip so remote
previews read over SFTP.

Verified in a real Codex native chat under an isolated dev instance: the chip
appears in 42-61ms with a spinner, settles at ~141ms, three rapid pastes produce
three independent chips with Send disabled throughout, and the lightbox opens the
full 5120x2880 image read from disk. Ablation confirms the authorization fix:
the written path reads back, an unauthorized sibling in the same temp dir does
not.

Claude-Session: https://claude.ai/code/session_01NnEfY8NpfFtVnboLKnmgdW

* fix(native-chat): avoid stale image attachments and preview cache growth

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-02 14:00:17 -07:00
Jinjing c2fce80289 Fix agent dashboard setting configure (#18245)
* Make agents activity always-on; toggle via bell icon

- Remove optional showAgentsSidebar setting
- Replace sidebar view-toggle with bell-button for activity access
- Agents activity now always accessible in sidebar
- Preserve migration flag for introduction to existing users
- Remove visibility inference utilities

* Simplify sidebar when agents view active: hide workspace options, add to

- Hide workspace options menu and add project button when agents view is
  active, reducing UI clutter in that mode
- Add tooltip to the activity bell button for better discoverability
- Localize sidebar search field text
- Move search and filter toggles to local state in SidebarAgentsList,
  removing unused callbacks from thread list components
- Manage search input focus properly when opening
2026-09-02 13:43:06 -07:00
Jinjing e3de6b2ce8 Add automation runs dashboard with pagination and filtering (#18226)
* Add automation runs dashboard with pagination and filtering

Adds a new Runs view in the Automations page that lets users browse all runs across automations with status/host filtering, search, and pagination support. Includes virtualized table rendering for efficient handling of large run histories and summary cards showing 24h/7d success/failure counts.

* Fix missing dependencies in useCallback hooks and imports

Missing dependencies in useCallback can cause stale closure bugs. This
adds missing state setters to dependency arrays and consolidates type
imports for consistency.

* Use keyset pagination for stable automation runs pages

Pagination now uses createdAt:id boundaries instead of offsets, so new
runs arriving between pages don't shift the window. Maintains backwards
compatibility with legacy offset cursors.

Move pagination to shared module, fix outcome counting for future-dated
runs, and improve hook state tracking on authority re-pairing or target
changes.

* Extract automation run details to top-level page view

Moves run display from detail pane to dedicated page, establishing
three-level navigation (Automations → Runs → Run Details) and simplifying
the detail pane component.

* Fix pagination stability when automation runs share createdAt

- Define a stable total order with createdAt and id tiebreaker to prevent runs tied on createdAt from being dropped when the boundary run is pruned between page requests
- Retain cursor on failed pagination so pages remain retryable
- Update ownerNotice type to AutomationActionNotice

* Extract automations list panel and worktree map logic

Split AutomationsPageSurface into smaller, focused modules for better maintainability and reusability. Move list panel UI rendering to AutomationsPageListPanel component and worktree map selection logic to a standalone utility function.

* Add i18n strings for automation runs dashboard

Adds localized strings for the automation runs dashboard view, including search, filtering by host and status, run counts for 24h/7d windows, and empty state messaging across all supported languages.

* fix missing translation

* fix missing translation
2026-09-02 13:42:14 -07:00
Jinwoo Hong a0de2fde0b fix(terminal): confirm an unrecognized foreground before downgrading agent prompts (#18238) 2026-09-02 13:39:56 -07:00
Brennan BensonandMerge Sim 974f3164a0 Make terminal error overlays opaque (#18231)
* Make terminal error overlays opaque

* fix(terminal): keep opaque error toast text readable

* fix(terminal): keep toast fallback opaque on older browsers

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-02 13:36:00 -07:00
Neil fda9cae5e2 perf(renderer): build useRef seeds once instead of every render (#18159)
* perf(renderer): stop re-running useRef initializers and ref-mirror effects every render

React evaluates the argument you pass to `useRef` on every render and discards
every result after the first. 30 renderer sites did real work in there — walking
every browser page/tab across all worktrees, building activation-order maps, and
minting `crypto.randomUUID()` per render on browser pages and the AI vault.

Also moves 12 verbatim ref-mirror Effects to render-phase assignment, and routes
the `tab.rename` shortcut straight to the focused tab instead of through a store
field every mounted tab subscribed to.

* perf(renderer): drop Fix 2 (render-phase ref mirrors) to satisfy no-ref-current-in-render

* perf(renderer): convert the four lazy-useRef sites that landed on main
2026-09-02 13:32:06 -07:00
Neil efe3305490 test(editor): isolate prefix bleed in the batched model sweep, drop a dead export (#18240)
Readiness-review follow-ups to #18144.

The parity test in closed-editor-tab-disposal.test.ts cannot see prefix bleed:
buildScenario closes tab-0..tab-99, so tab-10 is in the closed batch too and the
per-tab oracle disposes its models via tab-10's own prefix. Batched and oracle
agree and the assertion passes even with a bleeding predicate. Verified by
mutation: replacing the boundary probe with a naive startsWith leaves all five
of that file's tests green.

Adds a test through the batched disposeClosedEditorTabs entry point with a
still-OPEN tab-10 alongside a closed tab-1, which does fail under that mutation.

Also records the `boundary + 1` advance in hasPaneScopeOwner as load-bearing for
`:::` runs, with a test that fails under a `+ 2` "tidy-up", and removes
disposeUnattachedMonacoModelsByPathPrefix, which #18144 left with zero production
callers and a comment claiming it was kept for callers that do not exist.

Finally, documents why title-derived rows carry `startedAt: 0`, which is the sole
reason their `now` stamps cannot move a dashboard bucket.
2026-09-02 13:31:02 -07:00
Neil 1910ab9c9c fix(github): bound and coalesce the Orca star check so gh children cannot pile up (#18239)
`checkOrcaStarred`, `starOrca` and `getAuthenticatedViewer` were the only gh
call sites that reached for the legacy `execFileAsync` instead of
`ghExecFileAsync`, so they ran with no deadline, no process-tree kill and no
coalescing. A `gh` that never exits therefore ran forever and never released
its slot in the 4-wide GitHub semaphore in gh-utils.

Route all three through `ghExecFileAsync`, coalesce concurrent star checks onto
one child, and hoist the Landing star-state effect out of the conditionally
rendered footer so a repo-catalog rewrite no longer remounts it and re-forks gh.

Adds a ratchet test asserting no file outside the command runner names `gh` as
a spawned program.

Fixes #18234
2026-09-02 13:29:12 -07:00
Neil c7886e7a5f perf(diff): stop redrawing the whole combined-diff file tree on every section load (#18140)
Both PR files viewers rebuilt the section-index Map with
`useMemo(..., [sections])`. An on-demand section load replaces the sections
array while the section keys stay identical, so every load handed the file
tree a new Map identity — a memo miss for all ~900 `CombinedDiffFileTreeRow`s.

`useCombinedDiffTreeNavigation` already cached the map behind an
entry-signature + per-index key comparison. Extract that into
`useCombinedDiffSectionIndexMap` and use it from all three call sites. The
extracted hook seeds its cache from a `useLayoutEffect` rather than during
render, so a render React discards cannot leave behind an entry describing
sections that never committed.
2026-09-02 13:24:32 -07:00
Neil d4fa091714 perf(terminal): repaint only the rows an agent redraw touched (#18169)
Forced foreground repaints asked xterm for rows 0..rows-1. xterm's render
debouncer unions ranges, so one full-grid request widened every frame to a
whole-viewport `_updateModel` cell walk even when the write changed five rows.
Re-issue the repair over the parse's own dirty span instead, keeping the
whole grid for viewport scroll, alternate-screen flips, and any write whose
span cannot be observed.
2026-09-02 13:22:29 -07:00
Neil 6b1cbe54a1 fix(process-table): fail a short ps capture loudly, and stop a resume spending 49 of them (#18166)
The POSIX process-table capture ran `execFile('ps', ...)` with no `maxBuffer`,
inheriting Node's 1MB default. Measured at 1,460 processes the capture is 326KB
with a 5,116-char longest row — ~3x headroom, which a busy host clears.

Two separate defects follow, fixed here:

1. `parseProcessTableRows` drops unparseable lines, so any short capture reads
   as a COMPLETE table whose missing processes simply are not running. Verified:
   a capture cut at 4KB parses to 59 of 1,463 rows, and an empty capture parses
   to `[]`, both with no error — and `resolveAgentForegroundProcessWithAvailability`
   then answers `available: true`. That is the `unverifiable` -> `exited` collapse
   the execution boundary forbids. The capture now rejects with
   `ProcessTableCaptureError` on a ceiling-length or row-less capture, so both the
   lenient and strict views fail loudly and callers report unavailable.

2. `maxBuffer` is now an explicit 32MB, matching the sibling reader in
   `pty-descendant-termination.ts` and its stated reasoning. Without it a 4,000-
   process host fails EVERY capture, degrading the whole subsystem permanently.

Separately, `readStructuredTuiProcessIdentity` polled a fresh whole-machine `ps`
every 50ms for up to 5s. Each capture costs ~0.065 CPU-s, and the 5s ceiling is
only reached when the child never appears — where the tight interval buys
nothing. The interval now holds at 50ms for the first second, then doubles to a
500ms cap. Identification latency is unchanged for any child appearing inside
that window, and the 5s ceiling is unchanged.
2026-09-02 13:22:25 -07:00
Neil 698584f6ec perf(pty): stop startup history GC freezing the main process for seconds (#18165)
`runHistoryGc` walked every terminal-history directory synchronously ten
seconds after launch: `readdirSync` on the root, then per directory a
`statSync`, a `readdirSync`, a `statSync` per file, an `existsSync`, a
`readFileSync` and a `JSON.parse`. On a real 613 MB root (2,776 dirs /
6,703 files) that is ~20,000 syscalls and 2,774 parses in one
uninterruptible pass — the main process was frozen for the whole of it,
7.5-10.6 s on the reporting machine.

Move the enumeration to `fs/promises` behind the existing
`forEachWithConcurrency` fixed-worker pool over an iterative frontier,
yielding through the shared `yieldToEventLoop()` every 32 entries. The
pass is cancellable and a second call joins the in-flight one rather
than racing its tombstone renames.

Max main-thread gap over the real root: 122-155 ms -> 1.1-1.9 ms idle,
522 ms -> 1.1 ms under load. Syscalls per pass 20,578 -> 17,803. Total
elapsed is lower too (88-108 ms vs 126-154 ms warm), so nothing is
smeared into a longer tail.

The prune decision logic and the tombstone path are untouched. A new
suite asserts the new walk removes exactly the set the synchronous walk
chose over a fixture covering every decision shape, and covers the races
async introduces: a directory removed mid-walk, a half-written
`meta.json`, and malformed/truncated/oversized metadata. All of those
resolve to "keep", matching what the sync version did on a read error.
2026-09-02 13:10:05 -07:00
Neil b00ec20731 perf(startup): stop an unreachable SSH host from gating local terminal restore (#18164)
* perf(startup): stop an unreachable SSH host from gating local terminal restore

An asleep or unreachable SSH target held the terminal-restoration gate for the
full 15s reconnect timeout, so no terminal restored — local ones included.
Startup now awaits only the target that owns the active workspace's tabs and
lets the rest connect in the background, folded into the existing deferred path
that reattaches their PTYs on tab focus.

Also splits the renderer's git-environment fence out of the first-window PTY
services barrier: worktree hydration needs shell-PATH generation and the managed
WSL CLI registration, not a daemon PTY spawn or a hook-server bind. Terminal
restoration still fences on the first-window services via
app:prepareTerminalStartupRestoration.

Measured with tests/tools/benchmarks/startup-time-bench.mjs (382 restored tabs,
28k-file profile, medians of 3):
  unreachable SSH host: 17.27s -> 1.34s to renderer-startup-hydration-done
  all-local:             1.98s -> 1.33s

* fix(startup): restore the startup-ordering oracle and keep a connected background SSH target undeferred

app-startup-routing.test.ts pinned the old step names, so the two ordering cases
went vacuous-then-red when the barrier split. Repoint them at the steps that now
carry the same fences: 'git-environment-barrier-await' (shell PATH + managed WSL,
the fence host Git needs) before hydration worktrees, and
'prepare-terminal-startup-restoration' (which awaits firstWindowStartupServicesReady
in main) before terminal reconnect. Both still fail against main's hydration source.

Also: the timed-out-eager rewrite of the deferred list re-added background targets
that had already connected, undoing removeDeferredSshReconnectTarget and sending
fresh panes on a reachable host down the cold-restore path.
2026-09-02 13:10:02 -07:00
Neil d22d5c80a6 fix(terminal): stop a hidden pane's cursor blink deterministically (#18152)
A retained hidden pane keeps a live WebglRenderer, and the only thing that
stops its 600 ms cursor-blink timer is a real DOM blur event. Today that
arrives incidentally from display:none/visibility:hidden; under a hide mode
that keeps focus (opacity:0 without inert) it never fires and the pane blinks
— redrawing its whole cursor row per toggle — until the 5-minute idle timeout.

Park terminal.options.cursorBlink on suspend and restore the parked value on
resume, so the property holds regardless of which CSS hid the pane. Settings
writes land on the parked value while hidden, so a mid-hide settings change
cannot re-arm the timer behind the surface, and a user who disabled blink
never gets it back.
2026-09-02 13:08:27 -07:00
Neil 53f105827b perf(windows): stop asking the process table for memory, and share one projection per snapshot (#18151)
Two costs on the Windows process-table hot path, plus the EDR doc that
described neither of them accurately.

1. The snapshot set `ProcessDataFlag.Memory` and surfaced `memoryBytes`,
   which nothing read. The addon serves that flag with a second
   `OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)` and a
   `GetProcessMemoryInfo` per process (process.cc:47-63), so the flag was
   one wasted handle per process per snapshot.

2. The shared TTL cache gave every pane the same native rows array, but
   each pane still ran `native.map(toProcessRow)` over the whole table,
   rebuilt a `childrenByPpid` Map from scratch, and did two linear scans.
   The `.map()` also handed `getProcessTableIndex` a new array each call,
   defeating the POSIX memo by construction. Both now cache per snapshot
   identity, and the POSIX resolver drops its duplicate descendant walk.

`getProcessTableIndex` / `buildProcessTableIndex` are generic over the row
shape so the Windows rows reuse the existing pass instead of a parallel one.

No behavior change: same rows in, same rows out, same descendant ordering
and same has-children answers.
2026-09-02 13:08:01 -07:00
Neil 9377214b4b perf(renderer): reconcile hydrated workspaces in one store write (#18150)
* perf(renderer): reconcile hydrated workspaces in one store write

Session hydration reconciled each workspace with its own set(), so a
193-workspace session fanned 193 writes out to every non-React store
subscriber and re-spread three whole workspace-keyed maps per workspace.
Fold the whole session into one patch, release the string-keyed terminal
scroll-intent entries on pane close, and drop the per-workspace/per-tab
reconnect debug logs.

* fix(test): make the hydration fixture bucket switch exhaustive

oxlint --type-aware flags the default arm; naming the editor case clears it.
2026-09-02 13:07:58 -07:00
Neil 89bd18990d perf(renderer): stop three always-mounted selectors rescanning the store (#18136)
Zustand reruns every subscriber's selector on each store write. Three
selectors did an O(N) scan of a store collection inside that path, so at
10 repos / 423 worktrees / 382 tabs they were paid thousands of times a
second while the app sat idle.

- getLocalWorktree / getLocalRuntimeRepoForWorktree now read the shared
  WeakMap indexes (getIndexedWorktreeById, getIndexedRepoMap) instead of
  `Object.values(worktreesByRepo).flat().find(...)` and `repos.find(...)`.
  SidebarTaskNavButton is always mounted and calls this on every write.
- selectRepoByIdForActiveWorkspace caches its host-scoped resolution in a
  WeakMap keyed on the `repos` array, mirroring getIndexedRepoMap.
- getProjectRuntimeSessionSummary memoizes per (tabsByWorktree,
  ptyIdsByTabId, agentStatusByPaneKey, repoId) and reuses the existing
  identity-cached getTabIdToWorktreeId index.
2026-09-02 13:05:41 -07:00
Neil dff7f91401 perf(sidebar): stop rebuilding per-card selector records on every store write (#18133)
Zustand re-runs every mounted subscriber's selector on every store write. The
per-worktree sidebar selectors built a fresh Record per call, so 15 visible
cards x 6 reads x every write allocated a record each time even when nothing
they read had changed.

- Add createWorktreeRecordSelector: gates the build on the source slice
  identities, memoizes per worktree id, and carries the previous generation
  forward so a rebuild with equal contents keeps its reference.
- Route the pane-title, live-PTY, layout-root and terminal-layout selectors
  through it, and return a shared frozen empty when a worktree has no tabs.
- Swap useWorktreeAgentRows' inactive-branch `[]`/`{}` literals for the shared
  frozen constants so the `active` gate actually short-circuits on identity.
- Identity-cache the sidebar pending-worktree-creation key list, which ran
  Object.values(...).map(...) from an always-mounted subscriber.
- Drop `key={text}` from TruncatedSidebarLabel so a label change remeasures in
  place instead of remounting the span and rebuilding its ResizeObserver.
- Remove the non-compositable `width` from the board drop indicator's
  will-change hint.
2026-09-02 13:05:35 -07:00
Neil 28cb372559 perf(platform): resolve the immutable platform payload once (#18135)
`window.api.platform.get()` runs ~19x/sec while the app is idle. Every call
recomputed a payload whose fields are all fixed for the process lifetime
(`process.platform`, `process.getSystemVersion()`, `process.arch`, the shell
env vars, and the env-derived Linux display server), allocated a fresh object,
and crossed the context bridge.

Memoize the payload lazily at preload module scope and freeze it, and cache the
resolved platform in `getRendererAppPlatform()` so the 32 renderer call sites
stop crossing the bridge on every render. The user-agent fallback stays uncached
because the web client installs its platform API after boot.
2026-09-02 13:05:30 -07:00
Jinjing ae1dab40d6 Sta 6308 add copy session id option to terminal tab context menu (#18070)
* Move Copy Session ID from tab to terminal pane context menu

- Relocates session ID copy to the exact pane that owns it, not the tab's active pane
- Adds support for durable sleeping agent sessions as fallback for cleared live status
- Generalizes copy-rejection guards to handle any identity type, not just pane IDs
- Updates e2e test to verify pane-specific session ID copying

* Gate session ID liveness by shell foreground state

Once OSC 133;D proves a pane is back at the shell, don't return the
session ID even if a durable record survived the exit. This prevents
treating exited sessions as still active when the user is typing at
the prompt.

* Update hook order parity test for session-ID projection hook

The pane session-ID projection adds a render hook to TerminalPane.
Update the expected hook count from 229 to 230 and the corresponding
SHA256 hash.
2026-09-02 13:05:24 -07:00
Neil 084dbbc3b3 perf(persistence): build the state file once per save instead of seven times (#18161)
Every debounced save stringified the full persisted state, then ran two
`String.replace` passes per secret sentinel — one for the on-disk payload, one
for the guard hash. Each replace returns a rope the next one has to flatten
before it can search, so three sentinels cost seven flattened copies of a
4.65 MB state (a two-byte V8 string, ~8.9 MB each), and the state was then
UTF-8 encoded twice more: once inside `sha1.update(string)` and again inside
`handle.writeFile(payload, 'utf-8')`.

`applySecretSentinelSubstitutions` walks the state once with a single
alternation regex, encodes each literal run to a Buffer exactly once, and feeds
those same buffers to both the payload and the hash. Measured on the author's
4.65 MB store with three live secret slots: 48.8 MB -> 17.9 MB allocated per
save, 26.6 MB -> 0 of large_object_space churn, and 22.1 -> 15.1 ms (min) /
32.3 -> 16.9 ms (median) for build+hash+encode. Bytes on disk and the guard
hash are proven identical to the previous loop.

Separately, non-local host session partitions carried stale replicas of the
`browserUrlHistory` global — 589,807 bytes, 12.7% of the file — that neither
the split (which writes globals only to 'local') nor the merge (which reads
them only from 'local' unless local has none) can ever reach. The load path now
drops them when the local slice already holds the field. Only the two history
globals are dropped: the rest are read out of every partition by the worktree
ownership sweep or the mobile/runtime projections.
2026-09-02 13:05:16 -07:00
Neil f19a860a0f perf(terminal): sweep the parked-watcher registries once per pass, not once per workspace (#18157)
The always-mounted terminal controller looped every workspace surface (423 on
a large profile) and called syncParkedTerminalTabWatchers per surface; that
function scans both module-level registries in full, so one effect fire cost
surfaces x registry — 323,172 map-row visits at 423 workspaces / 382 tabs.

Add syncParkedTerminalTabWatchersForWorkspaces, which walks each registry once
and then runs the per-tab start/reconcile pass; the single-worktree entry point
delegates to it. Registry rows are tab-id keyed and a tab belongs to exactly
one worktree, so hoisting the dispose and capture sweeps ahead of the start
passes only reorders work across disjoint tab sets.

Also derive workspaceSurfaceIds/workspaceSurfaceIdSet once in the workspace
foundation (through the existing useReusedArrayIdentity) and key the watcher,
parking and browser-retention effects on the id array instead of the surface
array, which is re-identified on every worktree write. And pass the sidebar's
already-computed defaultHostId into useVisibleSidebarWorktrees so an unrelated
settings write stops re-running the 423-worktree visibility scan.
2026-09-02 13:04:22 -07:00
Neil 21a706a932 perf(terminal): stop shipping every agent spinner title frame to the renderer (#18155)
Main re-asserts a working OSC title per pane every 80ms (12.5/sec) while an
agent works, and every frame became its own pty:sideEffect IPC message. Both
renderer store writes already discard those frames via
isDecorativeAgentTitleFrameChange, and paired remote clients already never see
them (RuntimeClientEventBus's per-listener title gate). Only the local desktop
renderer was still paying for them.

Apply the same decorative gate main already computes for the mobile fan-out one
hop earlier, keeping a 500ms heartbeat so the renderer's 1500ms hook-done quiet
window still sees a working title and can cancel a Pi/OMP milestone 'done'.
2026-09-02 13:04:06 -07:00
Neil 93cb1074b7 perf(renderer): stop the mobile sync key rehashing every dirty file on each keystroke (#18154)
`useRuntimeGraphSync` is mounted unconditionally, and its projection layer runs
on every store write. Four of those projections did work proportional to the
whole slice rather than to what changed:

- `buildRuntimeMobileEditorDraftsProjection` FNV-hashed every open dirty draft
  on every `setEditorDraft`, which Monaco fires per keystroke with no debounce.
- `buildRuntimeMobileOpenFilesProjection` and the browser projection rebuilt and
  re-stringified everything on any `isDirty`/title/url/loading change.
- The agent-status sort built an ICU collation per comparison for a string that
  is only ever compared with `===`.

Each now memoizes per entry against the previous build, mirroring the tabs and
agent-status projections that already did. The duplicated draft-hash loop in
`mobile-session-inputs` is gone; both consumers share one memo.

The session-write subscriber also identity-scans SESSION_RELEVANT_FIELDS before
allocating its 35-field snapshot and changed-field array.

Projections are byte-identical apart from the agent-status sort order, which is
never displayed.
2026-09-02 13:04:02 -07:00
Neil c0e5b189aa perf(sidebar,terminal): memoize terminal-title agent classification and lineage projections (#18148)
Idle-app CPU profiling showed `titleHasAgentName` running 11,771x/sec and the
legacy any-agent regex 4,399x/sec, roughly once per zustand subscriber notify.
The regexes were already precompiled; the problem was call volume — every store
write re-classified every unchanged pane title through the whole agent-name
ladder.

Every title classifier is pure in the title string, so memoize them on it
(bounded FIFO, 1024 entries). A new title is a new key, so there is no staleness
window. The same profile showed the sidebar lineage projection re-scanning all
worktrees several times per pass; cache it on the identity pair of its two
immutable inputs, mirroring store/worktree-repo-index.ts.
2026-09-02 12:59:46 -07:00
Neil f03043544a perf(keybindings): stop recomputing shortcut labels on every render (#18145)
Shortcut labels were rebuilt from scratch in the render body of every
component that shows one, which kept parseKeybinding running ~120x/sec
in a fully idle app.

- Cache the label layer per overrides object (WeakMap), so a keybinding
  edit hands out a new object and therefore a fresh cache.
- Memoize parseKeybinding behind a bounded cache; binding strings come
  from a fixed definition set plus user overrides.
- Hoist the per-call token/label object literals in normalizeKeyToken
  and formatKeyToken to module constants.
2026-09-02 12:59:42 -07:00
Neil 3d67f2be2c perf(editor,source-control): batch closed-tab model sweeps, drop 40 store subscriptions (#18144)
Closing N diff tabs scanned the global Monaco model registry 2N times and
rendered both URI forms for every retained model on each scan. The Source
Control panel opened 42 store subscriptions from one hook, 40 of which watched
action identities that are fixed at store construction and can never change.
2026-09-02 12:56:26 -07:00
Neil 5510ad0cad fix(diff-comments): stop review refreshes from blanking inline comment cards (#18142)
commentableLineSet was memoized on array identity. Review surfaces hand the
decorator a fresh-but-equal number[] on every PR/MR data refresh, so the set
churned, tore down the overlay+zone effect (unmounting every comment card's
React root and clearing the zone map) while the zone-creating effect — which
does not depend on the set — never re-ran. Monaco kept the view zones as
untracked blank gaps, and the next refresh stacked more on top.

- memoize the set on a joined value key so equal refreshes are a no-op
- split the add-button overlay (needs the set) from the zone teardown (must
  not), so the teardown's deps stay a subset of the zone-creating effect's
- have the teardown actually removeZone what it stops tracking
2026-09-02 12:56:23 -07:00
Neil 104f9655e4 perf(git): answer remote-URL questions from one subprocess, not one per remote (#18158)
Four copies of the same loop ran `git remote` and then a serial
`git remote get-url <name>` per remote to answer "which remote has this
URL". On a repo with 58 remotes that is 59 subprocesses -- measured at
1083 ms -- for one question, and worktree create asks it several times.
`git remote -v` answers for every remote from one child, reporting the
same insteadOf-expanded first fetch URL `get-url` prints.

The batched `cat-file --batch-check` branch-conflict probe decides from
stdout, but its WSL route was unfenced, so a login-shell fallback printed
the distro banner onto the stream it parses. That broke the
one-line-per-ref contract, made every batch undecided, and fell straight
back to one `show-ref` per remote -- the cost the batch exists to remove.

Measured at 58 remotes / 4346 branches, spawns and wall time:
  push-target remote scan      59 -> 1  (1083 ms -> 8 ms)
  branch-conflict probe        60 -> 3  (984 ms -> 43 ms)
  configured push target      123 -> 6  (2707 ms -> 157 ms)
2026-09-02 12:53:48 -07:00
Brennan BensonandMerge Sim 1d94ebee3f fix(agents): stop a deeper vendor helper from stealing a pane's agent identity (#18062)
* fix(agents): keep outer agent identity over vendor helpers

* fix(agents): preserve outer identity across relay scans

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-02 12:49:15 -07:00
Brennan BensonandMerge Sim 616fa751da revert(native-chat): drop the speculative Fable model-switch detections (#18215)
Both changes shipped in #18055 were written against strings never observed
in a real session, and neither fixed a reported problem. Guessing at agent
output we have not seen is how the picker got a row that silently no-ops.

Fable consent detection is removed outright. It watched the session for
"Fable N uses usage credits and needs a one-time consent" and answered
`interaction-required`. No consent prompt appeared in any validation run —
the test account had already consented — so the matched wording was never
confirmed. With the detector gone nothing produces `interaction-required`,
so the outcome leaves the union and its unreachable handler goes with it.
A real consent prompt now reports the switch as unverified, which is the
honest failure mode for output we cannot recognize.

The weekly usage scope goes back to exact `display_name === 'fable'`. It
had been widened to `/^fable\b/` against a hypothetical rename of
Anthropic's own usage window; the API still reports "Fable", so the match
was insurance against a scenario with no evidence behind it.

Tests covering the removed behavior are deleted rather than rewritten,
including the two pre-existing `interaction-required` cases that asserted
the terminal is revealed.

The disabled-row filter from #18055 is deliberately untouched.

Claude-Session: https://claude.ai/code/session_01SJy4XGrdre6YaU1wYNKak4

Co-authored-by: Merge Sim <sim@local>
2026-09-02 11:20:53 -07:00
Jinjing 6c66487fca ci: checkout PR head for reusable E2E (#18230) 2026-09-02 11:20:15 -07:00
Jinjing 61e010079f New agent dashboard (#18222)
* more obvious toggle

* more obvious toggle

* feat(activity): redesign thread rows and add child agent filtering

- Emphasize task title and last activity in row layout over metadata
- Add child agent toggle; hide orchestration workers by default
- Support collapsible groups and ungrouped view mode
- Improve orchestration worker message handling to surface replies
- Add sidebar search and filter controls for agent activity

* periodic checkin

* feat(activity): add "Clear completed" action and performance improvement

- Add "Clear completed" action for activity threads with undo window; clears completed and interrupted rows from view, persists across restart
- Virtualize activity thread list to render only viewport-bounded rows
- Cache activity thread search text to prevent recomputation on every keystroke
- Cache dashboard bucket counts per-worktree for selective invalidation on unrelated changes
- Use useDeferredValue for activity search filtering to keep input responsive
- Make compact mode the default display for activity threads
- Add activity-cleared-at persisted state tracking (per-pane cutoff timestamps)

* improve style

* minor change

* feat(activity): add persisted host and project filters to agents view

Agents scope filters are deliberately separate from workspace-nav filters so a monitoring surface never inherits workspace context silently. Filters survive restarts and always display an active-filter chips row with hidden count, making filtering visible and reversible.

* Graduate Agents view from experimental, refine activity handling

- Agents Dashboard moves from experimental to standard feature with showAgentsSidebar setting controlling visibility
- Add identity-checked cache eviction (dropPersisted IPC) to prevent newer runs from being evicted when UI clears older status, fixing clear-completed safety
- Extract ActivityThreadHoverCardSummary and ActivityThreadListToolbar components for better organization and reusability
- Implement mark-thread-read as separate action from select with clickable bell icon
- Add hasActivityThreadWorkspace helper for checking workspace availability across hosts (SSH/runtime targets)
- Preserve scope filter array identity during hydration for memo optimization
- Track manually-unread turns in auto-ack to prevent re-acknowledgement
- Clean up activity cleared-at cutoffs on pane retirement
- Remove activity-thread-hover-card max-lines lint override (code refactored below threshold)

* Refactor agent cache identity to use timing fields only

- Simplify AgentStatusCacheIdentity: keep only paneKey, receivedAt, stateStartedAt
- This fixes silent no-ops where renderer-enriched fields diverged from main's cache
- Add worktree-jump-navigation for navigating activity to workspaces
- Add manual mark-unread protection separate from auto-ack
- Optimize activity owner resolution with per-build memoization
- Optimize detected worktree lookup with indexed search

* Remove sticky header, add scroll position persistence

Replace the floating sticky header overlay with scroll position memory via
a ref. This preserves the user's scroll location when switching between
threads or remounting the agents list, improving UX without requiring
React state.

* Implement sticky group headers in activity thread list

Keep group headers visible at the top while scrolling when threads are grouped. Headers stick to the viewport while their section is in view, then unstick as the next header approaches.

* add blue flash

* update settings appearnce

* Extracted activity acknowledgement/clearance actions from the oversized UI slice.
  - Removed dead sidebar search/menu props and the unused search ref.
  - Removed the unnecessary sidebar visibility bitmask.
  - Replaced hardcoded sidebar toggle colors with design-system tokens.
  - Removed duplicate “mark all read / clear completed” controls in the sidebar.
  - Preserved manual-unread state correctly across pane retire, transfer, and drop.
  - Made clear-completed cutoffs monotonic so clock skew cannot resurrect old activity.
  - Fixed blank workspace names in hover cards with the existing fallback helper.
  - Added missing localization entries and stabilized hydrated filter array identity.
  - Updated misleading Agents setting copy to describe both sidebar surfaces.

* add onboarding guide for the new agents panel

* Add activity clearance tracking and synced agent view settings

Agent view filters and presentation settings now sync across paired clients.
Preserves per-pane activity clearance cutoffs in persistent state. Improves
activity thread row accessibility with proper ARIA roles, and preserves
terminal host ownership after pane teardown via retained terminal handle.

* rm html

* Graduate Agents from experimental and improve activity visibility

- Migrate `showAgentsSidebar` setting from legacy experimental flags; default new profiles to the agents sidebar
- Replace scoped-thread filtering with visible-thread filtering so bulk actions (mark all read, clear completed) only affect rendered rows
- Rewrite child agent classification as a set of visible pane keys to fix orphan promotion and parent-cycle handling
- Improve activity cleared-at cutoff lifecycle: preserve on row dismissal (pane may still be live) but clear on pane removal
- Add pagehide flush for pending clear-completed evictions so quit/reload cannot replay cleared activity
- Polish agents sidebar: unread count badge, expand button, onboarding intro for migrated/new users
- Extract shared time-ago formatting to a library module
- Fix scroll restoration to defer until content can contain the saved offset
- Improve stable message hold for compact agent rows using state instead of refs
- Add worktree filter-visibility check to distinguish collapsed-but-unfiltered from filtered-hidden

* Graduate Agents from experimental and improve activity visibility

- Remove the deprecated full-page Agents view; fix settings navigation fallback
- Refactor bulk action bindings and separate mark-all-read from visible threads
- Preserve sidebar collapse state across remounts; fix child-agent badge filtering
- Add safety window for scroll-restore and improve worktree host-qualified filtering

* Graduate Agents from experimental and add manual unread tracking

- Move Agents sidebar from experimental settings to standard feature with intro flow
- Add persistent manual unread turn tracking for activity feed
- Consolidate workspace activation through activateAndRevealWorkspace dispatcher
- Improve sidebar view toggle with radio semantics and arrow-key navigation

* Graduate Agents sidebar and separate dashboard experiment

The Agents tab now has its own `showAgentsSidebar` setting (defaults on) independent from the dashboard popout experiment. Activity unread counting is simplified to count all events uniformly without mode-specific filtering. Dashboard visibility is now controlled solely by `experimentalAgentDashboardPopout`, with its own UI in the Experimental settings pane. Migration path updated: only `experimentalActivity=true` graduates to the sidebar; the dashboard experiment remains separate.

* Add agent-session tab support to activity tracking

Build activity event contexts from structured agent-session tabs and
worktree-attributed status entries. When activating a thread, try
agent-session tab activation before falling back to terminal pane.

* • The workspace sidebar tab is now a static Spaces
  label—no grouping-based “Projects” label or hidden
  width-reservation span.

* Show unread count badge and prioritize attention-needing agent threads

Activity group order now surfaces threads needing attention (blocked,
waiting, interrupted) before working/done so they're never buried. The
Agents tab shows an unread count badge while viewing Spaces, since the
open Agents list already highlights unread rows.

Also improves UX text ("Hide Agents" vs "Maybe later"), accessibility
with proper ARIA labels, and handles edge cases: preserves read state
for retained panes on SSH reconnect and handles deleted worktrees
gracefully in navigation.

* Batch agent-status evictions and optimize activity pane rebuilds

- Add dropPersistedStatusEntries batch API; consolidate evictions into one persist
- Implement fallback timeout in clear-completed for unseen toast callbacks
- Project only activity-relevant tabs; memoize terminal tab derivations
- Stabilize activity virtualizer key to prevent unnecessary item measurements

* Remove unread count badge from Agents sidebar tab

Simplify useActivityUnreadCount by removing the enabled parameter and
conditional logic, as the badge is no longer displayed in the UI.

* Deduplicate activity unread counts across source overlaps

Live pane status is the primary source; retained and migration entries
serve as fallback caches that may briefly overlap it during lifecycle
transitions. Count each pane only once by tracking seen keys, prioritizing
the live status as the canonical source.

Also fix monitoring state display: it's a distinct agent state, not a
tool-running row state, so exclude it from tool preview checks.

* Update activity pane tests to remove unread badge assertions

- Remove ActivityPaneVisibility type and readActivityPaneVisibility() helper
- Update agentsSidebarButton selector to match badge-less state
- Simplify assertions to check pane focus instead of visibility isolation
- Remove test for unread badge acknowledgement flow

* Fix activity pane workspace resolution and localization handling

- Thread defaultHostId through activity operations for correct host resolution
- Add language-aware caching for standalone terminal names with cache invalidation
- Fix scroll restoration bounds calculation for tall viewports
- Add focus management to sidebar radio group keyboard navigation
- Refresh localized sidebar content on language changes
- Preserve activity state across heartbeats to prevent history loss
- Improve host-id strictness in worktree jump navigation

* Preserve activity view when settings fetch fails

A failed window.api.settings.get() leaves settings null, which was
incorrectly treated as opt-out. Add the missing null check so the
activity-view gate only applies when settings are available.

Includes tests for this scenario and related edge cases in keyboard
navigation, worktree jumping, and session state handling.
2026-09-02 11:00:24 -07:00
Jinjing 6062edf296 test: simplify remote pane link routing to server-hosted placement (#18219)
Remote-pane links are now explicitly server-hosted regardless of generic
client-hosted preference. Remove client-hosted placement verification,
placement-switching test acts, and related type definitions. Focus the
test on verifying the core invariant: links stay server-hosted on their
owning runtime.
2026-09-02 10:39:46 -07:00
Jinjing 8dc3c1dd97 Display favicons for browser website entries (#18099)
* Display favicons for browser website entries

Capture favicons from pages as they load and persist them with browser
history entries. Display favicons in tabs, tab creation search results,
and palette searches to improve visual recognition of websites and help
users identify pages at a glance.

* Fix favicon retry on back navigation after load failure

Reset the favicon failure cache when the favicon URL changes, enabling
retry of a previously failed favicon when navigating back to the same
URL. Distinguish between explicit null (clear cached favicon) and
omitted (don't update history), so stale favicons don't persist
incorrectly.
2026-09-02 10:21:28 -07:00
Neil f737f3499f fix(relay): stream an oversized fs.listFiles reply instead of refusing it (#17954)
Opening Orca's own checkout over SSH cannot list its files in one response frame.
22,617 tracked paths average 58 characters, so the 20,001-row page the client asks
for serializes to 1,223,415 bytes — past `DISPATCHER_CONTROL_QUEUE_MAX_BYTES`, so
`sendResponse` demotes it to the `legacy-response` lane, where an unrelated
producer backlog can refuse it as an opaque `ResponseOverCapacity`. Break-even is
around 49 characters of average path; any `packages/<name>/src/...` monorepo is
over the line.

Picking a ceiling to refuse at does not fix that, it just moves where it shows up
and refuses listings that would have been delivered. `__streamResponse` already
exists for exactly this on the git methods, and it is its own negotiation in both
directions: an old client never sends it and gets the plain array on the
legacy-response lane as before, and an old relay ignores it and answers plainly,
which the client detects by the sentinel marker being absent. So fs.listFiles opts
into it — no new method, no new opcode, nothing to advertise — and the size of a
listing stops being a correctness question.

The response-stream registry becomes one per relay, shared by FsHandler and
GitHandler. A second registry is not an option and the header of
git-response-stream.ts says why: a client keys reassembly on `streamId` alone, so
two would hand out the same id and cross-feed chunks, and only the handler that
registers `git.responseAck` can credit the window a pump parks on.

Also declares `maxResults` on the runtime-RPC `files.listAll` and forwards it.
The mechanism "the client names its cap, so a full page reads as truncation" was
wired only on the Electron IPC hop; web and mobile were saved incidentally by
`remoteFileContentBudget` defaulting the cap inside `listRuntimeFiles`. A new
optional field is additive in both directions (wire rule 1).

The new Docker-gated spec is claimed by run-ssh-docker-e2e.mjs. The sharded e2e
lanes set no ORCA_E2E_SSH_DOCKER, so a Docker-gated spec that no runner names
self-skips everywhere and still reports green — pr-e2e-gate-contract enforces that.

Closes #12547
2026-09-02 05:36:54 -07:00
Neil e5a1e79e8e docs(linux): say which package to install and how updates arrive (#18123)
* docs(linux): say which package to install and how updates arrive

Closes #5188. Closes #10987.

The install guide's entire Linux section was "AppImage and `.deb` builds are
available. See the Releases page for details." It named two of the three
published packages, gave no basis for choosing between them, and said nothing
about updating -- which is the one thing that actually differs between them.
Separately, nothing human-facing said the Linux CLI is `orca-ide`; only
skills/orca-cli/SKILL.md carried it, which agents read and humans do not.

Install page now picks the package by update behaviour: the AppImage
self-updates, deb/rpm report the new version and hand over the install command,
and a repackaged build is not offered a download it cannot apply. Records that
Orca never escalates privileges for the package install, and points at #18086
for the signed repo as planned, not shipped.

Adds .rpm to the download list. Release CI builds it
(release-cut.yml: `--linux AppImage deb rpm`) and
verify-release-required-assets.mjs requires the artifact, so omitting it was
just wrong.

The CLI command name is now stated where humans hit it -- the CLI reference and
overview -- with the GNOME Orca collision as the reason, plus the two places
bare `orca` does work: inside Orca-managed terminals (PTY PATH shim) and on a
packaged `orca serve` host (the ~/.local/bin dispatcher). The headless guide
gains the same note, which is what makes its `orca skills install` lines
correct rather than a typo.

* docs(linux): fix install ordering, CLI verification, and serve bootstrap

Readiness review found ten defects. Two would have had a reader run the wrong
program, and one would have had them install a .deb over a live app.

Install ordering was reversed. The page said "run it, then quit and reopen
Orca"; the ref this is gated to land with says the opposite in four places
(linux-package-downloaded-status.ts LINUX_PACKAGE_MANUAL_INSTALL_MESSAGE,
"Quit Orca before running the system package install command", plus the
recovery card's title, summary and explainer). That wording came from main's
older run-then-quit card, which the stack deliberately reversed when it
retitled the card to "Manual Install Required". Now: quit first.

CLI verification put the Linux caveat *below* `command -v orca`. That check
succeeds on any GNOME desktop and resolves to the screen reader, so the reader
got a confident hit from the page's own verification step and then invoked the
wrong program. Caveat moved above, and the block now spells `orca-ide`
literally instead of asking the reader to substitute.

The serve bootstrap was circular: the bare-`orca` dispatcher is written *during*
serve startup (main-process-runtime-launch.ts), so it can never be the command
that starts serve. First launch is `orca-ide serve`. Fixed here and in the two
pages this links to.

Accuracy: the install command now matches what the code emits -- absolute paths
resolved from the trusted directories and a POSIX-single-quoted package path,
as pinned by linux-package-install-command.test.ts -- and names the manager
fallbacks (dpkg; zypper/dnf/yum/rpm) rather than presenting apt as the only
form. The pending path honours XDG_CACHE_HOME. rpm arch tokens are x86_64 and
aarch64, not deb's amd64/arm64. arm64 AppImage is linked. Dropped the container
example: isExternallyManagedLinuxInstall() needs a root marker AND no trusted
package manager, and a Debian-based container has apt, so it is not flagged.
2026-09-02 03:49:40 -07:00
Neil f37d2fec97 fix(linux): land the reviewed Linux packaging stack on main (#18100)
* fix(linux): give the CLI one entrypoint by extracting the AppImage once

* refactor(linux): trim AppImage CLI registration seams

* test(cli): assert registration lock serialization

* fix(linux): fence AppImage terminal shim mounts

* fix(linux): accept extracted AppImage runtimes with APPDIR only

* docs(linux): make headless AppImage extraction runnable

* refactor(linux): import bundled launcher directly

* fix(linux): reclaim superseded AppImage payloads and packaged symlinks

Pruning removed 3215 of 3216 files from a superseded generation and always
stranded resources/app.asar, leaking ~105 MB per version update. Electron's
asar shim reports a *.asar file as a directory, so the recursive remove tried
to rmdir a real file and failed with ENOTEMPTY; the .catch(() => {}) hid it.
Reproduced end to end on Ubuntu 24.04: 519M -> 623M across one update, and
519M again once the payload is actually reclaimed.

removeExtractedAppImagePayload holds process.noAsar for the removal, counted
so overlapping removals cannot hand the shim back early, and the prune site
now warns with the path instead of swallowing the rejection. All three
removal sites use it -- staging cleanup and displaced roots leaked the same
way.

Also reclaim symlinks left by a packaged deb/rpm install, which the
extracted-cache-only rule turned into a hard conflict on a deb -> AppImage
migration, and name the remedy in the conflict error.

* fix(linux): bound the CLI registration lock wait

`retries: 1000` caps the attempt count, not elapsed time, so at up to 1s per
attempt an IPC-driven registration could hang ~16 minutes against a wedged
holder with no feedback.

A legitimate holder is bounded by the extraction timeout, so wait that plus
slack and then fail with a message naming the lock file, rather than hanging.
`maxRetryTime` is forwarded verbatim to the `retry` package by proper-lockfile.

* fix(linux): stop re-extracting the AppImage on inode metadata churn

The extracted-payload cache key hashed ctime alongside dev/ino/size/mtime.
ctime moves on any inode metadata write -- `chmod +x`, which every AppImage
user is told to run, plus `chown`, an ACL or SELinux relabel, and a backup
restore -- none of which alter a byte of the payload.

Measured on Ubuntu 24.04: `chmod +x` leaves dev, ino, size and mtime
identical and moves ctime alone, so the key changed and the next launch paid
a full ~519 MB re-extraction and a multi-second stall to rebuild a payload it
already had, then pruned the old generation.

Key on content identity instead. An in-place content change moves mtime and
almost always size; a replacement moves the inode. The existing
replace-in-place test still passes.

* fix(linux): stop CLI commands from falling through to Chromium startup

* refactor(cli): remove redundant command membership check

* test(cli): cover command-named project selectors

* fix(cli): redirect the open-url command before startup

* test(linux): cover AUR serve wrapper flags

* fix(linux): tighten CLI launch detection

* fix(linux): respect CLI flag value boundaries

* fix(linux): strip injected Chromium switches from CLI args

* fix(linux): report a missing display instead of dying in uv_close

* refactor(linux): read display locks without a preflight race

* fix(linux): preserve unverified external displays

* chore: format reliability gate manifest

* test(packaging): split runtime resource checks

* fix(linux): fail serve when no display is available

* fix(linux): do not treat a lockless X socket as a dead display

An X server writes its lock beside its socket and both survive a crash
(verified against Xvfb under SIGKILL), so a socket with no lock was never
left by a crashed server. It is an endpoint published from elsewhere: a
container bind-mounting only /tmp/.X11-unix, WSLg, or a foreign PID
namespace. Declaring those dead made the desktop gate exit(1) on displays
that work, with no workaround, and the serve gate refuse to start.

Liveness now splits by ownership. A foreign DISPLAY trusts a lockless
socket; Orca's own :99 does not, because removeStaleDisplayArtifacts
unlinks the lock before the socket and so manufactures that state itself --
adopting it would resurrect the orphan-socket bug and stop the cleanup from
self-healing. The stale-lock rejection is unchanged.

Also correct four doc statements this behaviour falsified.

* fix(linux): fail closed when a stale socket blocks the Xvfb rebind

Readiness only checked that /tmp/.X11-unix/X99 exists. A stale socket we
could not unlink still exists after our own Xvfb refused to bind, so Orca set
DISPLAY to a dead server and Chromium died in Ozone init.

Measured on Ubuntu 24.04 against the pre-fix build: with a leftover :99
socket and no lock, serve exits 139 (SIGSEGV), the socket inode is unchanged
before and after, and no lock is recreated -- it neither cleaned up nor
respawned. To a user that is a crash, not a misconfiguration.

This is reachable in the documented topology, where orca-xvfb.service has no
User= and runs as root while serve runs as User=orca: /tmp is sticky, so the
orca uid cannot unlink a root-owned socket, rmSync fails, and Xvfb exits with
the display already active.

Readiness now requires the display to actually be live -- our socket plus a
lock naming a running process -- so the same state reports an unusable
display and exits 1 with the existing diagnosis.

* fix(linux): recognise abstract X sockets and inherited Wayland fds

Two display setups this gate could not prove were refused outright, and on the
desktop path that is app.exit(1) with no workaround.

An X server may bind only the abstract namespace (`@/tmp/.X11-unix/X0`), which
leaves no filesystem socket to stat. Abstract addresses are kernel-owned and
vanish the moment the owner exits, so an entry in /proc/net/unix is proof of a
live server -- no lock file needed and no stale entry possible. Verified on
Ubuntu 24.04, where 139 such addresses were present.

WAYLAND_SOCKET is an already-connected fd handed over by the compositor, so
there is no path to stat and WAYLAND_DISPLAY may be unset entirely. Its
presence is the display.

Both are consulted only after the filesystem-socket check fails, so no
existing verdict changes.

* fix(linux): never treat Orca's own display number as a foreign endpoint

Recognising a lockless X socket as live is correct for an endpoint published
from elsewhere -- a container bind mount, WSLg -- because an X server writes
its lock beside its socket and both survive a crash. It is wrong for
VIRTUAL_DISPLAY_NUMBER, because Orca's own teardown unlinks the lock before
the socket and so manufactures that exact state.

The managed branch was already strict, but a caller that sets DISPLAY=:99
explicitly takes the foreign path and skipped it, accepting a dead display
left by Orca's own interrupted cleanup. Route the managed number through the
strict probe on both paths.

Found by an adversarial audit of the asymmetry introduced earlier in this
branch; the documented systemd topology is unaffected because its Xvfb writes
a real lock.

* test(linux): add a packaged-artifact contract for the CLI launch paths

* test(linux): avoid buffered serve readiness detection

* test(linux): signal AppImage serve owner directly

* test(linux): tolerate readiness timeout boundary

* test(linux): add startup margin to shutdown oracle

* ci(linux): give package contracts timeout headroom

* fix(ci): route all Linux packaging contract changes

* test(linux): poll shutdown readiness without tail leaks

* test(linux): bound shutdown cleanup grace

* test(linux): assert on CLI output, not the harness's own control lines

run-cli-case.sh echoes `RESULT status=N case=<name>`, and the two cases named
*-skills asserted `expectOutput: 'skills'`. That substring was satisfied by
the case name in the harness's own line, so 2 of 8 cases asserted nothing
about the command -- gutting `skills` entirely would still have gone green.

Control lines are now excluded before matching, and both cases assert the
rendered help header, which only real help output produces. Verified on an
Ubuntu 24.04 host: 8/8 still pass against a stack-tip AppImage.

Also register the gate in reliability-gates.jsonc, which #15085 added a CI
Docker gate without. Red/green is recorded from a stock release AppImage
failing 4 of 8, three of them at status 133 (SIGTRAP).

* fix(linux): require static AppImage runtimes (#17319)

* test(linux): reject a wrong-architecture native binary at packaging time

Cross-building the arm64 slice on an x64 host silently packed an x86-64
`pty.node` -- the rebuild logged "Forcing native rebuild for linux-arm64" and
shipped the host's binary anyway. Every gate here inspects symbol versions,
which are perfectly valid on the wrong architecture, so nothing noticed.

Observed on a Raspberry Pi 5: the packaged app loaded, then failed with
"Failed to load native module: pty.node", and the launch contract reported
3 of 8 cases crashed rather than naming the cause. Swapping in the aarch64
`pty.node` took the same build to 8/8.

Compare ELF `e_machine` against the slice being packaged and fail with the
offending path. Checked before the glibc pass, because a wrong-architecture
binary's symbol versions are valid but meaningless and would send the reader
down the wrong path.

Release CI builds arm64 on a native runner, so this guards local and future
cross-builds rather than a shipped artifact.

* test(linux): judge per-arch vendored binaries against their own path

The first CI run of the architecture gate failed the x64 package job on
`@parcel/watcher-linux-arm64-glibc/watcher.node`. That binary is arm64 on
purpose: the package ships every architecture and its loader picks the match,
so its presence in an x64 build is correct.

Judge a binary against the architecture its own path names, falling back to
the slice when the path names none. That keeps the case this gate exists for
-- `bin/linux-arm64-*/node-pty.node` holding an x86-64 binary, which is what
shipped to a Raspberry Pi 5 -- while letting multi-arch dependencies through.

Dry-run over the real dependency tree flags nothing for either target arch.

* fix(linux): move deb/rpm update installation outside Orca (#17318)

* fix(linux): complete deb/rpm package metadata

* fix(linux): preserve CLI link during package upgrades

* docs(linux): document local RPM build prerequisites

* fix(linux): move deb/rpm update installation outside Orca

* fix(updater): preserve Linux recovery across stale events

* fix(updater): fence stale downloaded events by active target

* fix(updater): preserve active Linux package recovery

* test(linux): keep workflow order assertion in scope

* test(updater): assert stale recovery stays silent

* fix(updater): preserve Linux package recovery after checks

* refactor(updater): keep Linux marker message with status

* fix(linux): describe the right manual update path for deb/rpm hosts

A remote host installed from .deb or .rpm now reports
manual-service-update-required, and the guidance told the operator to
"update through the service manager that starts this server" -- which is
correct for unsupported-headless-serve but wrong for a package install,
where nothing about the remedy involves the service manager.

Say both, keyed on how the host was installed.

* docs(linux): document orcad update restart safety

* docs(linux): scope restart census omissions

* docs(linux): use absolute service CLI launcher

* fix(serve): validate in-process serve options before startup (#17683)

* fix(linux): stop offering updates a distro-managed install cannot apply (#17918)

Closes #17702.

The resources/package-type marker is authoritative but never checked against
the host, so any repackager that unpacks Orca's .deb -- AUR, Nix, a container
rebuild -- inherits `deb` verbatim. Install feasibility was then computed
after a ~165 MB download, so those users got check -> download -> a card
promising an install command -> a dead end.

Validate the marker against the host: a deb/rpm marker with no matching
package manager in the trusted directories means a package manager owns this
install. This reuses the exact lists and resolver that
buildLinuxPackageInstallCommand already loops over, so a false positive is
impossible by construction -- any host flagged here would have failed with
no-package-manager after the download anyway. The gate only moves that
verdict earlier. Verified across Debian 12, Ubuntu 24.04, Arch, Fedora 40 and
openSUSE Leap: no false positive on a real deb host, correct on every
repackaging host.

The release is still reported, because the user does want to know 1.4.194
exists and to update through their distro; only the download path is closed.
`externallyManaged` is an additive optional field on the existing `available`
status, so older paired clients decode it unchanged. downloadUpdate() refuses
authoritatively, since main owns this verdict rather than the card, and
unwinds any pinned-build state first -- a Linux pinned jump resolves to
'release', and stranding isPinnedBuildActive would silently kill every
background check for the rest of the process.

Note the fix the issue suggests cannot work: electron-updater builds a
PacmanUpdater whose doDownloadUpdate looks for a .pacman asset Orca does not
publish, then dereferences undefined.

* style(cli): restore prettier wrapping on install error copy

* test(linux): re-pin the child-process ratchets and the batch-shim allowlist after the merge
2026-09-02 03:08:01 -07:00
Neil aa3ae6f56e fix(ssh): close the pty master fd leak on relay hosts too (#17920)
* fix(ssh): close the pty master fd leak on Linux relay hosts

The app gets the FD_CLOEXEC patch through pnpm patchedDependencies (#17914);
the relay installs stock node-pty from npm, where no pnpm patch reaches. Linux
is where that matters -- it is the only relay platform that takes forkpty()'s
no-atomic-O_CLOEXEC path, and it is also the only one that already compiles
node-pty at install time, so the fix costs a second compile rather than a first.

Ships the patch as a relay asset applied like the existing Windows console-list
one, and rebuilds only after the probe has proven node-pty loadable. The rebuild
is non-fatal by construction: the working build is moved aside first and moved
back on any failure, a failed attempt drops a skip marker so the compile is
attempted at most once per relay directory, and the caller swallows the whole
step. macOS and Windows relays never run it.

Measured on node:22 with a relay-style npm install: before, the master is
cloexec=false and shows up as `26 -> /dev/pts/ptmx` in both a later pty child
and a later child_process child; after, cloexec=true and neither child sees it.

Closes #17915.

* test(ssh): feed the cloexec patch exec to the hand-rolled namespace fixtures

These sequences are positional, so the new Linux-only patch exec swallowed the
READY slot and every install/repair case timed out waiting for the relay.

* fix(ssh): patch the pty master before publishing the shared native-deps tree

* fix(ssh): refuse to publish a native-deps tree whose cloexec patch did not take
2026-09-02 03:02:27 -07:00
Neil 62e9949141 perf(renderer): index worktree owner lookups instead of rescanning every workspace (#18130)
`worktreeUsesRemoteConnection`, `getRemoteConnectionIdForWorktree`,
`worktreeUsesWslPath` and `rightSidebarShowsPullRequestData` each did
`Object.values(state.worktreesByRepo).flat().find(...)` plus a linear
`repos.find(...)`. They are called from unmemoized Zustand selectors
(`use-tab-agent.ts:263`, `use-visible-review-refresh.ts:45`), so every store
write re-ran the whole scan once per open tab.

Measured on a real instance (10 repos / 423 worktrees / 382 tabs): the `.find()`
predicate alone ran 1,320,424 times in 30s — 44,000 worktree visits/sec — while
the app was idle.

Switched to the existing WeakMap-cached `getIndexedWorktreeMap` /
`getIndexedRepoMap` from `store/worktree-repo-index.ts`, matching what
`connection-owner-resolution.ts` already does. Same duplicate-id and
host-collision semantics; no behavior change.

Benchmark at that scale, 200 store writes x 382 tabs x 3 lookups:
  before 2.762ms per store write
  after  0.167ms per store write   (16.6x)
At ~20 store writes/sec that is 55.2ms/sec of renderer CPU down to 3.3ms/sec.

The new scale test counts worktree `id` reads: 160,000 before, 800 after.
2026-09-02 03:01:07 -07:00
Neil 0da52453a7 fix(settings): surface why CLI registration failed (#18125)
The Settings CLI panel treated every resolved `cli:install` as a success,
so a refusal that arrives as data (conflict, missing launcher, unreadable
Windows PATH) produced a green "Registered `orca` in PATH." toast while the
switch stayed off. A thrown refusal fared little better: the raw Electron
`Error invoking remote method 'cli:install': ...` string went into a toast
that then disappeared, leaving the panel indistinguishable from "not yet
installed".

Inspect the returned status with the predicate the onboarding and
agent-skill flows already use (`state !== 'installed'`), unwrap the IPC
transport prefix off thrown installer messages, and persist the existing
main-process reason inline per STYLEGUIDE (toasts disappear; errors the
user must act on stay inline). No new error taxonomy — the reasons already
carry path and remedy; a conflict status, which names the path but not the
remedy, gets the installer's own remedy sentence.

Closes #3952
2026-09-02 02:55:38 -07:00
Neil 34999e328e fix(orcad): stop demanding a spawn-helper only macOS builds (#18122)
node-pty declares the spawn-helper target inside binding.gyp's OS=="mac"
block and pty.cc execs it only under __APPLE__. Asserting it on
`!== 'win32'` made every Linux orcad boot degraded with
spawn_helper_missing while its terminals worked fine.

Route all four sites through one shared `usesNodePtySpawnHelper`
predicate: the precondition verdict, the prebuilt slot install, the
+x repair, and the prebuilds build script (which threw outright on a
Linux slot build).

Fixes #17844
2026-09-02 02:49:09 -07:00
Neil 8197268956 fix(pty,remote): close the pty master fd leak, and two remote-terminal defects (#17914)
* fix(pty,remote): close the pty master fd leak and two remote-terminal defects

so on Linux every later child of the process -- both later pty children and
plain child_process spawns -- inherits it and keeps the /dev/pts device alive.
Measured on Linux with stock node-pty 1.1.0: master fd flags 0404002
(cloexec=false), and 17 -> /dev/pts/ptmx present in both a later pty child's
/proc/self/fd and a later child_process child's. Extend the existing node-pty
patch with pty_cloexec() on both PtyFork spawn paths; after the patch the flags
read 02404002 (cloexec=true) and neither child sees the master. This covers the
app and terminal daemon only -- the SSH relay installs node-pty from npm on the
remote host, so it stays exposed (see the report).

rejecting inspection as a renderer-global unhandledrejection, which an
unreachable runtime produced on every cadence tick.

path cleared the close intent for it exactly like a dropped connection, so a
host that keeps republishing the dead surface re-materialized the pane the user
just closed. Keep that intent and drop its TTL. Also route the banner's
"Remote terminal was closed." line through translate() so it stops mixing
English into a localized banner.

* test(pty,remote): make the fd-leak evidence positive and size the close intent to its RPC

The Linux 'does not hand an earlier pty master to a later pty child' case only asserted that ptmx was absent from the captured listing, so any run that produced no listing passed without inspecting a single fd. Block the child on stdin, emit a sentinel, and assert both the sentinel and a real /dev/pts fd row before the negative assertion. Verified in node:24-bookworm: passes with the patch, and with pty_cloexec() reverted it fails on four inherited /dev/pts/ptmx rows.

The close intent's TTL was a 10s literal while the close RPC that can still answer tab_not_found had its own 15s literal. A host that answered slowly while republishing the surface had its intent evicted by the republish path's own pending-check, so makeWebSessionCloseIntentDurable found nothing to flip and #9194 reproduced. Derive the TTL from the shared session.tabs RPC timeout so the two cannot cross, with an invariant test and a regression test for the slow answer.
2026-09-02 02:23:45 -07:00
Neil 4bc20cb842 fix(wsl): name an explicit Windows cwd for wsl.exe spawns (#17834)
* fix(wsl): name an explicit Windows cwd for wsl.exe spawns

Removing the worktree Orca was launched from broke every wsl.exe spawn for
the rest of the session. The WSL command builders passed `cwd: undefined`
meaning "the directory is inside the command" -- but CreateProcessW reads
NULL as "inherit the parent's", and the parent's was a \\wsl.localhost path
Linux had just deleted.

Fixes #16463

* fix(wsl): name the spawn directory at the six remaining wsl.exe sites

The first commit fixed the WSL command builders. Six spawn sites were left
inheriting the process cwd, which is the same deletable `\\wsl.localhost`
worktree: `wsl-availability` (both probes), the WSL filesystem watcher, the
agent-hook relay launch, the UNC delete, and the local worktree filesystem.

`wsl-availability` is the one that matters most, and it turns the bug into a
latching false negative. `isRetryableWslProbeFailure` returns false for ENOENT,
so a spawn that failed only because the inherited cwd was gone is cached as
"WSL is not installed" on the 10-minute definitive TTL with exponential
backoff up to 30 minutes. Git keeps working and Orca reports WSL unavailable --
worse than the bug being fixed.

ENOENT stays non-retryable. It is answer-shaped for the reason it is meant to
be -- wsl.exe is not on PATH -- and naming the directory is what removes the
one cause that was not. Making it retryable would instead re-probe every
non-WSL Windows machine on the short window, and would leave the false ENOENT
in place for the other five sites, which have no cache to correct.

Three of these are also on the `runWslProcess` W3 migration allowlist; this is
the interim until they move, and matches what #17837 does inside the runner.
2026-09-02 01:39:48 -07:00
5dc1195a47 fix(native-chat): keep disabled CLI models out of the Claude picker (#18055)
* fix(native-chat): keep disabled CLI models out of the Claude picker

The Claude CLI advertises models it cannot run yet as disabled placeholder
rows. On 2.1.237 `list_models` returns a sixth row alongside the four real
models:

  {"value":"cc-update-required-1","displayName":"Fable 5.1 (disabled)",
   "description":"Update to 2.1.255+ to use Fable 5.1","disabled":true}

`toListedModel` never read `disabled`, and for Claude the discovered list
replaces the seed catalog verbatim, so the picker rendered that row as a
selectable model and `/model cc-update-required-1` went to the CLI. It was
also adoptable as a launch default, putting the sentinel behind `--model`
on spawn. Drop disabled rows at the parse choke point, which both the
native-chat picker and commit-message model discovery share.

The two adjacent fixes are the same version-pinning bug the placeholder
announces. `compactTerminalText` strips only whitespace, so a point release
keeps its dot and the pinned consent literals (`fable5uses…`,
`switchtofable5?`) stop matching a "Fable 5.1" prompt — the switch would
degrade to `unknown` instead of `interaction-required`. Likewise the scoped
weekly usage window matched `display_name === 'fable'` exactly, so it would
disappear once the scope is named "Fable 5.1".

Claude-Session: https://claude.ai/code/session_01SJy4XGrdre6YaU1wYNKak4

* fix(native-chat): make the Fable consent match version-optional

Probing a 2.1.258 CLI shows the shipped Fable 5.1 row carries displayName
"Fable" with the version only in the description:

  {"value":"claude-fable-5-1[1m]","resolvedModel":"claude-fable-5-1",
   "displayName":"Fable","description":"Fable 5.1 · Most capable for …"}

So the consent prompt may name the model with no digits at all. Requiring
a version would have missed that, the same way the old pinned literal
missed "Fable 5.1". Accept both.

Claude-Session: https://claude.ai/code/session_01SJy4XGrdre6YaU1wYNKak4

---------

Co-authored-by: Merge Sim <sim@local>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-09-02 01:34:58 -07:00
Neil 7eb13c184c fix(ssh): keep remote PowerShell commands inside what sshd's cmd.exe accepts (#17947)
* fix(ssh): keep remote Windows commands inside cmd.exe's command-line limit

Windows OpenSSH runs every exec request through sshd's DefaultShell, which is
cmd.exe on a stock install, and cmd.exe refuses a line over 8191 characters with
exit 1 and a localized "The command line is too long". `-EncodedCommand` spends
2.67 characters per script character, so five commands on the first-connect path
were already over: the stale upload-stage recovery that opens a fresh install
(23,210), promote (20,646), cleanup (19,798), the install-lock steal (11,798)
and reserve (9,434). A Windows-to-Windows `ssh:connect` died on the first of
them before the relay was ever uploaded (#16126).

powerShellCommand now falls back to a gzip self-extracting bootstrap once the
inline form passes the budget - these scripts are repetitive enough that the
worst one lands at 6.5KB - and throws a message naming the limit if even that
cannot fit, rather than letting cmd.exe answer in the host's locale. Commands
that already fit are byte-identical.

The real-binary PowerShell suite in ssh-relay-upload-stage-commands.test.ts
exercises the bootstrap end to end, including `exit` and here-string semantics
through Invoke-Expression.

* fix(ssh): cite the real command-line budget and reuse the cmd.exe ceiling
2026-09-02 01:29:30 -07:00