Commit Graph
8978 Commits
Author SHA1 Message Date
Brennan BensonandCodex a6d4bc0380 test(structured-chat): make the stale-binding clear and the availability guard observable
The bridge-route assertion used deep equality with an explicit undefined, which
vitest cannot distinguish from an absent key, so reverting the stale-binding
clear left the suite green. canUseStructuredNativeChat had no coverage at all.

Co-authored-by: Codex <codex@openai.com>
2026-08-19 15:26:49 -07:00
Brennan Benson 0c74172fcd fix structured Codex TUI adoption 2026-08-18 20:14:30 -07:00
Brennan Benson 6b5e0b6c45 feat(native-chat): adopt Codex terminal sessions 2026-08-18 18:30:13 -07:00
Brennan Benson b33e0bfd35 feat(native-chat): drop the standalone Chat session launcher
The QuickLaunch "Chat session" entry ("Start Codex without a terminal") was
scaffolding for testing the structured path, not a product surface. Shipping it
would introduce a second way to reach chat alongside the existing terminal
toggle, and then take it away again once the toggle routes codex to the
structured path — a surface that appears and disappears for exactly the
experimental users who would notice.

Structured chat now ships dormant: the store, journal, lease and app-server
transport are all present and exercised at startup, with no user-facing entry
point. The toggle users already know (right-click a codex tab -> Switch to chat
view, plus the pane menu, the pane shortcut, and the source-control surfaces)
becomes the single door in a follow-up, once a running CLI session can be
adopted into a structured one.

`useStructuredAgentSessionCreate` stays — adoption will call agentSession.create
through it. Only the menu entry and its now-unused imports go.

Verified: 1198 renderer tests, web typecheck, oxlint clean.
2026-08-18 15:15:01 -07:00
Brennan Benson 1f5d39b58e fix(native-chat): keep the working dots up while a command runs (#15188)
The trailing "…" disappeared the moment Codex started running shell commands —
the part of a turn that actually takes time. One turn showed dots while thinking,
then went still for the length of a minute-long command, with only the tab
spinner and the Stop button left to say anything was happening.

The rule suppressed on `role === 'assistant'`. On the PTY transport a command row
arrives as a synthetic `command:` marker and hit the show branch; on the
structured transport the same work is a journal tool-call item projected as
`role: 'assistant'` with tool blocks, so it hit the suppress branch instead. The
module's header claimed the rule was "pure so the two transports can't drift" —
the logic was pure, the inputs drifted, and isCommandMarkerId was dead code here.

Suppression is now about the row's CONTENT rather than its role: a row carrying
only tool-call/tool-result blocks is progress, not the turn's answer, so it keeps
the dots. Prose still suppresses them, which is what stopped the placeholder from
reflowing the list under streamed text.

The existing tests only ever built `command:`-prefixed rows, which exist on the
PTY path alone — that is why they were blind to the shipping transport. The new
cases build rows through the real structured projection instead.

Verified: 5778 tests across renderer native-chat and shared, web typecheck,
oxlint clean. Mutation: dropping the tool-activity branch turns the new
running-command assertion red.
2026-08-17 16:28:13 -07:00
Brennan Benson 498768c4db fix(native-chat): show what a failed provider dependency actually said (#15177)
A failed MCP server reached the chat as `codex · notification:mcpServer/
startupStatus/updated` — a raw protocol method name where the provider had sent
a real sentence: "MCP client for `codex_apps` failed to start: authentication
token invalidated".

The classification was already right. The frame carries an error, so it is
correctly surfaced rather than suppressed, and a merely starting or ready server
stays chrome. The gap was in the row text: readableMessage looks at message,
text, warning, detail, description and reason, but not `error` — which is the
only field a failed dependency reports itself in. So it fell through to the
method name, which reads as protocol noise for something the user can act on
(here: their ChatGPT auth had expired).

Verified: 1728 tests across native-chat and codex. Mutation: dropping `error`
from the key list turns the new assertion red.
2026-08-17 14:50:18 -07:00
Brennan Benson 1a3837d72f fix(native-chat): give agent-session store recovery a real exit (#15174)
Creating a chat session failed permanently with "The session store refused this
call: execution_owner_reconciling" on a real profile, and no restart, relaunch
or upgrade cleared it. Two defects, one symptom.

The write path moved the live file aside before writing the new one, so any
death in that window — crash, OOM, an external kill — left a backup and no
primary. saveAgentSessionStore now makes the new content durable in a temp file
FIRST, copies the old content to the backup, and only then renames. The live
path is never absent, and the backup is a copy rather than a move.

Recovery then latched. The gate refused every transaction while the store had
loaded from its backup, but the only line clearing that flag ran after a
successful save inside a transaction the gate had already refused — and the flag
was re-derived from disk each time. No exit existed by construction. Recovery
now raises each recovered record's fence by two inside the first transaction and
continues, instead of refusing. Two, not one: isAgentSessionFenceCurrent is
strict equality, so a floor of +1 would EQUAL the highest fence the lost commit
could have granted rather than dominate it.

Ownership is untouched. claimStatus, ownerProcess and handoffStage carry forward
verbatim — a conflict must survive restart, and ownerProcess is the identity
evidence the owner probe needs, since the lease owner is a child process that can
outlive a main-process crash. Erasing it is how two writers reach one provider
session. No record, operation, or retired claim key is discarded.

Two supporting changes: a failed backup rotation now aborts the save, because the
+2 bound depends on the backup being at most one committed generation behind; and
a non-ENOENT read error on the primary no longer selects the backup, since a
transient EACCES/EIO says nothing about the primary's contents and falling back
would overwrite newer state with older.

Verified: 5062 tests across runtime and native-chat, node typecheck, repo lint.
Mutations: +2 to +1, removing the fence raise, letting a failed backup copy
continue, and dropping the read-error guard each turn a test red.

Two pinned tests changed. agent-session-record-store.test.ts asserted that the
transaction after backup recovery REJECTS — it pinned the wedge as desired
behavior. The refusal oracle's 30s budget was raised: every commit now rotates a
durable backup, so 26 real host round trips do more fsync work than that budget
was set for.
2026-08-17 14:44:54 -07:00
Brennan Benson d05819bf10 fix(native-chat): close minor silent-drop seams in the journal path (#15144)
Grouped no-silent-drop minors from the structured-core review:

- A rollout file that vanishes mid-scan no longer aborts the whole TUI
  proof scan (open() was outside its try, so one ENOENT fed the
  indeterminate-owner path).
- CodexTurnOrdinals.forgetTurn was dead code deleting a bare turnId while
  keys are thread:turn. It now releases the finished turn's per-item map
  (called on turn/completed) while keeping the counter, so a straggler
  frame can never reuse an ordinal and upsert another item's row.
- Malformed journal lines were counted and discarded; the count now lands
  in the timeline as one stable disclosure row per journal, upserted on
  reopen. The malformed lines themselves stay on disk untouched.
- Dispatch and submission rows were validated only by clientMessageId;
  epoch and item rows skipped their payloads entirely. Rows are now
  type-checked field by field (never enum-checked, to stay forward
  compatible within a row version).
2026-08-17 14:01:59 -07:00
Brennan Benson abb1e05859 fix(native-chat): actually run journal compaction in production (#15160)
`AgentSessionJournal.compact()` had zero production callers, so a session's
log.jsonl only ever grew — read in full (twice) on every open, with blobs/ never
shedding anything, until the 256MB row-byte bound refused every subsequent
append for the life of the session with a code no consumer handles.

Compaction now runs from the append path, gated on the retention window
actually having rows to shed. That gate matters: inside the window `retainTail`
returns every row, so an unconditional trigger would serialize the entire state
and rewrite an identical log on every append past the threshold. `autoCompact`
is an explicit option (default on) rather than being inferred from whether a
policy was passed, so tuning the policy cannot silently disable shedding.

This lands after the compaction-correctness fix on purpose: before it, enabling
compaction would let a reopened journal accept a lower fence and resurrect
tombstoned content.

Verified: 101 journal tests, node typecheck, oxlint clean. Mutation: stubbing
the trigger to false turns the shed assertion red.
2026-08-17 13:59:28 -07:00
Brennan Benson 4a1ebe6608 fix(native-chat): keep paged-in history on no-op tail refresh (#15091)
A window-focus tail refresh whose cursor equals the client's live cursor
carries nothing new; replacing state dropped every paged-in older item and
reset hasOlder, snapping the reader back to the newest 40 rows on each
focus. Treat an equal-cursor tail page as a no-op.
2026-08-17 13:58:36 -07:00
Brennan Benson 6260f849a7 fix(native-chat): bound history pages by bytes as well as rows (#15143)
Legal 256 KiB user messages made a 40-row history page serialize past the
4 MiB outbound channel cap; the channel closes on overflow, so a mobile
client requesting its 40-row tail before subscribing disconnected on every
reopen — an unrecoverable loop from the user's side.

Pages now keep the edge nearest the requested position within a byte
budget (half the outbound cap). Backward pages drop the oldest rows and
report hasOlder; forward catch-up shrinks the row window and reports
hasNewer, so the cursor never skips a revision. A single over-budget item
degrades to a visible truncation marker instead of overflowing — the
journal record itself is never rewritten.
2026-08-17 13:58:32 -07:00
Brennan Benson 98039f922c fix(native-chat): keep capped and error provider frames visible (#15142)
Two no-silent-drop violations in the unhandled provider frame path:

- The per-turn generic-row cap silently discarded every frame past eight.
  Capped frames now upsert one per-turn summary row with an exact count,
  and error-surface frames are exempt from the cap entirely.
- classifyProviderFrame ran the delta name-suffix heuristic before payload
  error inspection, so an unknown future delta method carrying an error
  vanished. Error inspection now outranks the name-shape heuristic.
2026-08-17 13:58:28 -07:00
Brennan Benson 81c960a137 fix(native-chat): salvage unreadable session records from the committed backup (#15149)
One malformed current-schema record in agent-sessions.json quarantined its
session forever: every ownership attempt was refused with
execution_owner_reconciling even when the .bak held a valid copy, with no
repair or import route.

The loader now adopts the previous committed copy for any session the primary
retained as unreadable. The salvaged lease is re-adjudicated like any other,
the unreadable bytes stay quarantined verbatim in unusableRecords, and salvage
only fills gaps: a stale backup copy can never clobber a readable record.
With no committed copy the session still fails closed. saveAgentSessionStore's
write ordering and the transaction queue are untouched.
2026-08-17 13:58:24 -07:00
Brennan Benson ef52608914 fix(native-chat): give latched recovery stages evidence-driven exits (#15147)
A transient failure could latch a session in recovering/manual-recovery with
every documented exit gated on tui+ownerProcess, leaving native sessions
permanently wedged behind a switching spinner:

- adjudication now releases an abandoned native reservation at restart (the
  reserving runtime is proven gone and a never-proven native child has no
  request channel left), and routes a surviving native owner to recovery
  instead of readopting a transport that died with the previous process
- a recovery resolver re-probes latched native records at attach and during
  startup restore; a live identity-matched orphan gets a stop request and is
  evicted only after a later probe proves it absent, never on a timeout
- the lease renewer no longer extends leases for native records parked in
  recovery, and the host runtime-state probe no longer fabricates
  reservation-unused for a live reservation
- codex spawn identity retries the start-time read and refuses an owner no
  probe could ever verify; on hosts that expose process environments the
  probe now reads the spawn token back as a second identity element
2026-08-17 13:58:20 -07:00
Brennan Benson 1187d94d94 fix(native-chat): require a proven exit before handing off the thread (#15162)
Native-to-TUI handoff called closeSession, but connection close stopped waiting
after a fixed forced-exit deadline even when the child never emitted 'exit' and
the kill request itself had failed. Handoff then durably recorded
`exit-observed`, advanced the fence, and launched a TUI against the same
provider thread while the app-server could still be alive and streaming — two
live writers on one Codex session.

`close()` now reports whether exit was actually observed, and the forward
handoff refuses on an unproven one rather than advancing the fence. The gate
lives at the forward call site, where the fence decision is made, so a caller
cannot bypass it by ignoring the returned proof.

Two supporting changes: on POSIX the kill reaped only the direct child, missing
the npm-shim grandchild the Windows branch's own comment warns about; and
recovered-owner termination sent SIGTERM without escalating. SIGTERM is a
request — it now escalates once and requires an independent absence probe
before the lease transition proceeds.

Verified: 2148 tests across ai-vault/codex/native-chat, node typecheck, oxlint
clean. Mutation: stubbing out the forward gate turns the exit-proof test red.
2026-08-17 13:58:16 -07:00
Brennan Benson bc42ccb4d6 fix(native-chat): refuse target-less resumes onto a structured session (#15161)
`isResumeCommandFor` checked id-containment first, so `codex resume --last` —
which carries no session id — never matched the owned session and the legacy
writer guard admitted it. A TUI then attached to the very provider session the
structured runtime owns. No race: it reproduces every time. Bare `claude
--resume`, `-r`, `--continue` and `-c` have the same shape.

Resume invocations are now parsed into (provider, target) and the guard fails
closed: a resume is refused unless it names a target provably different from the
owned session. `--continue`/`-c` never take a session id, so a trailing token is
a prompt and they are always target-less — reading it as a target would have
admitted a writer onto the owned thread.

Verified: 10 ownership tests, node typecheck, oxlint clean. Mutation: dropping
the target-less branch turns 4 assertions red.
2026-08-17 13:58:12 -07:00
Brennan Benson b544a0e1d3 fix(native-chat): stop journal recovery from destroying transcripts (#15159)
A single corrupt line took the whole transcript with it: open() set `corrupt`,
rolled the epoch, and rebuilt an EMPTY reducer state over the log, discarding
every intact message before the bad row. A torn final line was worse — the next
append concatenated onto the fragment, fsynced, and RESOLVED, telling the caller
a write had landed durably that reopen would then skip, producing the sequence
gap that triggered the wipe.

Now the readable prefix stays live and the unreadable remainder is moved aside
as a quarantine file, a torn tail is truncated (or its missing newline restored)
and fsynced before any new row is appended, and appends fsync the parent
directory too. Compaction persists `highestFence` and tombstones so a reopened
journal cannot accept a lower fence or resurrect deleted content, and retention
now covers provider-frame blob digests and rows whose render item is gone.
A future-schema snapshot degrades to read-only instead of staying writable, and
`schema_unreadable` rollover is finally reachable — it preserves the unknown
rows first rather than asserting the flag that blocks it.

Verified: 499 native-chat tests, node typecheck, oxlint clean. The multi-byte
torn-tail test goes red against a string-offset repair (truncate/write take
byte offsets, and transcripts are full of emoji and CJK).
2026-08-17 13:58:08 -07:00
Brennan Benson 041dd5f05b fix(native-chat): bring the structured core branch back under max-lines (#15168)
Static analysis was red on the branch itself, which is why every stacked PR
inherited three failing checks. Two files were over their caps, both from this
branch's own growth:

- `use-app-startup-hydration.ts` (307/300): the startup store subscription moves
  to `use-app-startup-actions.ts`. It is a separate concern from the boot chain
  and was the only reason the file needed `useShallow`.
- the mobile session route (5036/5015): the structured chat view was wired
  inline with eighteen props. It moves to `MobileStructuredSessionPane`, so the
  route passes one entry object.

No per-file cap was raised and no rule was disabled.

Verified: repo-wide oxlint clean (0 errors, 15765 files), web typecheck, mobile
typecheck, and the startup source-slice suite (25 tests).
2026-08-17 13:57:55 -07:00
Brennan Benson 735b9860f1 Merge main into the structured chat core branch
Resolves 14 conflicts. Four of them were files main SPLIT since the branch
diverged, where taking our side would have compiled green with the change
landing in dead code, so each was re-homed into the module that now owns it:

- worktrees.ts (3.2k lines -> a barrel): the agent-session visible-tab-type
  hunk moved to worktrees/listing/worktree-catalog-visibility.ts.
- worktree-activation.ts (605 lines removed): the activation gate that
  supersedes resumeSleepingAgentSessionsForWorktree was ported onto both of
  main's remaining call sites.
- App.tsx (2.7k lines removed): the two startup steps and the restoration-ready
  flag moved into app-shell/use-app-startup-hydration.ts, the sync hook into
  use-app-shell-services.ts, and the status bridge into AppBackgroundServices.
  The degraded-boot path releases the same flag so a failed startup still
  unblocks the gate.
- useTabGroupWorkspaceModel.ts (split into five hooks): the agent-session
  projection, close branches, and activation command were re-homed per hook.
- Three deleted test files (#14728 max-lines split) had their real assertions
  moved into the split files that replaced them, not resurrected.

Behavioural resolutions worth naming:
- Terminal.tsx keeps our activation gate but folds in main's
  activeWorktreeHasTerminalState guard, so a gated workspace still refuses to
  re-seed a terminal the user deliberately closed.
- The orchestration mailbox pointer moved into orchestration/mailbox-pointer-*,
  which dropped our agent-session lease enforcement on the pointer -> Enter
  span. Re-homed onto writeOrchestrationPointerPty: the admission is snapshotted
  with the pointer text and re-asserted before the submit, so a session that
  re-leases the pty during the 500ms pause no longer receives the Enter.
- dispatcher.ts widens our Pick to carry main's authenticatedCallerFingerprint
  rather than dropping either side.

Verification: three typechecks clean; 54,7xx tests pass. The IME preedit pair
fails identically on pristine main and is not from this merge.
2026-08-16 22:53:16 -07:00
Brennan Benson 61471580c7 fix(native-chat): avoid unretained summary blobs 2026-08-16 22:14:59 -07:00
Brennan Benson 4abb89a906 fix(native-chat): render bounded provider frame summaries 2026-08-16 22:14:59 -07:00
Brennan Benson 50fec5630e fix(native-chat): keep structured rows put and stop raw opcode rows
Three seams behind "the compaction and notification rows moved above the
message I'd already sent".

- The reducer pinned `sequence` across revisions but let `observedAt` advance,
  and clients sort the timeline by that timestamp. Codex echoing a send revises
  the submission row, so the user's own bubble jumped below every row that
  landed while the turn was in flight. Pin `observedAt` with `sequence`: a
  revision refreshes content, never position.
- `item:contextCompaction` reached the timeline as a raw `codex · item:<type>`
  row because the frame catalog is keyed by notification METHOD and never
  matches item-form kinds. Codex item types now get their own dispositions;
  compaction is chrome, matching the `thread/compacted` notification it
  duplicates. Undispositioned item types still fall through visibly.
- The generic fallback row named the opcode and nothing else, so a real warning
  read as `codex · notification:warning`. It now leads with the provider's own
  sentence when the payload carries one, with the raw frame still behind the
  row's disclosure.
2026-08-16 22:14:59 -07:00
Brennan Benson 6dcbf41394 fix(native-chat): keep send placeholders stable 2026-08-16 22:14:59 -07:00
Brennan Benson 793c323368 fix(mobile): preserve queued image preview on WAL adoption 2026-08-16 22:14:59 -07:00
Brennan Benson 0127d5c953 fix(native-chat): stop the send/reply reflow in structured chat
Two seams made a structured turn visibly jump. Frame-stepping a 54fps capture
of one send measured both:

- The host renders the user bubble from the submission WAL row, which lands
  while dispatch is still `pending`, but the optimistic outbox echo is only
  retired on `accepted` — so the same bubble drew twice for the whole provider
  round trip (~74ms) and then collapsed. Desktop drops the echo once its WAL
  row exists; mobile adopts the canonical row so Retry / edit-queued stay
  reachable on it.
- The trailing "…" row keyed its visibility on the PTY path's synthetic
  streaming id, which the structured path never mints, so it stayed pinned
  below the reply until `isWorking` cleared (~110ms later) — a second reflow.
  The rule moves to a pure module that scans back to the turn boundary, so a
  system row mid-turn cannot flicker the dots back on.
2026-08-16 22:14:59 -07:00
Brennan Benson 8ca4ed945e feat(terminal): report execution host and listing scope in terminal list (#14973)
* feat(terminal): report execution host and listing scope in terminal list

`orca terminal list` returned rows with no host identity and no statement
of what the listing covered, so a scoped listing that saw nothing read as
"nothing exists anywhere" — an agent reported a live remote worker dead.

Each row now carries an optional `executionHostId` derived from the PTY id
(SSH and paired-runtime ids embed their owner), and the result carries an
optional `hostScope` naming the hosts covered and the known hosts skipped.
Both are surfaced in `--json` and in the human-readable CLI output, where
an absent field renders as `unknown` rather than `local`.

Both row builders route through one resolver, so the rule lives in one place.

* fix(terminal): preserve unverifiable host scope

* fix(terminal): fail closed on unverifiable hosts

* test(terminal): name unverifiable scope explicitly

* perf(terminal): keep graph hydration host scans narrow

* fix(terminal): reject blank foreign host owners

* fix(terminal): validate inferred inventory hosts

* fix(terminal): preserve paired folder host scope

* fix(terminal): keep inventory host inference typed

* fix(terminal): disclose paired folder hosts
2026-08-16 22:13:03 -07:00
Brennan Benson b36456007e fix(repos): never probe the client filesystem for a remote repo icon (#14947)
detectRepoFileIcon fell back to a LOCAL read whenever the SSH filesystem
provider was absent, so a disconnected/not-yet-reattached remote repo whose
path also exists on the client picked up the wrong repository's icon. Thread
the connection identity through and fail closed, matching the rule already
stated in connection-context.ts and repo-default-branch.ts.
2026-08-16 22:05:46 -07:00
Brennan Benson 0ac2e77db1 fix(agent-hooks): default-form managed hook vars so a static precheck cannot reject them (#14994)
The managed hook command embedded a bare $SYSTEMROOT. Grok loads Claude's
settings.json hooks and statically prechecks env vars across the whole command
string, so the reference inside the never-taken Windows branch made it refuse
the hook on macOS on every event:

  hook not executed: required env var(s) not set: ${SYSTEMROOT}

Grok fails these open, and Orca installs Grok's native hook separately, so no
status was lost -- the symptom is a swallowed failure line per tool call.

$VAR and ${VAR-} expand identically in POSIX shells absent set -u, so this has
no execution-time effect; only the static precheck observes it. Verified in Git
Bash on Windows that both guard forms resolve identically ($SYSTEMROOT is set
there and uppercase is the correct spelling -- $SystemRoot is undefined).

Also converts the three bare $HOME references so the regression test can assert
zero bare variable references with no exemption. A $SYSTEMROOT-specific check
would not have caught this class of bug being introduced elsewhere.
2026-08-16 21:58:28 -07:00
Brennan Benson a9f93172cf Revert "fix(crash-reporting): give the parking census a retained breadcrumb slot without starving memory highwaters (#14867)" (#15005)
This reverts commit 28c93da474.
2026-08-16 21:54:44 -07:00
OrcaWinandm4air fed6a7d4fd Rethrow update errors after lineage recovery attempt (#15008)
Previously silent failures are now rethrown after recovery attempts.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-16 21:53:41 -07:00
Jinjing 84784f5393 Split pull request page (#14853)
* refactor: split PullRequestPage.tsx under 400 lines

Move the 5888-line PR page into nested domain modules under
src/renderer/src/components/pull-request-page/ and leave a thin public
barrel. No intentional behavior change.

* rm plan

* Improve React stability and remove manual ref caching

- Stabilize React keys in CheckDetailsPanel using content fields instead of array indices to prevent unnecessary remounting
- Remove manual ref-based entries cache in PRFilesCombinedDiffViewer, rely on useMemo dependency (diffEntrySignature) instead
- Move sectionsRef assignment to useLayoutEffect to avoid render-phase ref writes
- Refactor usePRFileSectionLoader to destructure args for readability

* Improve PR page stability: add error handling and fix race conditions

- Add error handling with user feedback (toast notifications) for comment submission, review comments, diff loading, and file view syncing
- Internationalize hardcoded strings for PR state labels and error messages
- Fix race condition in reviewer submission by using a ref-based guard instead of render-time state
- Fix scroll restoration to avoid overwriting target positions with intermediate clamp values
- Add effectiveRepoId parameter for proper repo context in review operations
- Disable reviewer picker during submission to prevent concurrent requests

* Improve PR page stability: add timeout and stable list keys

- Add 45s timeout for diff loading to prevent indefinite hangs
- Fix React list key generation for annotations/jobs using content-based keys with occurrence tracking
- Refactor scroll position caching to properly handle mid-restore teardown
- Replace interpolated error messages with full locale-specific strings for close/reopen actions

* Fix PR diff viewer cache isolation and list key collisions

- Changed list key generation from string concatenation to JSON serialization to avoid collisions with actual content keys
- Added host-aware scoping to diff view caches so local and remote execution don't share entries
- Optimized virtualizer keys to use lightweight revision counter instead of full serialized signature

* Extract PR file state into entry-scoped hooks

Replace manual state resets with custom hooks that automatically clear section heights and active section when switching PR entries. This prevents state leakage between files and simplifies the diff viewer component. Also validates the active section key exists before passing it to child components.

* Improve PR page error messages, accessibility, and stability

- Show actual error messages from failed operations instead of generic fallbacks
- Add aria attributes for combobox/listbox patterns and proper option identifiers
- Consolidate duplicate label/assignee update logic and fix event listener passive mode
- Memoize GitHub source runtime to prevent stale closure in checks callbacks
- Extract filled state badge tone for reuse and fix workspace attachment type
- Guard textarea shortcuts against concurrent saves and add mention query test

* Add explicit PR file content cache eviction

Extract PRFileContentRequestArgs type and create evictPRFileContentRequest
function to handle cache eviction explicitly. Call eviction on load timeout
so retries fetch fresh content. Adds tests for cache behavior.
2026-08-16 21:45:41 -07:00
Brennan Benson 1eb1ee377c fix(test): run the login-shell probe against a shell the machine has
The real-shell case hardcoded /bin/zsh. Linux CI runners have no zsh, so the
probe spawn failed, the resolver fell back to the parent environment, and the
assertion read undefined — shard 11/16 has been red on every branch stacked on
the structured-chat core, which reports no checks of its own.

Pick the first installed candidate instead and pair it with the profile file
that family sources under `-ilc`; a login bash reads .bash_profile, never
.bashrc. Both branches were run locally, so Linux keeps real coverage rather
than trading the failure for a skip.
2026-08-16 21:33:32 -07:00
Neil 71bbab72e1 fix(commit-message): keep Windows paths intact in agent command overrides (#14984)
* fix(commit-message): keep Windows paths intact in agent command overrides

`tokenizeCustomCommandTemplate` applies POSIX backslash-escape rules on every
platform. On Windows `\` is the path separator, so a native absolute path in an
agent command override is silently destroyed:

  C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
  -> C:WindowsSystem32WindowsPowerShellv1.0powershell.exe

which is then reported as not found on PATH. The agent *startup* path already
routes Windows shells to the Windows tokenizer, but the commit-message AI path
still calls the generic tokenizer directly, so overrides, extra CLI args and
custom commands there are all affected.

The tokenizer gains an explicit `'escape' | 'literal'` mode rather than reading
`process.platform`, because the same template can be parsed on one host and
executed on another. `'escape'` stays the default, so POSIX behaviour — where
`foo\ bar` is deliberately one token — is unchanged.

`'literal'` is selected only where the command provably runs on native Windows:
a LOCAL target, on win32, with no WSL distro. A WSL target runs a Linux binary
inside the distro, and a remote target runs on a host whose platform this
process cannot see; both keep POSIX escaping.

Fixes #11375

* test: pin the platform decision for literal-backslash parsing

commandBackslashMode is the only place that reads the platform, so it is where
this can be wrong in the direction that matters — applying Windows rules to a
command that will actually run under a POSIX shell. WSL and remote targets are
pinned explicitly; both were previously untested.
2026-08-16 21:12:14 -07:00
Brennan Benson 80b0ab16ff test(crash-reporting): keep ambiguous whole-tree kills reportable (#14667)
* test(crash-reporting): keep ambiguous tree kills reportable

* test(crash-reporting): drop the stale sibling-settle deferral comment
2026-08-16 21:07:33 -07:00
Brennan Benson 88b1a69824 Fix Windows horizontal computer-use scroll (#14727) 2026-08-16 20:57:11 -07:00
hwantage 8b6d0231b2 feat(i18n): localize Automations settings and navigation to Korean (#15004) 2026-08-16 20:49:59 -07:00
Brennan Benson 226cf88ba6 fix(terminal): inset the grid inside the xterm surface (#14583)
* fix(terminal): inset the grid inside the xterm surface (#13252)

Padding X/Y was applied as start-edge container margin, so the cell grid
stayed flush on the trailing edges and a fractional background opacity
stacked a darker gutter around the viewport. Put the setting on .xterm
so FitAddon insets both axes and the themed background fills the pad.

* fix(terminal): normalize padding before fit

* fix(terminal): align stored and fitted padding

* test(terminal): lock padding before fit

* fix terminal padding opacity compositing

* fix live terminal padding backgrounds

* fix(terminal): preserve source-over alpha blending

* fix(terminal): restore WebGL alpha blending

* test(terminal): complete hidden retention pane fixture
2026-08-16 20:49:45 -07:00
OrcaWinandOrcaWin 02ba70a847 fix(agent-hooks): make the Windows managed hook survive Claude-hooks-compat consumers (#14825)
* fix(agent-hooks): make the Windows managed hook survive Claude-hooks-compat consumers

`~/.claude/settings.json` is not read only by Claude Code. Third-party
Claude-hooks-compat layers (cursor-agent, Devin) import the same file and
reimplement hook execution, so Orca's entry has to survive consumers that
support strictly less than the documented schema. Three separate defects
came from assuming otherwise.

1. The entry depended on `args`, which a compat consumer ignores.
   `args` is valid Claude Code syntax, but cursor-agent spawns `command`
   alone -- so `conhost.exe` ran bare, which opens an interactive console
   that never closes. Hook payloads were typed into those stranded shells
   (#14815). The entry is now one self-contained `command` string that
   depends on nothing optional.

2. `conhost.exe --headless` never relayed anything. It implements the
   ConPTY server protocol, not a generic no-window wrapper: it does not
   wait for the hosted process and relays neither exit code nor stdout.
   Measured directly -- `conhost --headless cmd /c "echo X& exit /b 42"`
   yields empty stdout and no exit code, while the replacement returns
   both and waits. So every hook was fire-and-forget, and whatever it
   printed was discarded. Replaced with `-WindowStyle Hidden`, which
   suppresses the window and keeps wait/exit-code/stdout intact.

3. The hook never wrote anything to stdout. Guards exited silently and
   curl's output went to nul. Claude Code documents empty stdout as "no
   decision", but cursor-agent treats PreToolUse as a permission gate,
   fails to parse empty stdout as JSON, and blocks the tool call -- so
   every shell command in every cursor-agent session on Windows failed
   (#14818). The script now writes `{}` first, on both the Windows and
   POSIX branches, which is documented to be identical to writing nothing
   for real Claude Code. Gemini and Antigravity already did this.

Defects 2 and 3 are causally linked: `{}` cannot reach any consumer while
conhost is swallowing stdout, so neither fix works without the other.

Also fixed while establishing the contract:

- The launcher's own missing-script fallback returned empty stdout,
  reproducing #14818 whenever `~/.orca` was cleaned or an install was
  half-finished. It now emits `{}` too.
- PowerShell serializes progress records to stderr as CLIXML when stderr
  is redirected; a consumer merging stderr into stdout would see those
  bytes before the JSON. Every encoded payload now silences progress.
- `runtime-home-hook-command.ts` built its own launcher without window
  suppression -- exactly the drift #14815 asks to prevent. All launcher
  construction now goes through `windows-powershell-hook-launcher.ts`, so
  the switch list cannot be present in one installer and missing in
  another.
- Renamed `usesWindowsHeadlessHook` to `usesWindowsPowerShellLauncher`;
  nothing is headless anymore, and the flag selects a launcher.

Testing: the new regression test asserts the effect a consumer observes
-- it runs the exact `command` string from settings.json through both
cmd.exe and Git Bash, across the guard-exit, reached-curl, and
missing-script paths, and parses stdout. Verified it fails when
`conhost --headless` is reintroduced. The previous tests all asserted
installer intent, which is why they passed through all three defects.

* fix(agent-hooks): close hook launcher review gaps

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-16 20:48:26 -07:00
Brennan Benson 5e9e38fa75 fix(agent-status): announce Claude turn complete while background work runs (#14580)
* fix(agent-status): announce Claude turn complete while background work runs

Lead Stop/StopFailure already ends the turn, but resolveClaudePaneState keeps
the pane working for subagents, background shells, and session crons. That
erases the working→done edge that mints the completion banner: subagent turns
notify late with a stale body, and shells/crons never notify at all.

Stamp turnCompletedAt on the gated lead Stop, announce immediately from that
row, and pair the later all-clear done to the same end time so it cannot
double-fire or collapse consecutive turns onto the pinned stateStartedAt.

Fixes #13245

* fix(agent-status): suppress stamped turn replays

* fix(agent-status): notify paired clients at turn end

* Fix late-paired completion notification arming

* test(agent-status): make the notification-id test fail on the pre-fix ordering

The stored row inherited the helper's default codex agentType while the event
named claude, so agentSnapshotMatchesExplicitTitle dropped it and
freshStoredAgentStatus was undefined — the assertion held under either side of
the `??`. Name the stored row's agent so the pinned working row survives and
the snapshot-first precedence is what the test actually pins.

* Suppress stamped completion tail replays

* Prevent cross-coordinator title replays

* Preserve stamped tails across fallback signals

* Keep remount replay state while sibling lives

* Scope OSC turn stamp preservation

* Forward paired host completion stamps

* Deduplicate paired completion tails

* Bind paired completion tails to their turn

* Preserve paired completion tail ownership

* Keep paired tail replay state across remounts

* Seed paired recovery without replaying completions

* Seed startup replay and release stale fallback dedupe

* fix(notifications): preserve stamped OSC repaints

* fix(notifications): retain paired client turn boundary

* fix notifications module import safety

* chore: keep main integration focused

* fix: preserve completion re-enable boundary
2026-08-16 20:47:02 -07:00
Brennan Benson e16a22ef58 fix(browser): return a real PermissionStatus from the query override (#14684)
* fix(browser): shadow PermissionStatus state instead of proxying it

Rebased onto main after #14685 landed in the same file. Both changes coexist:
#14685's Firefox gating and narrowed promptPerms are preserved untouched, and
this change replaces only the query-override implementation.

The previous Proxy bound every callable property, which broke three observable
things:
- onchange assignment threw "Illegal invocation" — the native setter received
  the proxy rather than the branded target.
- Listeners were delivered with the real target and the NATIVE state, so on a
  real permission change event.target.state read 'granted' while the returned
  status still read 'prompt'.
- Method reads returned a new bound function each time, so name became
  'bound addEventListener', toString lost its name, and identity was unstable.

Separately, the notifications state was captured once as a string, so an
already-returned status went stale after requestPermission updated it.

Shadow only 'state' on the genuine PermissionStatus with a lazy provider. The
object the site holds IS the real one, so identity, brand checks, method
fingerprints and native event delivery survive with nothing to keep in sync.

Verified against real Chromium (Chrome 151) rather than only the vm stand-in,
because the stand-in cannot show whether defineProperty succeeds on a branded
instance: the instance is extensible, the prototype 'state' accessor is
configurable, defineProperty succeeds, instanceof survives, addEventListener
keeps its native name and referential stability, and onchange assignment works.

New tests live in their own file rather than merged into anti-detection.test.ts,
whose harness diverged on main. The fallback test targets 'camera' because
#14685 narrowed the intercepted set, so a name outside it never reaches the
fallback.

* test(browser): cover intercepted PermissionStatus identity
2026-08-16 20:35:34 -07:00
OrcaWinandOrcaWin 378c60071a fix(grok): strip quotes from GROK_HOME in the Windows hook (#14221) (#14985)
`setx GROK_HOME "C:\path\"` stores `C:\path"` — the CRT turns the `\"` into a
literal quote. That quote unbalanced the trailing-backslash `if` operand, so
cmd aborted grok-hook.cmd with exit 255 before curl and every Grok hook event
failed. A quote elsewhere in the value closed curl's `grokHome=` argument
early, swallowing the `^` continuation and dropping the `payload@-` line.

`"` is illegal in a Windows path, so strip it during the copy.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-16 20:33:37 -07:00
Jinwoo Hong b6d5972ec4 fix(mobile): reland truthful Relay recovery status (#14986) 2026-08-16 19:09:15 -07:00
Brennan Benson 21ed09e45c Bump mobile app.json to 0.0.44 (#14983) 2026-08-16 18:51:02 -07:00
Brennan Bensonandmanuaudio 886dec1d2a fix(browser): report cookies an import could not decrypt (#14683)
* fix(browser): report cookies an import could not decrypt

Supersedes #13193, which reported only the Windows v20 case.

Nothing distinguished "decryption failed" from "no cookies present". A row that
would not decrypt was folded into the generic `skipped` counter, and a profile
whose rows all failed returned ok:true with importedCookies:0 and no warning —
a green "Imported 0 cookies from Google Chrome." The two situations produce
opposite result shapes and the worse one reported success.

Attribute the cause at the point of failure, while the version prefix is still
in hand, and surface it as one `cookies-undecryptable` warning carrying the
reason. Covers all three known causes rather than one prefix:

- app-bound-encryption: Chrome/Edge 140+ on Windows write `v20`, which only the
  writing browser can unwrap. The version gate is a FORMAT check (`/^v\d\d$/`),
  so v20 passed it and failed inside AES like corruption.
- linux-keyring-unavailable: getLinuxEncryptionKey derived the v11 key from an
  empty password when both secret-tool lookups failed, so it never returned null
  and the "Could not access encryption key" guard was unreachable on Linux.
- unknown: any other cause still warns instead of reporting success.

Deliberately not a hard failure on Linux: Chrome falls back to the "peanuts" v10
key precisely when no keyring exists, so those profiles still import. Pinned by
a regression test.

Refs #13192, #14181

* fix(browser): attribute decrypt failures exactly and gate CBC by version

Review-loop findings on the initial commit, all fixed here.

- CORRECTNESS: v11 rows were attempted with the v10 key when the keyring was
  unavailable. AES-128-CBC is unauthenticated, so a wrong key that yields valid
  PKCS#7 padding was accepted — roughly 1 in 256 per row. Garbage values were
  written into the jar as real cookies, and because those rows counted as
  successes the warning this PR adds could never fire. Key eligibility is now
  explicit per version rather than implicit in key ordering.

- CORRECTNESS: the CBC path returned an empty Buffer for a prefix-only value
  BEFORE checking eligibility. An empty Buffer is truthy, so an ineligible row
  counted as imported and reached the live-jar clear. Eligibility now precedes
  that branch and empty CBC ciphertext is rejected as malformed.

- ACCURACY: a named cause reported the TOTAL failure count, so one v20 row plus
  one corrupt row claimed both failed to app-bound encryption. Counts are now
  exact per cause, with the remainder reported separately and a tie falling back
  to 'unknown'. Exact-count approach carried over from #13193.

- The app-bound copy no longer dead-ends. It names the existing in-app file
  import without describing how to produce the file — Chrome has no native
  decrypted-cookie export, so concrete guidance would send users to an
  extension that can read their whole session jar.

- Direct prefix edge tests carried forward from #13193.

Repo-wide search found no second multi-key unauthenticated-CBC first-success
site, so this pattern was one occurrence rather than a class.

Co-authored-by: manuaudio <manuaudio@users.noreply.github.com>

* fix(pr): preserve split worktree slice

Remove the unrelated rollback of the worktree-slice split and its forbidden max-lines baseline addition from this cookie-import PR.

* fix(browser): match the unknown decrypt reason explicitly

CI's type-aware code-quality gate flagged the reason switch as non-exhaustive:
the 'unknown' member was handled by `default:` rather than matched.

Matching it explicitly keeps the behaviour identical today and makes the gate
enforce the thing that matters — adding a new reason to the union now fails the
switch instead of falling silently into a generic message that would not
describe it.

This gate is separate from `oxlint` and is not covered by running oxlint on the
changed files, which is why it only surfaced in CI.

---------

Co-authored-by: manuaudio <manuaudio@users.noreply.github.com>
2026-08-16 18:43:11 -07:00
Brennan Benson 5e189d6081 feat(browser): add a WebAuthn account picker (#14687)
* fix(browser): prompt for WebAuthn account selection

* fix(browser): scope WebAuthn cancellation to session

* fix(renderer): keep WebAuthn render phase pure
2026-08-16 18:32:20 -07:00
Neil 96b03bab4f fix(terminal): align Cursor Agent IME preedit anchor (#14982) 2026-08-16 17:18:24 -07:00
Brennan Benson a324ee20d4 Reset terminal SGR state around restored output (#14700)
* fix(terminal): reset SGR around restored output

* fix(terminal): preserve live replay styling

* fix(terminal): ground dead reattach fallback
2026-08-16 17:08:10 -07:00
Brennan Benson c73e5a2f59 fix(mobile): bound pending-handle session-tab recovery polling (STA-4407) (#14916)
* fix(mobile): bound pending terminal recovery polls

* wip(mobile): partial STA-4407 bound pending-handle poll

* fix(mobile): finish bounded pending-handle recovery

* fix(mobile): preserve pending-handle recovery attempts on slow links

* fix(mobile): retain pending-handle cadence budget semantics

* test(mobile): pin pending recovery parked state resets

* chore(mobile): drop the STA-4407 worker status logbook

* fix(mobile): preserve pending recovery liveness

* fix(mobile): coalesce repeated recovery retries

* fix(mobile): memoize pending recovery context

* fix(mobile): type the pending recovery poll test renderer explicitly

* fix(mobile): type the poll test renderer without an any union

* fix(mobile): keep recovery context refs current

* test(mobile): tighten pending recovery coverage

* test(mobile): preserve recovery-source liveness

* fix(mobile): keep the poll test renderer union free of any

* test(mobile): prove parked recovery isolation

* fix(mobile): write the parked-recovery callback ref after commit

* test(mobile): isolate recovery identity changes

* test(mobile): prove recovery publication boundaries
2026-08-16 17:00:49 -07:00
Neil 85565a9302 reland(workspace): set project location from the create-worktree host picker (#14965)
* feat(workspace): reland set project location from the create-worktree host picker

Relands #14868 (reverted by #14912) with a fix for the regression that caused the
revert: setting a project location could change the path before Orca used it.

The retarget-after-setup path read the raw store record to find a just-created
setup, because the memoized picker options had not refreshed yet:

    useAppStore.getState().projectHostSetups.find(
      (candidate) => candidate.id === setupId && candidate.setupState === 'ready'
    )

That hand-rolls a second selection path that skips every rule the option builder
applies — repo eligibility, ephemeral-VM and runtime-owned SSH host exclusion,
and the one-setup-per-host dedupe whose own comment notes that
resolveWorkspaceCreationTarget takes the first project+host match and ignores the
rest. So the composer could be retargeted at a setup other than the canonical one
for that host, pointing creation at a different location than the one chosen.

Resolves through buildProjectHostSetupOptions against fresh store state instead,
so the fallback and the steady-state picker agree by construction.

STA-4547

* fix(workspace): sanitize the clone prefill and drop an abandoned set-location

Review follow-ups on this PR.

The "Clone from URL" prefill seeded the field with the verbatim `git remote` URL,
which can embed a PAT (`https://x-access-token:ghp_...@github.com/...`). The
clone then runs on the *target* host, writing that token into its .git/config —
a credential the user never typed into this flow, now readable by anyone on a
shared host. Strip it with the same sanitizer `getProvisionedRootRecipeRepoUrl`
already applies to the ephemeral-VM recipe URL. Extracted to
resolveProjectCloneUrlPrefill so the rule is directly testable.

The dialog also stays dismissable while a submit is in flight, and an SSH clone
is unbounded. A clone the user backed out of minutes earlier still called
onReady, silently moving the run target and resetting start-from under a form
they had since pointed at another host. Drop the result if the dialog went away.

* fix(workspace): re-arm the abandoned guard on mount

StrictMode runs mount/cleanup/mount, so latching `abandoned` on the first
cleanup left it true for the rest of the session and permanently suppressed
onReady — the app wraps its root in StrictMode. Reset it on mount.
2026-08-16 16:50:58 -07:00
Neil 3f58d5cf9a fix(daemon): bound cwd validation per UNC route (#14967)
Async cwd validation dedupes by exact path but had no concurrency bound, and a
dead UNC share answers `stat` in ~21s while holding one of libuv's 4 default fs
threads. Four distinct paths on one unreachable server therefore starved every
other async fs read in the daemon — including the cold-restore history replay
running alongside them — which moves the head-of-line stall #14848 removed from
the event loop into the thread pool.

Adds a per-route lane of 2, reusing PrioritySemaphore and matching the per-distro
lane in rate-limits/auth-filesystem-operation.ts. Keyed by the host that has to
answer (WSL distro, or the `\\server` prefix) so many dead subdirectories of one
share fold into one lane. Local-disk paths bypass the lane entirely: a global cap
would queue a healthy local spawn behind a dead share.

Moves PrioritySemaphore to src/shared. It has no imports, and reaching into
src/main/daemon from src/main/providers inverted the dependency direction that
already runs daemon -> providers.

Note this bounds pool occupancy, which cancellation cannot: an aborted `stat`
still holds its libuv thread until the OS returns.

STA-4543
2026-08-16 16:50:26 -07:00