Commit Graph
8702 Commits
Author SHA1 Message Date
Brennan BensonandKaylee Williams 7558fb064a fix(browser): keep browser guests painting when the workbench is hidden (#14599)
* fix(browser): keep browser guests painting when the workbench is hidden

Chromium never paints inside a display:none subtree, so an Electron <webview>
stops emitting CDP screencast frames the moment any ancestor is parked that way.
Orca already models this per pane (browser-page-paintability.ts) and per worktree
surface, using opacity:0 so a phone- or agent-driven page keeps compositing — but
three ancestors above those layers still used `hidden` unconditionally:

  - the App-level terminal workbench container, hidden whenever activeView is not
    'terminal' (opening Settings froze every mobile browser pane),
  - Terminal's root, hidden when there is no active worktree,
  - the split-surface wrapper, hidden when the active worktree has no layout.

A pane-level escape hatch cannot override an ancestor, so all of them have to
agree. Share one predicate across the chain and swap `hidden` for an out-of-flow
transparent layer while a remote controller needs frames.

The predicate ORs automation visibility with the mobile driver, matching the
per-worktree gate. That term is load-bearing, not symmetry: agent-browser
commands acquire a visibility lease and then capture, so gating on the mobile
driver alone left automation from a non-workspace view capturing a blank surface.

Mobile: a stream can report `ready` and then deliver no frames, which cleared the
loading indicator and left an unexplained black rectangle. Key it off actually
having pixels. That also retires the `ready` state and its ref.

Co-authored-by: Kaylee Williams <65376239+KayleeWilliams@users.noreply.github.com>

* fix(browser): keep paint retention off store hot paths

---------

Co-authored-by: Kaylee Williams <65376239+KayleeWilliams@users.noreply.github.com>
2026-08-14 19:13:12 -07:00
73bbe20ca7 fix(i18n): correct Chinese translations (#14299)
* fix(i18n): correct Chinese translations

* fix(i18n): keep startsIn a location label, not a countdown

'Starts in:' renders immediately before request.initialCwd, a directory
path, so the string labels a LOCATION. The new value read as a time delay.
Peer locales agree: ja 開始場所, ko 시작 위치, es 'Se inicia en:'.

* fix(i18n): keep zh terminology aligned with the rest of the locale

- thinking: revert 思考中 -> 思考; it names the Thinking session option
  (alongside Model/Effort/Fast mode) and feeds 切换{{value0}}, so a
  progressive-aspect status reads as ungrammatical there.
- review pills/filters/fixture: revert 审查 -> 评审. English "Draft review"
  was rendering as 草稿审查 on the dashboard card but 草稿评审 in the PR-checks
  row summary; zh.json uses 评审 for the review artifact ~70x vs ~6x 审查.
- notAgent.subtitle: revert 编程智能体 -> 编码智能体, matching 16 other
  "coding agent" keys.

* fix(i18n): translate drop folder prompt correctly

* fix translation

---------

Co-authored-by: DaQun <1.404848e+07+DaQun@users.noreply.github.com>
Co-authored-by: Brennan Benson <brennan@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-14 18:58:37 -07:00
fsdwenandBrennan Benson 190d153194 fix: detect external git init on folder projects and upgrade to git repo (#11480)
* Remove scheduled triggers from E2E and README badge workflows

* fix: detect external git init on folder projects and upgrade to git repo

Closes #11477

Three root causes fixed:
1. buildWorktreeBaseDirectoryWatchTargets continued for folder repos
   - now register parent dir as base watch target so poller sees .git creation
2. No path re-evaluated kind after registration
   - add tryUpgradeFolderRepo, checks .git on structural change, calls
   store.updateRepo(repoId, { kind: 'git' })
3. No IPC signal after store update
   - emit repos:changed so frontend git polling re-evaluates

* revert: drop the base-watch-target approach to folder-project git detection

Registering dirname(repo.path) as a base watch target makes the existing poller readdir
the parent directory and stat every sibling, so a project under the home directory scans
the whole home directory on every poll. Replaced by a per-repo .git poll in the following
commits.

* fix(folder-projects): upgrade to a git repo when an external git init lands

Folder projects were registered once as kind: folder and never re-evaluated, so
running `git init` outside Orca left them without any git affordances until a
restart (#11477).

Poll `<repo>/.git` for each local folder project on the base-watcher cadence and
flip kind to git when the marker appears, matching a freshly added git project
(explicit externalWorktreeVisibility, prepared worktree root) before notifying the
renderer and resyncing the base watchers.

One stat per folder project per tick, parked while the window is hidden, backed off
to 30s while no folder project exists.

* fix(folder-projects): reuse the shared repo-change notifier and stop reading the store at attach

Perf audit follow-ups: reading getRepos() synchronously in attachMainWindowServices
broke every test in that file and put O(repos) hydration on the startup path, and a
bare repos:changed send skipped the paired-client broadcast (#11994). Also invalidate
the authorized-roots cache the way the runtime's own folder->git path does.

* fix(folder-projects): keep the project's workspace visible when git's root differs from the stored path

Electron QA found the golden path breaking for a folder project whose path traverses
a symlink: Add Project stores git roots as rev-parse reports them, folder projects
keep the raw path, so after the upgrade the root checkout reads as an *external*
worktree and externalWorktreeVisibility: 'hide' hid the project's only workspace.
Only set 'hide' when git's toplevel matches the stored path.

Also gate the upgrade on isGitRepo so a stray .git file cannot flip a project, and
switch the tests to real git init so both guards are exercised against real git.

* fix(folder-projects): refuse non-root folders and stop re-probing git for a rejected marker

Review round found three real defects:
- A folder project inside another repo's work tree upgraded with repo.path pointing at a
  non-root subdirectory, because git accepts any path inside a work tree. Refuse unless
  git's toplevel resolves to the project directory itself.
- A .git git keeps rejecting re-ran two synchronous git spawns every 2s forever. Cache the
  verdict against the marker's stat signature and re-probe only when the marker changes.
- The poll kept probing after the window was destroyed (macOS keeps the app alive with no
  window), so idle out there instead.

Tests: build the symlink explicitly instead of relying on macOS TMPDIR being one, so the
spelling-mismatch case runs on Linux and Windows CI too; count real git probes; assert
per-project stat counts instead of a modulus; make the idle-backoff test observe the
interval it names.

* fix(folder-projects): refuse the upgrade when it would destroy the project's workspaces

Reproduced in the app: a folder project with extra workspaces went from three sidebar
rows to one within ~2s of an external git init, and their lineage was pruned.

A folder project's extra workspaces are worktreeMeta rows keyed
repoId::path::workspace:<uuid>, and only the folder branch of the worktree listing knows
those keys. Flipping kind moves the repo onto the git branch, which lists git worktree
list (one path) and prunes every lineage id under the repo that is not in it.

Migrating that meta belongs to the listing code that owns both shapes, not to this watch,
so refuse the upgrade for those projects. They keep working exactly as they do today.

* test(folder-projects): wait for the stat count instead of a fixed number of ticks

A tick that spawns git can outrun a fixed wall-clock wait on a loaded machine, so the
rejected-marker test failed roughly one run in six. Poll for the stat count with a
deadline; the load-bearing assertion (git probed exactly once) is unchanged.

* fix(folder-projects): wake git upgrade checks on catalog changes

* docs(folder-projects): align upgrade polling rationale

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-14 18:47:14 -07:00
Jinjing 8b31910ac1 Split worktrees slice (#14643)
* refactor: split worktrees.ts under 400 lines

Move the worktree slice into dest modules under store/slices/worktrees/ and leave a thin public barrel so existing imports keep working.

* refactor: nest worktrees dest modules by domain

Group the extracted slice files into catalog, refresh, create, remove, and related domain folders instead of a flat dest dump.

* refactor: group worktrees dest modules by lifecycle

Collapse the 12 noun folders into listing, create, teardown, metadata, and session so dest layout matches the slice methods.

* rm duplicated mport
2026-08-14 18:44:44 -07:00
Jinwoo HongandJinwoo-H 1d8eaa81c7 fix(orchestration): preserve mailbox delivery identity (#13717)
* test(orchestration): reproduce mailbox pointer mismatch

* fix(orchestration): align pointers with actionable mailboxes

* fix(orchestration): harden pointer reservation lifecycle

* fix(orchestration): bound mailbox reconciliation

* fix(orchestration): make pointer staging restart-safe

* test(orchestration): expect scoped dispatch index

* fix(orchestration): guard skewed inbox indexes

* refactor(orchestration): extract mailbox notification lifecycle

* fix(orchestration): settle mailbox pointer writes

* fix(orchestration): bound mailbox recovery work

* fix(orchestration): fence inactive mailbox snapshots

* test(orchestration): cover mailbox notification boundary

* test(orchestration): pin STA-4325 delivery identity

* fix(orchestration): preserve mailbox delivery identity

* fix(orchestration): harden mailbox settlement

* test(orchestration): make mailbox gates self-contained

* fix(orchestration): preserve paged mailbox ownership

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-14 18:36:51 -07:00
Brennan Benson 78d5920446 fix(orchestration-cli): point dropped mutations at --retry-request (#14586)
* fix(orchestration-cli): guide dropped mutations to idempotent retry

* test(orchestration-cli): preserve read-only drop message

* fix(orchestration): harden mutation replay identity

* fix(orchestration): preserve replay across remints

* fix(orchestration): defer local mutation identity
2026-08-14 18:11:12 -07:00
9cb04b5a35 fix(orchestration): stop fencing fresh-run callers and accept revoked coordinator after takeover (#11582)
* fix(orchestration): stop fencing fresh-run callers and accept revoked coordinator after takeover

LegacyCoordinatorAuthority.resolve was forcing the adopted legacy run for
every orchestration preflight and throwing legacy_read_only at any caller
that could not prove legacy-coordinator identity — including fresh-run
coordinators with no connection to the legacy system. Now only callers
previously known to the legacy run are fenced; unknown fresh-run callers
fall through to the normal current-run handler.

isLegacyCoordinatorHandle returned only the committed principal's handle,
so after a takeover that revoked the principal, worker_done/escalation to
the new coordinator was rejected with "not a retained coordinator". Now
both the retained legacy handle and the current run binding's handle are
accepted, so workers can deliver lifecycle mail to either coordinator.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(orchestration): deliver legacy lifecycle mail to the replacement coordinator

After `run-use --takeover-legacy` revokes the old coordinator principal, a
retained legacy worker addressing worker_done/escalation/ask at the new
coordinator was rejected with "not a retained coordinator", so the dispatch
stayed open forever.

Add a recipient-side permit, isLegacyCoordinatorDeliveryTarget, that also
accepts the current Run binding's coordinator handle. Both handles already
route to run:<id> via resolveLegacyWorkerCoordinatorDelivery.

isLegacyCoordinatorHandle stays narrow: #11745 reused it as the caller-side
fence jurisdiction, where widening it fences MORE callers and replaces an
actionable run_required with dead-end legacy_read_only guidance.

Co-Authored-By: Leonardo <leonardo.marciano@toolzz.me>

* fix(orchestration): keep the delivery permit in step with the takeover router

Round 1 review fixes on top of the recipient-side permit.

isLegacyCoordinatorDeliveryTarget accepted any handle bound as the Run's
coordinator, but resolveLegacyWorkerCoordinatorDelivery only promotes to
run:<id> once the legacy principal is no longer committed. bindRun leaves a
committed principal alone when it rebinds without a takeover over live legacy
work, so a coordinator restarting inside the legacy pane produced a permitted
send that routed legacy_direct to a handle no reader can see: current-contract
inboxes require current_delivery, and legacy mail requires a principal on that
handle. Gate the binding branch on the same takeover test the router uses, so
that send goes back to request_mismatch instead of vanishing.

Tests: cover the ask call site (it had none — reverting question.ts alone
failed nothing), assert the takeover bind landed inside the helper rather than
two assertions downstream, and replace the not-legacy_read_only assertion on
the fence guard with the concrete outcome it means to protect.

Drop the fresh-run coordinator tests: they guard #11745/#11802, already on
main and already covered by orchestration-legacy-fence-jurisdiction and
orchestration-11745-regression-verification, and they pass with this fix
reverted. Rename the file to what it now contains.

* refactor(orchestration): drop the unreachable pane-key clause from the delivery permit

The permit's takeover branch must mirror resolveLegacyWorkerCoordinatorDelivery,
which tests only the principal status. Every runs-table write sets
coordinator_handle and coordinator_pane_key together, so the extra pane-key
term never fires — and if it ever did it would deny mail the router would have
promoted to the readable run mailbox.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-14 18:03:07 -07:00
Brennan Benson 83d79f3846 fix(workspace-cleanup): make the Filters panel scrollable (#14629)
* fix(workspace-cleanup): make the filter panel scrollable

The height cap sat on the ScrollArea Root, but the Radix viewport inside is
h-full — under an indefinite root height that collapses to auto, so the
viewport grew to full content height with nothing to scroll while the root
clipped at 420px. The last facet groups (Archived, and the tail of Workspace
status) were unreachable behind the footer.

Move the cap to the viewport, which is what the ScrollArea wrapper's
viewportClassName exists for. Adds a regression test that pins the cap to the
viewport; it fails against the previous markup.

* fix(workspace-cleanup): keep filter footer visible
2026-08-14 17:59:25 -07:00
Neil a3b472d050 refactor(renderer): decompose App.tsx into an app-shell module (#14607)
* refactor(renderer): decompose App.tsx into an app-shell module

App.tsx was 2831 lines behind an `eslint-disable max-lines` and a
grandfathered entry in the max-lines ratchet baseline. It is now 92 lines:
a root element, the shared providers, and three children.

The body is split by concern into src/renderer/src/app-shell/:

- use-app-chrome-layout — titlebar/sidebar/workbench layout derivations
- use-floating-workspace-panel — overlay open state, persistence, return focus
- use-app-startup-hydration — the boot chain (order preserved verbatim)
- use-app-session-persistence — session writer + shutdown checkpoint
- use-persisted-ui-writer / use-document-appearance /
  use-runtime-graph-sync / use-window-visibility-effects
- use-onboarding-and-feature-tips — first-run education gating
- use-global-keybindings + app-command-handlers — window shortcut dispatch
- use-app-shell-services — app-level subscriptions that outlive any surface
- AppWorkspaceShell / AppRootSurfaces / AppBackgroundServices / titlebar parts

Two startup branches that no longer needed to sit inline moved to
src/renderer/src/startup/: startup-ssh-connection-restore and
startup-degraded-recovery.

No behavior change. Sibling order of root overlays and modals is preserved
so stacking is unchanged; the sidebar's virtualized scroll refs still live
above the sidebar's remount boundary.

The source-assertion tests in app-startup-routing.test.ts follow the code to
its new files. The one that claimed to check "first-window startup services
before terminal reconnect" was matching the degraded-recovery block, not the
success path; it now asserts the success path, and the degraded ordering
keeps its own dedicated test.

Removes the disable comment and drops App.tsx from
config/max-lines-baseline.txt.

* refactor(renderer): move app-shell ref writes out of render

React Doctor's changed-lines purity gate flags three render-phase ref
writes in the new app-shell files. The patterns predate the split, but
moving them into new files brings them into the gate.

- use-app-chrome-layout: the terminal-workbench latch becomes state set
  during render (the pattern use-lazy-modal-mounts already uses). The
  `canMountTerminalWorkbenchNow ||` term keeps the current render correct,
  so the latch only has to be visible to the next one.
- use-app-startup-hydration: the onboarding callback ref syncs in an effect
  declared before the boot chain, so it lands first on mount. The callback
  is a `useCallback([])`, so its identity never actually changes.
- use-global-keybindings: the shortcut-state mirror syncs in useLayoutEffect,
  which commits before any key event can read it. Key events are discrete,
  so handlers always observe committed state.

All three now hold committed state only, so nothing can leak from a render
React discards — the behavior the gate is protecting.

pnpm run check:react-doctor:changed: 0 errors (was 3).
2026-08-14 17:46:44 -07:00
Brennan Benson e12b22c435 fix(terminal): bracket agent-pane pastes so a pasted newline can't submit the draft (#14456)
* fix(terminal): bracket agent-pane pastes so a pasted newline can't submit

A paste of <=64KB is handed to xterm's term.paste(), which rewrites newlines to
CR and wraps in ESC[200~/ESC[201~ only when its parser observed DECSET 2004.
Windows ConPTY never forwards that mode, so an unbracketed pasted newline
reaches a TUI agent as Enter and submits the draft parked in its composer.

The existing force-bracket guard keyed on isWindowsUserAgent() - the client's
platform - but the ConPTY can be on a remote host, so the guard was off in
exactly the configuration that needs it. Gate on the pane's own TUI agent
instead, applied to all four paste entry points; middle-click primary selection
had no force flag at all.

The agent status row is retained deliberately and is not lifecycle-driven, so
it can outlive the agent. Veto on shell-confirmed foreground (OSC 133;D) and on
a row rehydrated across an app restart. The freshness TTL and a state check are
both unusable here - an idle-but-live agent sits at done and still needs
bracketing.

Refs STA-4294

* fix(terminal): key agent-paste bracketing on live evidence, not shellForeground

Measured against a real pane: shellForeground is republished only at OSC 133
boundaries, so a shell without 133 integration leaves it latched true while an
agent owns the foreground. Vetoing on it silently reinstated the submit bug the
parent commit fixes - the parked draft was sent on paste with the gate in place.

Prefer process-confirmed agent identity when present, keep the restart-rehydrated
veto, and drop the shellForeground veto. Erring toward bracketing costs a literal
ESC[200~ in a non-2004 program; erring the other way sends the user's draft.

Refs STA-4294

* docs(terminal): record why paste bracketing keys on the agent, not the mode bit

Measured in real ptys before taking the obvious alternative. A tri-state on
DECSET 2004 (observed-on / observed-off / never-observed) does not work: zsh 5.9,
fish 4.8.1 and bash >= 5.1 announce and withdraw the mode cleanly, but macOS
/bin/bash 3.2, /bin/sh, bash 4.4 and any shell with bracketed paste disabled emit
nothing at all, byte-identical to a bare `cat`. Silence cannot be read as consent.

Agent identity disambiguates it in the one direction that matters: agents always
enable the mode, so silence on an agent pane means the announcement was lost in
transit, never an opt-out. Also measured: bracketing a program that never
negotiated is worse than useless - the markers land as literal payload bytes and
ICRNL still turns the CR into a submit - so the gate stays narrow.

Refs STA-4294

* fix(terminal): guard the paste pane key and document the evidence policy

Readiness review follow-ups, none behaviour-changing for reachable inputs.

makePaneKey throws on a malformed leaf/tab id. It is unreachable today (pane.leafId
is a minted UUID and the same pair is already called unguarded from a hotter site),
but the failure mode was bad: the throw escapes before the paste helper's catch is
attached, so the paste would be a silent no-op with no error surface. Degrade to the
pre-fix path instead, with a test.

Also record two things a future reader needs: the process-confirmed branch is dead
for remote-runtime and SSH panes because foreground tracking is disabled there, so a
remote pane's status row is its only evidence; and why this resolver deliberately
omits the shellForeground/routingRevoked/routingTrusted gates its two siblings
enforce - they route input bytes, this only wraps a paste whose payload is
ESC-sanitized downstream.

Refs STA-4294

* fix(terminal): encode Windows agent paste newlines as input records

* fix protected paste handling in dashboard previews
2026-08-14 17:18:14 -07:00
Wooseong KimandBrennan Benson c0f9dcc8f4 fix(git): allow bounded override of worktree-add timeout (#12823)
* fix(git): allow bounded override of worktree-add timeout

Keep the 180s OneDrive stall guard as the default floor, but accept
ORCA_WORKTREE_ADD_TIMEOUT_MS up to 30 minutes for legitimately slow
checkouts (large repos, git-crypt).

Preserves a closed upper bound; never removes the timeout.

Fixes #12696

* review: read the worktree-add timeout override at the call site

Keeps WORKTREE_ADD_TIMEOUT_MS meaning the 180s default instead of
silently becoming an env-resolved value, drops the redundant third
export, and folds three parse guards into the clamp Number() already
covers.

Adds the missing coverage that addWorktree actually passes the raised
timeout to git — reverting the call-site wiring previously failed no
test.

* review: clamp an infinite override to the max and warn on a discarded value

Number.isFinite sent ORCA_WORKTREE_ADD_TIMEOUT_MS=Infinity — the natural
way to say 'stop killing my checkout' — back to the 180s default, handing
the operator the exact failure they set the variable to escape. Reject
only NaN and let the clamp handle magnitude.

Every discarded or clamped value was silent, so the '=300' seconds/ms
mixup the floor exists for produced an identical 'git timed out.' with no
signal. Warn once, naming the accepted range.

Also pins both bounds as literals and refreshes two comments that no
longer described the code.

* review: name the real problem in the override warning

An unparseable value took the range branch, so ORCA_WORKTREE_ADD_TIMEOUT_MS=600_000
— the literal style this file itself uses — reported a bound violation that had not
happened. Split the two cases and quote the value so trailing whitespace is legible.

Uses the file's [git/worktree] log prefix, drops a #7225 citation that describes a
startup/UI-freeze report rather than a large checkout, and states why the ceiling
is 30 minutes.

* review: give the resolver a contract and stop splitting the timeout block

Moves resolveWorktreeAddTimeoutMs below the constants so the module's five
timeouts read as one group, and replaces the edge-case JSDoc with the actual
contract — what it reads, what range it clamps to, when it warns.

Comments the NaN-comparison the unparseable-value warning depends on, since
'fixing' it with an isNaN guard would silently delete that warning. Test
spy now matches the file's local-spy idiom, the bound literals get their own
test, and the env stub deletes the key instead of setting an empty string.

* review: pin the clamp-up warning text

Every warn assertion covered a value clamped DOWN to the floor, so swapping
the discriminator back to !Number.isFinite passed all 74 tests while telling
an operator that ORCA_WORKTREE_ADD_TIMEOUT_MS=Infinity 'is not a number;
using 1800000ms' — naming the number it just used, and misdirecting exactly
the person this override exists for. That mutation now fails.

Restores the STA-1292 rationale the call-site comment had dropped, and puts
the env var name and issue back on the ceiling constant.

* review: correct two comment claims about the warn path

The JSDoc promised a warning 'whenever the value is not used verbatim',
but trimming and fractional truncation deliberately stay silent — the
suite asserts exactly that for '300000.9', so the contract contradicted
the tests below it.

The condition comment named 'NaN !== NaN', a comparison that never runs:
resolved is the default whenever requested is NaN, so the live comparison
is 180000 !== NaN. Same warning, right mechanism.

* review: fix the ceiling arithmetic and name the default/floor coupling

30 min against a 3.5 min worst case is ~8x, not ~10x — the comment's only
job is justifying that number. States the actual cost too: a genuine stall
now blocks a create for up to 30 min instead of 3.

WORKTREE_ADD_TIMEOUT_MS silently serves as both the default and the clamp
floor, so tightening it to fail faster would also re-admit the
'=300 means seconds' mistake the floor exists to catch. Now said out loud.

Widens 'git-crypt' to 'a slow content filter' so an LFS or large-monorepo
reader does not conclude their case is different, drops a call-site clause
that restated the constant's comment, and corrects a test comment that
claimed an idiom the code does not use.

* review: pin the warning's prefix and variable name

Deleting the [git/worktree] prefix left the suite green — all three warn
assertions started matching after it. A diagnostic nobody can grep for is
not a diagnostic, so one assertion now pins the whole line.

* review: correct the last three comment claims

A blank value is clamped (Number('') is 0) and stays silent, so 'warns when
a value is rejected or clamped' had an exception the test below it already
exercised. Now says non-blank.

The floor-coupling note claimed lowering the default re-admits the
'=300 means seconds' mistake; it does not — a 60s floor still clamps 300.
The actual cost is that the minimum any override can request drops with it.

Drops the spy comment rather than rewriting it a third time; beforeEach and
afterEach say it themselves.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-14 17:00:16 -07:00
Brennan Benson 0e8d52912c fix(source-control-ai): stop duplicating singleton CLI flags in agent argv (#14585)
* fix(source-control-ai): stop duplicating singleton CLI flags in agent argv

Recipe CLI arguments and agent command overrides were appended on top of
Orca's generated flags, so a user-supplied --model produced a repeated
flag. yargs collapses a repeated flag into an array, crashing OpenCode
with "j.split is not a function"; clap rejects it outright for Codex.

Declare the at-most-once option groups per agent spec and fold every
user-supplied occurrence into the generated slot, with recipe args
outranking a command-override prefix.

Fixes #12305

* fix(source-control-ai): preserve command override argv order
2026-08-14 16:53:43 -07:00
Jinwoo Hong 19bc1ccf7e Keep other-client workspace filter visible during catalog hydration (#14595) 2026-08-14 16:12:49 -07:00
Jinwoo Hong 500b72d8ef fix(vm): harden provisioned root ownership and cleanup (#14477)
* fix(vm): verify provisioned root ownership

* test(vm): retry transient removal menu

* test(vm): stabilize provisioned root teardown

* fix(vm): clarify recipe-owned cleanup

* fix(vm): pin provisioned root source commit

* fix(vm): make runtime cleanup user-cancellable
2026-08-14 19:04:55 -04:00
Jinwoo Hong 32f46f9a24 fix(vm): keep failed cleanup retryable (#14476) 2026-08-14 19:04:54 -04:00
Jinwoo Hong 8b22f044f5 fix(vm): preserve runtime sidecar rollback compatibility (#14444)
* test(vm): reproduce runtime store rollback poisoning

* fix(vm): keep runtime sidecar rollback-readable

* fix(vm): harden rollback-compatible runtime persistence

* fix(vm): publish rollback lifecycle authority first

* test(vm): harden rollback compatibility coverage
2026-08-14 19:04:54 -04:00
Jinjing 3a212584ec feat(native-chat): Extra high grok effort per model (#14577)
* feat(native-chat): offer Extra high grok effort per model

Slice grok's reasoning-effort menu by each model's advertised ceiling so
4.6 can reach xhigh while 4.5 stays at high, and keep the untouched
default at high so launch argv does not silently escalate.

* fix(grok): parse dashed rows in grok models listing

Grok stars only the default model and dashes the rest. A star-only
bullet dropped 4.5 from the picker once discovered models were
authoritative.
2026-08-14 15:52:09 -07:00
Brennan Benson 9bb8836bb6 fix(agent-launch): wait longer for cold-boot Codex composer before dropping prompt (STA-3367) (#12853)
* fix(agent-launch): wait longer for cold-boot Codex composer before dropping prompt (STA-3367)

Continue-in-new-session pastes the handoff prompt once Codex renders its
composer glyph, gated on an 8s readiness budget. A cold/first-run Codex can
take longer than 8s to mount its composer, so the wait timed out and the
prompt was silently dropped into an empty terminal.

Marker-gated ready signals (Codex glyph, opencode show-cursor) are positive
proofs: the paste fires only when the marker actually renders, so a longer
budget can never paste prematurely — it only tolerates slow cold boots. Give
those signals a 20s budget while the markerless quiet-window signal keeps 8s.

* fix(agent-launch): share the composer-readiness budget across all three delivery owners (STA-3367)

The cold-boot fix was correct but landed as a single-path exception, and it
double-spent its own budget. Three follow-ups so the behavior is a system rule:

1. Split the PTY-spawn wait from the composer wait in pasteDraftWhenAgentReady.
   Both were handed the same budget, so a codex tab took up to 41s to report a
   dropped prompt. "Tab has a PTY" and "composer accepts input" are separate
   states: spawn keeps a fixed 8s, and the readiness budget now starts once the
   PTY exists, so a slow spawn can't shorten a cold composer's window.

2. Move the per-signal budget to draftPasteReadyBudgetMs() beside the shared
   readiness scanner. The budget is a property of the ready signal — only that
   module knows which signals are marker-gated — so all three delivery owners
   (renderer tab paste, renderer startup paste, main runtime startup paste)
   consume one policy instead of three hardcoded 8s constants.

3. Give the main-runtime startup paste the process-ownership fallback both
   renderer paths already have. It resolved null on budget expiry, silently
   dropping the prompt on worktree-create / CLI / remote-host delivery — the
   same STA-3367 failure, on the path the original fix didn't reach.

Adds coverage for the main-runtime waiter, which had none.

Test: vitest src/main/runtime src/shared src/renderer/src/lib
      src/renderer/src/components/terminal-pane — all green; tsc clean.

* test(agent-launch): consume the shared readiness budget instead of restating it

Hardcoding 20000 in the runtime waiter test meant it would keep passing if
OrcaRuntimeService stopped consuming draftPasteReadyBudgetMs — the exact drift
this PR exists to prevent. The literal values stay pinned once, in the scanner
test.

* refactor(agent-launch): collapse the readiness budget to one flat timeout

The per-signal budget (marker 20s / quiet-window 8s) tied the timeout to how
readiness is DETECTED. The budget is really a property of how slowly an agent
can boot — a marker, a quiet window, and a process check all wait out the same
cold start — so one number covers all three signals.

Replaces draftPasteReadyBudgetMs() with DRAFT_PASTE_READY_TIMEOUT_MS: drops a
constant, a branch, and two tests, and removes the only reason a delivery path
needed to know which signal class it was using.

Cost: a launch that never emits DECSET 2004 now surfaces its 'prompt not sent'
toast at 20s instead of 8s. That is the failed-launch path only; successful
markerless delivery still resolves on the 1.5s quiet window as before.

* fix(agent-launch): constrain cold Codex readiness budget

* fix(agent-launch): observe Codex readiness from PTY bind

* fix(agent-launch): anchor early Codex prompt to TUI screen
2026-08-14 13:33:05 -07:00
Brennan Benson e2d309e9cd fix(agent-map): commit slider changes once (STA-4281) (#14442)
* fix(agent-map): commit time slider changes

* fix(agent-map): cancel invalidated slider commits

* fix(agent-map): reconcile collapsed slider drags
2026-08-14 13:10:51 -07:00
Brennan Benson cbed44410a STA-4276: preflight Codex in Command Prompt and Git Bash (#14441)
* fix(terminal): preflight Codex in Windows cmd and Git Bash

* test(terminal): run Windows preflight through ConPTY

* test(terminal): isolate cmd harness exit status

* test(terminal): allow slow Git Bash ConPTY startup
2026-08-14 13:10:29 -07:00
Brennan Benson b614908ccc fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270) (#14458)
* fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270)

The Codex launch preflight carried a bare command name ('orca' / 'orca-dev') in
ORCA_CODEX_LAUNCH_PREFLIGHT. The codex() wrapper that invokes it is emitted after
the user's profile scripts are sourced, and those routinely rewrite PATH, so the
name was resolved against a PATH Orca neither controls nor can predict.

Resolve and verify the shipped CLI's absolute path instead, and return null when
no path verifies so the preflight is skipped rather than run against an
unidentified program.

* test(codex): align bundled launcher fixture across CI hosts
2026-08-14 13:09:50 -07:00
Jinwoo Hong 552dedd26b Keep remote workspace filter visible (#14579) 2026-08-14 13:06:57 -07:00
Jinwoo Hong ff9bc0f079 fix(orchestration): wait for Codex composer render (#14575) 2026-08-14 12:56:35 -07:00
Brennan Benson ebb60a0757 fix(devin): preserve JSONC comments when writing hook config (#14199)
Devin documents config.json as JSONC. Installing hooks parsed it with
jsonc-parser and then reserialized with JSON.stringify, silently dropping
the user's comments, key order, and formatting on every install.

Edit the original text with modify/applyEdits one hook event at a time so
untouched entries keep their attached comments, and let both writers accept
pre-serialized text so the shared atomic write and rolling backup are reused.

The two existing tests asserted with JSON.parse, which could only pass once
the comment had been stripped; both now parse as JSONC and assert the
comment survives.
2026-08-14 12:50:35 -07:00
Brennan Benson b3c4ba9ed3 fix(daemon): harden checkpoint admission after per-session isolation (#14385)
* fix(daemon): bound checkpoint overlay fanout

* fix(daemon): retain checkpoint admission through deadlines

* docs(reliability): refresh checkpoint gate evidence

* fix(daemon): prevent checkpoint admission starvation

* docs(reliability): clarify checkpoint bounds

* fix(daemon): distinguish checkpoint admission warnings

* fix(daemon): bound checkpoint admission diagnostics
2026-08-14 12:21:55 -07:00
Brennan Benson f4cebe14f5 fix(daemon): bound the caller's wait on final durable-history checkpoints (STA-4228) (#14497)
* fix(daemon): bound the caller's wait on final durable-history checkpoints (STA-4228)

shutdownWithHistoryLock threaded the caller's absolute deadline into
ensureConnected and into the kill RPC, but awaited the final keep-history
checkpoint between them with no bound at all. Worktree sleep supplies that
deadline, so a stalled history write pinned the process-wide checkpoint tail
and stranded Sleep Terminals until an app restart.

Bound only the caller's wait. The checkpoint itself stays deadline-free: it
remains the exclusive tail, runs to completion, and still commits, so nothing
durable is cancelled or deferred. On expiry the caller stops awaiting, throws
FinalCheckpointWaitExpiredError, and never falls through to the kill, so the
PTY stays alive and the stop is reported unverified.

* test(daemon): prove final checkpoint deadline outcomes
2026-08-14 12:21:35 -07:00
Brennan Benson 6cd987effb fix(daemon): persist the pending-output counter across empty incremental takes (STA-4297) (#14496)
* fix(daemon): persist the pending-output counter across empty incremental takes (STA-4297)

An empty incremental take advanced pendingOutputSeq without writing a log
batch, so the in-memory counter ran permanently ahead of the log. The next
warm reattach could not prove continuity and committed the live 1000-row
window over a deep durable checkpoint.

Advance the counter only for takes that get persisted: a snapshot take
(stamped into the checkpoint) or one carrying records/overflow. This matches
the layers below, which already treat an empty take as a no-op write.

* test(daemon): keep empty-take coverage outcome-based
2026-08-14 12:21:03 -07:00
Brennan Benson 83e2123582 Add global worktree visibility source defaults (#14276)
* Add global external worktree visibility defaults

* Expand global worktree visibility source defaults

* Fix host-scoped visibility settings races

* Fix global worktree visibility integration

* Enable source visibility defaults on mobile

* Polish external worktree settings navigation

* Clarify inherited worktree visibility settings

* feat(sidebar): replace the inherited-visibility switch with a Show/Hide picker

Each source row now shows a two-segment Show / Hide control preselected to the
global setting, and explains itself only where the project actually disagrees:
an "Overriding global setting: <value>" card names the value being ignored.
Picking the segment global already holds drops the override instead of pinning
a duplicate, so the same control both overrides and reverts, retiring the
separate "Use global" link. The dialog footer now lists every inheritable
source with its global value.

* fix(sidebar): preserve reset for matching visibility overrides
2026-08-14 12:15:58 -07:00
Brennan Benson 266b5ae8f5 fix(mobile): match desktop project and run target picker (#14457)
* fix(mobile): disambiguate repository locations

* fix(mobile): preserve explicit repository ownership

* test(mobile): use explicit renderer type

* refactor(mobile): match desktop project targets
2026-08-14 10:22:55 -07:00
Jinjing cb42b60849 fix(linear): label Start workspace and add Open on Linear (#14492)
* fix(linear): label Start workspace and add Open on Linear

The issue page header used three unlabeled icons. Match the GitHub
issue header: copy stays quiet, Open on Linear is an external-link
control, and Start workspace is a labeled primary button.

* Address PR review feedback (#14492)

- Make the header source contract ignore Start workspace formatting
2026-08-14 02:29:45 -07:00
Neil 87bafb5e6a fix(terminal): ground the emulator to the snapshot's baseline after a byte gap (#14374)
A hidden-delivery byte gap can strand more than the SGR pen, and the reset
#14241 added to the split alt-screen replay is undone before any content is
painted: xterm answers `?1049l` with restoreCursor(), which reloads the pen,
all four G-set designations, GL, origin mode and wraparound from the register
saved at `?1049h`.

- Bracket the buffer switch with the baseline: before, so `?1049h` banks
  grounded state rather than the gap's; after, so `?1049l`'s restore cannot
  reapply it.
- Ground everything a serialized payload is diffed against, not just the pen:
  SGR, GL, all four G-sets, origin, autowrap, insert, the per-buffer scroll
  region, and the saved-cursor register.
- Switch buffers only when the pane is actually on the other one. `?1049` is
  not a no-op otherwise — it still swaps the kitty flag registers, which would
  park the flags of an agent that negotiated them on the normal screen.
- Return to the normal buffer when the gap ate the TUI's exit sequence; the
  restored history was painting into the alt buffer with scrollback left empty.
- Restore the CAN #14241 dropped, so a control string the gap truncated is
  discarded instead of committed by the next ESC.
- Ground the abandon path exactly once instead of twice.
- Derive the parity/fuzz preambles from the same builder; they had drifted and
  were asserting against bytes production no longer emits.
2026-08-14 02:22:46 -07:00
NeilandOrca 41ba29d79a test(e2e): headless preedit-geometry coverage for Korean and CJK terminal input (#14500)
* test(e2e): headless preedit-geometry coverage for Korean and CJK terminal input

Both IME defects that shipped and were reverted passed a suite of ~3000 IME
assertions, because every one of them checked bytes reaching the pty and a
preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The one arm that asserted real geometry was headful-gated and
macOS-only, so it never ran in CI.

Drives composition through CDP Input.imeSetComposition rather than a native
input source, which removes the accessibility grant, the system input source
and the visible window that forced that gate, so this runs in the ordinary
electron-headless project.

The load-bearing assertion is the composition overlay's real bounding rect.
Verified to have teeth: with max-width 0 and overflow hidden injected, the
active class, the textContent, display block and checkVisibility all still
pass, and only the rect assertion fails.

* test(e2e): restore the CDP composition drivers the preedit specs need

The trimmed copy on main kept only the key-dispatch helpers, so the composition
drivers the geometry specs import were missing. Adds them back: setImeComposition,
commitImeText, dispatchImeProcessKey, composeHangulSyllable and
dispatchResumedCompositionUpdate. The shared helpers are unchanged.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-14 01:47:03 -07:00
Langning ZhangandJinwoo-H 92b6ffd17d Terminate renderer graph reload generations and contain disposed-frame notifications (#14070)
* fix(runtime): terminate renderer graph reload generations

* fix(runtime): harden renderer reload teardown

* fix(runtime): fence renderer graph publication ownership

* test(runtime): register renderer graph reload gate

* test(runtime): record live reload validation

* fix(runtime): ignore cancelled renderer navigations

* chore: preserve main formatting during branch sync

* chore: satisfy changed-code quality gate

* fix(runtime): restore cancelled renderer reloads

* fix(runtime): preserve committed reload fencing

* test(runtime): prove cancelled reload timeout

* docs(reliability): record reload cancellation oracle

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-08-14 01:21:00 -07:00
NeilandOrca 46e1c3eee4 fix(terminal): strip the captured shim dir across trailing-separator spellings (#14439)
* fix(terminal): strip the captured shim dir across trailing-separator spellings

The scrub compared the captured ORCA_ATTRIBUTION_SHIM_DIR to PATH entries literally, so a trailing-separator difference left the legacy shim directory on the spawned PATH. Same class already fixed in the generated wrappers.

Also drops a dead default in the POSIX filter, consolidates comments that had accumulated across fixes, and normalizes the legacy directory once rather than per PATH entry in the cmd wrapper. The boundary scanner stays: a shim path can contain the PATH delimiter, which splitting would fragment.

* fix(terminal): keep the git shim tombstone parseable on Windows

The cmd wrapper carried two em dashes in comments. cmd.exe seeks through a
batch file in bytes but advances by decoded character count, so those four
extra UTF-8 bytes made it drop the first four characters of every line and
the wrapper died with "The syntax of the command is incorrect."

Also move the legacy-dir trailing-separator strip into a CALL body: cmd
expands a whole line before evaluating `if defined`, so inline it ran its
substring syntax against an unset variable and mangled the line.

Rooted-path checks are shared by a single subroutine, a relative or
drive-relative captured ORCA_REAL_* is rejected, and relative PATH entries
are dropped from the exported PATH so the cwd cannot select spawned tools.
The POSIX tombstone is deleted rather than written when no absolute
interpreter can be verified.

Verified on Windows 11 (cmd and PowerShell 5.1): normal lookup, relative
and drive-relative ORCA_REAL_GIT, relative and drive-relative PATH
entries, trailing separators, legacy shim dir, empty PATH, and a
cwd-only PATH with a planted git.cmd/git.bat.

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

* fix(terminal): stop the git shim tombstone re-expanding PATH data

cmd re-expands a CALL command line, so path data handed to a subroutine as an
argument got a second round of percent expansion. A PATH entry holding a
literal %CD% became the current directory before the rooted-path guard saw it,
and the wrapper then ran a planted git.cmd from that directory (exit 66,
reproduced on Windows 11). Callers now pass the value in a variable, which is
expanded once.

Re-verified on Windows 11 across 15 cases: the %CD% entry is now dropped and
the real git runs, and a PATH entry spelled 'C:\paren9 (x86)\cmd' still
resolves, so the new for-block did not regress paths with parentheses.

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

* fix(terminal): keep percent expressions out of the shim tombstone comments

cmd expands variables inside rem, so a comment naming the working directory
substituted a path into itself. Verified on Windows 11 that rem does not
re-parse the result -- a cwd of 'C:\x&pwned&rem' executed nothing and the real
git still ran -- so this was not exploitable, but rem handles separators
differently inside a parenthesized block and this script now has some. A test
now rejects any percent sign in an emitted rem line.

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

* fix(terminal): pin shim tombstone shell state and directory identity

Three fixes, each proven before and after.

Delayed expansion: bare setlocal inherits the caller's state. Under a parent
shell started with /V:ON, a literal !CD! PATH entry became the current
directory and a planted git.cmd ran (exit 66), and a legitimate directory
containing ! stopped resolving (exit 127). Both on Windows 11, both gone with
setlocal DisableDelayedExpansion.

Directory identity: the POSIX filter compared the legacy shim directory
lexically while comparing its own directory with -ef, so a symlink or a
<legacy>/../<legacy> spelling escaped the filter and the live attribution
wrapper won the lookup. It now tests both. The env scrub had the same gap and
now normalizes before its suffix test.

Retained POSIX wrappers: with no absolute bash verifiable the wrapper was
deleted, which strands a shell that already hashed the path on 127 instead of
falling through to PATH. It now reuses the shebang of the wrapper it replaces,
which is known to work on that host, and rejects /usr/bin/env so the ambient
lookup stays closed. Deleting is the last resort.

Two Windows test pins matched the wrong occurrence and stayed green with the
guard they claimed to protect removed; they now assert the subroutine body.
All five fixes were mutation-tested.

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

* fix(terminal): require bash for a reused shebang and exclude slash-spelled dirs

The retained-shebang fallback accepted any absolute executable that was not
env, but the rendered body needs BASH_SOURCE, [[ and local, so a #!/bin/zsh
wrapper was accepted and then exited 1 on 'BASH_SOURCE[0]: parameter not set'.
It now requires bash, which also rejects /usr/bin/env as before.

The PowerShell filter trimmed only backslashes while its rooted-path regex
accepts forward slashes, so a wrapper or legacy directory spelled with a
trailing / missed the lexical exclusion. Verified on Windows 11 that both
spellings are now excluded and the real git still runs.

The test that claimed to cover the shebang fallback only called the resolver
directly, so deleting the wiring left the suite green on any host with
/bin/bash. It now mocks the resolver to null and asserts through
neutralizeLegacyTerminalShimDir that the wrapper survives with the retained
shebang, and that a wrapper without a reusable one is still deleted. Both
mutations are now killed.

The Windows wrapper text assertions move to their own file rather than taking
a max-lines exemption.

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

* fix(terminal): stop the shim PATH scrub deleting a legitimate directory

The previous round collapsed '..' lexically before classifying a PATH entry.
That is not the same as resolving it: when <shim>/posix is a symlink,
<shim>/posix/../posix lands elsewhere, so a legitimate directory was
classified as the shim and removed, leaving git unresolvable. Reproduced with
a symlinked shim/posix and a real git behind it.

Resolving for real is not available here either -- this env is also built for
remote and WSL panes whose paths name nothing on the local filesystem -- so the
classifier is lexical again, deliberately. A '..' spelling that slips through
costs nothing at runtime: that directory holds the pass-through tombstone, and
the tombstone excludes its own directory by -ef, so the lookup still reaches
the real git.

Separately, pathEntrySpellings can only enumerate one added separator, so a
captured directory spelled with two or more survived the literal removal. The
split filter now also compares separator-stripped forms, which covers any
number. Both changes are mutation-tested.

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

* fix(terminal): stop a relative shim dir letting the cwd pick the binary

The -ef identity test added two rounds ago resolves a relative right-hand
operand against the wrapper's current directory, so a relative
ORCA_ATTRIBUTION_SHIM_DIR let the cwd decide which PATH entry counted as the
legacy directory and got a legitimate one skipped. Reproduced as SAFE vs LATER
purely by changing the cwd. Identity is now attempted only for an absolute
target; the lexical compare still covers the rest.

The cmd wrapper had the same shape: full-path expansion made a relative
captured value absolute against the cwd before PATH filtering. It now requires
a rooted value and leaves the normalized form unset otherwise, which makes the
reject subroutine a no-op. Verified on Windows 11 that two runs differing only
in cwd now agree.

Separately, trailing-separator stripping treated a backslash as a separator on
POSIX, where it is a legal filename character, so '/tmp/captured\' and
'/tmp/captured' compared equal and a real directory was deleted from PATH. The
rule is platform-specific now; the cross-platform classifier still understands
both styles because a Windows PATH reaches it through the remote env.

Both fixes are mutation-tested.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-14 01:17:37 -07:00
Neil b82a8791f5 perf(runtime): resolve an explicit worktree id without scanning every repo (#14399)
`resolveWorktreeSelector` resolved every selector kind from the whole-fleet snapshot, so a targeted `id:<repoId>::<path>` lookup fanned `git worktree list` across every registered repo to answer a question about one of them. With a cold scan cache -- app startup, or the first lookup after a mutation clears the snapshot -- that is one subprocess per repo, ~17ms each, to find a worktree whose owning repo the id already names. Measured on a ten-repo fleet: one `id:` lookup scans 10 repos before and 1 after.

Scope only `id:`. Every other selector kind is matched across the fleet and its `selector_ambiguous` contract is defined over all repos, so scoping `branch:`, `name:`, `issue:`, or a bare selector would silently pick a winner where they correctly refuse today. A test pins that: `branch:main` across ten repos still throws `selector_ambiguous` and still scans all ten.

Lineage stays correct because edges are intra-repo by construction. The scoped path returns null and falls back whenever that does not hold: a repo id registered on several execution hosts, an unknown repo id, or a worktree the scoped scan does not contain. A warm fleet snapshot always wins.

Row resolution moves out of orca-runtime.ts into repo-worktree-row-resolution.ts, which owns no state -- the cache-aware scan and folder-workspace stamping are injected. orca-runtime.ts ends up 65 lines shorter than before despite the added feature.
2026-08-14 01:15:30 -07:00
Jinjing a6a64439a0 fix(terminal): keep split error when rejected cleanup throws (#14463)
Wrap kill and retireRejectedPty so a cleanup failure cannot replace
the original split-authority error or skip the remaining teardown.
2026-08-14 00:47:45 -07:00
dngur6344 78ca45e2ae fix(renderer): remove duplicate git history tooltips (#14453)
* fix(renderer): remove duplicate git history tooltips

* test(renderer): harden tooltip regression coverage
2026-08-14 00:33:15 -07:00
Jinjing d03eb3218e Revert "fix(renderer): stop git history hover from showing two tooltips (#14468)" (#14485)
This reverts commit 2e91a74deb.
2026-08-14 00:32:43 -07:00
NeilandOrca 2100fb2553 fix(runtime): cap remote git.diff and file previews at the transport budget (#14160)
* fix(runtime): cap remote git.diff and file previews at the transport budget

A remote or mobile user who opens the diff of a large image loses their whole
WebSocket, not just that request: the E2EE channel closes with 1013 when a reply
exceeds the 4 MiB outbound envelope. Two producers can exceed it unaided.

git.diff/branchDiff/commitDiff cap text with MAX_RENDERED_DIFF_COMBINED_CHARACTERS
(6M chars) -- a *renderer* budget that sits above the transport limit -- and return
base64 for previewable binaries bounded only by MAX_GIT_SHOW_BYTES, so a 10 MiB PNG
changed in place is ~26.7 MiB in one envelope. files.readPreview inlines base64 up
to 10 MiB, and mobile calls it for every image tab.

Both now measure against a budget derived from the outbound limit. The check sits in
orca-runtime-git.ts, downstream of the dedupe and of both the SSH-provider and local
branches, so a payload forwarded verbatim by an old relay is covered by the same code
and src/relay needs no change. Local and in-process callers pass no budget and keep
full fidelity.

Measuring raw bytes would not work, which is the whole reason this needs a module.
JSON escaping turns one control byte into six (\u00XX), and binary-buffer.ts sniffs
only for NUL in the first 8 KiB -- so a NUL-free file of 0x01-0x1f bytes is classified
as *text*, would pass a raw-byte cap, and would then blow the envelope. The budget is
escape-aware, with a three-branch fast path that keeps normal diffs at two native
byteLength calls and scans only the ambiguous band.

The SSH branch of readFileExplorerPreview had the same raw-vs-escaped gap: its stat
gate sizes base64 binaries, but text crossed unbounded. It now honours the same
decoded-text limit the local branch already enforced.

No wire change: GitDiffResult is untouched -- no third kind, no new field. Old clients
see an error for one request instead of a dropped connection. diff_too_large joins the
structured passthrough codes and lands on an existing error arm in both mobile
consumers and the desktop remote path; file_too_large was already handled on both.

Instruments the 1013 close, which nothing measured before, so the incidence this cap
is meant to drive to zero is finally observable. `emitter` separates a producer size
bug from a wedged link.

Known regression: remote image previews between ~3.096 and ~3.146 MB now return
file_too_large. They only intermittently worked before -- above ~3.0 MB they killed
the socket -- so this trades intermittent connection loss for a consistent error.

Test: 10281 passed in src/main/runtime + src/shared + src/main/git; mobile 3427
passed. Each of the six budget-enforcement sites is independently mutation-killed.
Escaping fixtures cover newline-dense, control-char, CJK, lone-surrogate and base64
content against native JSON.stringify. tsc clean for node, web and cli; oxlint clean.

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

* fix(runtime): harden remote reply transport budgets

* test(runtime): cover desktop remote preview budgets

* test(runtime): close telemetry review gaps

* chore(shared): repoint budget imports after the shared/types barrel removal

Upstream #14447 dropped the shared/types barrel; GitDiffResult now lives in
git-diff-compare-types and GlobalSettings in global-settings-types.

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

* fix(ssh): surface an over-cap preview read as file_too_large

The stream reader aborts an over-cap read with StreamProtocolError, whose numeric
code falls through mapRuntimeError to a generic runtime_error carrying the raw
"Reported totalSize N exceeds client cap M" string. Neither preview client
recognizes that: runtime-file-client.ts and mobile-file-preview-response.ts both
key on file_too_large. It also made the two file_too_large guards directly below
the read unreachable on the streaming path.

Gives the cap its own error type so the caller can translate it, keeping the
bandwidth saving the cap exists for. A genuine protocol fault still propagates
unmasked.

Found by the readiness review. Mutation-verified: removing the translation fails
exactly the new test.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-14 00:24:57 -07:00
Brennan BensonandBrennan Benson d137bb93e1 fix(agent-status): stop start-less child stops from minting phantom working (#14375)
* fix(agent-status): stop start-less child stops from minting phantom working

buildClaudeCachedLeadStatusPayload fell back to 'working' whenever the pane had
no cached lead-turn state. That default is right for a spawn or a child tool
call, but the same helper serves SubagentStop and TeammateIdle, which end work
and prove the opposite.

claudeLeadStateByPaneKey is in-memory only, so every app restart empties it. A
Claude session that outlives the restart reports its next child event into an
empty map and the pane latches 'working' with an empty roster -- no Stop ever
clears it, and the 30-minute window only decays the sidebar dot, never the
stored state.

Fall back by the event's evidence: terminating child events resolve to 'done',
which still gates up through resolveClaudePaneState when the roster or
background work proves the pane is busy.

* fix(agent-status): require evidence for child completion

* fix(agent-status): publish matched teammate idle

* fix(agent-status): preserve confirmed child work

* fix(agent-status): retain live restored teammates

* fix(agent-status): reap unconfirmed siblings after child drain

* fix(agent-status): preserve unmatched restored children

* fix(agent-status): wait for lead completion after child stop

* fix(agent-status): persist restored child transitions

---------

Co-authored-by: Brennan Benson <brennan@stably.ai>
2026-08-14 00:13:56 -07:00
Neil b6d36c26f4 fix(terminal): resolve Ctrl chords positionally on a non-Latin layout (#14469)
macOS maps a control chord by physical key regardless of layout: measured with
UCKeyTranslate, physical A/U under Control produce U+0001/U+0015 on 2SetHangul,
Russian and Greek exactly as on ABC, though unmodified those keys give ㅁ/ㅕ,
ф/г, α/θ. A native terminal inherits this by passing the OS characters through.

The browser does not expose that translation. xterm's legacy encoder works
anyway because it reads keyCode, which Chromium reports from the physical key,
but its kitty encoder derives the key number from `key` and only consults
`code` when Shift or Option is held. Ctrl is not in that gate, so a pane with
the kitty protocol negotiated reports CSI-u for U+3141 rather than 'a' and the
chord does nothing. Ctrl+C escaped this only via its hand-written ETX bypass.

Recover the byte from `code` when `key` is non-ASCII, which reproduces the
OS control table. An ASCII `key` stays authoritative so a Dvorak remap is
honoured. KeyC is excluded: the interrupt policy owns it, and off macOS that
policy declines to a selection so the copy binding wins.

Fixes #13331
2026-08-14 00:02:53 -07:00
Jinjing b3efd48c7f test(e2e): stabilize terminal failure coverage (#14466)
* test(e2e): stabilize terminal failure coverage

* docs(e2e): classify failed-run findings

* rm report
2026-08-13 23:59:18 -07:00
Brennan Benson 9e5ee5ef8e feat(workspaces): rework cleanup discovery and dialog (#13413)
* fix(workspaces): support full cleanup scans

* feat(workspaces): persist cleanup snapshots

* feat(workspaces): add cleanup filter model

* refactor(workspaces): remove cleanup presets

* feat(workspaces): rework cleanup dialog

* fix(workspaces): keep cleanup row ordering render-pure

* refactor(workspaces): simplify cleanup browsing

* refactor(workspaces): show cleanup facts

* refactor(workspaces): surface cleanup row facts

* fix(workspaces): remove misleading cleanup count

* fix(workspaces): preserve full scan semantics

* fix(workspaces): scope snapshot persistence

* fix(workspaces): preserve cleanup browse compatibility

* fix(workspaces): reconcile cleanup dialog state

* test(workspaces): update snapshot store fixtures

* test(workspaces): preserve cleanup scan modes

* perf(workspace-cleanup): stream scan progress and size results

* fix(workspace-cleanup): address review feedback

* fix(workspace-cleanup): preserve host-scoped cleanup metadata

* fix(workspace-cleanup): declare review source dependencies

* fix(workspace-cleanup): align size scan banner

* fix(workspace-cleanup): shorten scan action

* perf(workspace-cleanup): avoid redundant scan IO

* perf(workspace-cleanup): bound restarted evidence scans

* fix(workspace-cleanup): satisfy scan queue lint

* perf(workspace-cleanup): bound scan and snapshot work

* perf(workspace-cleanup): serialize final enrichment

* test(workspace-cleanup): assert final enrichment drain

* fix(workspace-cleanup): stop progress after renderer teardown

* perf: batch workspace cleanup git evidence scans

* perf(workspace-cleanup): stop redundant snapshot and scan work

* fix(workspace-cleanup): resolve review findings across scan, store, and dialog

Correctness:
- Chunk git-evidence dispatches at the shared 500-target limit and exclude
  queued/in-flight ids from target selection, so fleets past the limit can no
  longer strand rows permanently mislabeled as checked-but-unknown.
- Key destructive selection pruning on the user's filter state instead of the
  per-tick matched-set identity; streaming reclassification no longer silently
  deselects rows.
- Clamp the facet clock to max(scannedAt, open time): a stale hydrated
  snapshot no longer misbuckets idle thresholds or keeps dead agents fresh;
  row labels use the same clock.
- Supersede and cancel the previous broad scan when a new one starts (renderer
  registry and same-sender guard in main) instead of racing two fleet scans.
- Gate snapshot persistence on hasTargetedWorkspaceCleanupScan so
  worktreeIds: [] can never persist an empty fleet snapshot.
- Re-apply dismissals at set-time in progress application so a dismissal
  landing mid-enrichment is not clobbered.
- Record a one-off local snapshot prune for single (unbatched) remote deletes
  so removed workspaces cannot resurrect from cache.
- Strip .exe when normalizing foreground process names so Windows agent
  processes match.

Performance:
- Cache per-candidate facet and review-info objects on candidate identity;
  no-op streaming ticks reuse the previous rows array and skip every
  downstream pass; matched-set identity is stable under equal membership.
- Compute facet counts/options only while the filter popover is open.
- Equality-bail git-evidence publishes; structural (non-stringify) facet-group
  comparison memoized in the toolbar.
- Identity-token fast path for the enrichment cache (cache hits skip both
  JSON.stringify signatures); prune viewed/dismissal records on removal and
  expiry; bound the superseded-scan-id set.
- Restore the no-op bail in removeWorkspaceSpaceWorktrees (regression).
- Abort main-side scans when the renderer is destroyed; module-scope
  controller maps survive handler re-registration.
- Batch removal preflight into one targeted scan (with refreshActivity) per
  500 ids instead of one scan per row.
- Scan repos at concurrency 2, report discovered counts upfront for honest
  progress, share fs-activity probes per path (folder workspaces), read only
  the reflog tail, and skip the snapshot read-before-write via a remembered
  scannedAt.

Split workspace-cleanup-worktree-listing, workspace-cleanup-facet-row-caches,
and workspace-cleanup-selection-model out of files that crossed max-lines.

* fix(workspace-cleanup): address verifier findings

- Fall back to a full reflog read when the newest record exceeds the 8KB
  tail window, so an oversized subject cannot hide recent ref activity.
- Bound the single-removal snapshot prune batch id with a UUID; embedding
  the unbounded worktreeId silently failed main's 128-char validation and
  skipped the prune for long remote ids.
- Key the main-side broad-scan supersession by sender AND scan mode so
  legacy suggestion-only and full-workspace scans stay isolated, matching
  the renderer registry.

* fix(workspace-cleanup): own facet caches with useMemo instead of render-time ref writes

React Doctor (CI changed-lines gate) correctly flagged the three cache refs
written during render. Each per-candidate cache now lives in one memo with
the derived context it is keyed on, so the memo deps are the invalidation
and interior fills stay content-addressed; the matched-set identity
stabilization is dropped since its only consumer reads through a
useEffectEvent and never keys on identity.
2026-08-13 23:47:20 -07:00
github-actions[bot] f4e4b5aded Update README downloads badge 2026-08-14 06:46:22 +00:00
Jinjing 2e91a74deb fix(renderer): stop git history hover from showing two tooltips (#14468)
Remove native title attributes from commit rows, subjects, and ref
badges so only the managed tooltip appears.
2026-08-13 23:44:43 -07:00
Neil fcd99fc032 fix(sidebar): satisfy extracted hook quality gates (#14467) 2026-08-13 23:40:04 -07:00
Neil 08027d0d23 refactor(sidebar): split WorktreeList into a worktree-list module (#14465)
WorktreeList.tsx was 6.8k lines behind an `eslint-disable max-lines`. Break it
into `sidebar/worktree-list/`: the container keeps store wiring and composition,
the virtualized viewport keeps layout, and the drag, reveal, virtualization,
row-model, and row-render concerns each get their own file. Every file now fits
the oxlint budget, so the suppression and its baseline entry are gone.

Behaviour-preserving. The only deliberate cleanups are duplicate branches folded
into shared helpers (drop-preview state updates, status-hover fallback, the two
identical scroll-to-index reveal branches) and a dead sticky-header-index ref.

Tests that asserted on WorktreeList.tsx source text or imported its named
helpers now point at the module that owns them.
2026-08-13 23:35:41 -07:00
Neil e2e7768cc8 fix(terminal): deliver Ctrl+C when a non-Latin input source rewrites the key (#14462)
With a non-Latin input source the OS reports the layout's own glyph for `key` —
a Hangul jamo on Korean 2-Set, Cyrillic es on Russian — while `code` stays KeyC.
The interrupt policy read `key` for identity and only consulted `code` when
`key` was empty or Unidentified, so a jamo short-circuited it to false: the
press missed the ETX path and was CSI-u encoded instead, leaving a TUI running.

Trust `key` only when it is a Latin letter, which keeps a Dvorak remap of C
authoritative. Otherwise ask the layout map what the physical key produces
unmodified: an IME layered over a Latin layout answers 'c', and over a Dvorak
base answers 'j', which correctly declines. When the map is itself non-Latin it
cannot answer either, so fall back to physical position — how terminals have
always resolved control chords.

Fixes #14460
2026-08-13 23:18:08 -07:00
Brennan Benson 3035aa9211 fix(browser): restore replaced cookies through CDP identities (#14383)
* fix(browser): restore replaced cookies through CDP identities

Both remaining callers of the imported-domain replacement rolled back by
rebuilding cookies with cookies.set, which silently drops partitionKey.
The rollback in importValidatedCookies puts back the user's ORIGINAL
cookies that the import already deleted, so a CHIPS cookie came back as
an ordinary one and no restart recovered it.

Snapshot CDP identities before the first removal and undo through them,
the same machinery removeTransplantableCookies already uses. The store
type omits 'set' so the lossy reconstruction cannot be reintroduced, and
restoreImportedDomainCookies is deleted now that both callers are gone.

* fix(browser): skip the CDP rollback when nothing was replaced

restoreClearIdentities attaches the debugger before it iterates, so an
empty restore set would spin up a hidden BrowserWindow to put nothing
back. The old cookies.set restore was a no-op loop in that case.
2026-08-13 23:15:21 -07:00
NeilandOrca b9627e91a9 perf(renderer): parallelize runtime catalog and worktree refresh (#14138)
Co-authored-by: Orca <help@stably.ai>
2026-08-13 23:11:24 -07:00