Commit Graph
8831 Commits
Author SHA1 Message Date
Brennan Benson 7fad71e448 fix(worktree): skip retirement backfill on every non-local host (#15023)
The backfill guard tested repo.connectionId, but a runtime-owned repo
carries executionHostId with no connectionId, so it read as local. The
scan then walked this machine's workspace and agent-transcript
directories and filed the result under the runtime host's namespace —
retiring names never used there while missing the ones that were.

Guard on the execution host id instead. Ongoing retirement was already
correct for these repos; only the one-time historical seed was wrong.
2026-08-17 01:23:08 -07:00
Brennan Benson 1aa51f6914 fix(computer): preserve accessibility value types (#15031)
* fix(computer): preserve AX value types

* fix(computer): preserve exact integer values

* fix(computer): preserve exact integer exponent forms
2026-08-17 01:20:31 -07:00
Brennan Benson c7995a66ae fix(mobile-native-chat): reland glued pending retirement without the two revert causes (STA-4482, STA-4492) (#14936)
* fix(mobile-native-chat): reland glued pending retirement without the two revert causes

Relands #14665 (reverted by #14819). #14665 retired mobile pending bubbles when
two fast sends landed as one transcript row, but shipped two regressions; both
are fixed here rather than re-applied and hoped for.

1. A rejected send restored a TRIMMED composer. #14665 reassigned `text` to
   `text.trimEnd()` at the top of `sendMessage` and then used that one value for
   both the bytes on the wire and the composer restore, so a rejection put back
   less than the user typed. The draft and the payload are now separate values:
   `draftText` is what the user typed and is what `clearDraftForSend` /
   `restoreRejectedDraft` see; only the transported `text` is trimmed.

2. Sends issued during hydration were stranded forever. #14665 persisted
   `glueBaselineTrusted: false` on any send captured while the transcript was
   still loading and never cleared it, so that send could never retire and stood
   as a permanent glue barrier for its neighbours. A hydration-time baseline is
   now a placeholder (`baselineResolved: false`) that the first authoritative
   read rebases onto real rows, ordinals included, instead of a permanent
   disqualification. That is STA-4492.

The intended behavior is unchanged: one transcript user turn retires a run of
2+ adjacent text-only pending sends only when it exactly spells their normalized
concatenation, every send is bounded by its OWN transcript tail, and exact
landings, image echoes and unresolved tails stay barriers.

No wire change: `baselineResolved` and the baseline tail are client-local React
state in `pendingBySession` and are never exchanged with a host. The only
client->host difference is trailing whitespace no longer being written onto the
agent's input line, over the existing `terminal.send` params.

Refs STA-4482, STA-4492. Original PR #14665, revert #14819.

* fix(mobile-native-chat): let the untrimmed draft reach the send seam

The composer sent `value.trimEnd()`, so the raw draft never reached
`sendMessage` and a rejected send still handed back a trimmed composer —
the split of `draftText` from the transported `text` had nothing to
restore. Pass the draft through; the seam already owns the wire trim.

Also pins the array-identity contract of
`retireLandedMobileNativeChatPending`: the drafts effect early-outs on
`next === current`, and nothing tested it.

* docs(mobile-native-chat): name the hydration rebase's residual ambiguity

* fix(mobile-native-chat): stop the hydration rebase stranding a send on its own echo

Rebasing recounted the send's ordinal against the first authoritative read.
That read can already carry the send's own echo — a re-subscribe after a tab
switch or reconnect returns whatever exists now — so the ordinal landed one
past anything the transcript could supply. The bubble never cleared, it stayed
a live segment at the head of its run so no later pair could glue either, and
`earlierOutstanding` carried the inflation onto the next send of the same text.
Only the tail needs recovering; the ordinal was already counted against an
empty transcript, which is right for "no history was known". A caption-less
image echo keeps its captured tail, since it counts turns after it.

`baselineResolved` also has to mean "captured against a settled read", not
merely "not loading": a read that failed hands back an empty list that reads as
an empty conversation, and the null tail then let any row the successful read
finally brought glue-retire those sends.

* test(mobile-native-chat): pin that a resolved hydration send leaves its run glue-capable

A held send sits as a live segment at the head of its run, so the cursor can
never reach a later pair — the stuck bubble takes the whole feature down with
it. Goes red against the ordinal recount.

* fix(mobile-native-chat): pin an image echo that captured no tail, and require the settled flag

A caption-less image echo keeps its captured tail because it counts image turns
after it — but a send issued before any history was known captured null, which
counts from the top of the transcript. An old image turn then claimed the send
and bound the user's fresh photo to it, leaving the just-sent turn with no
preview. A null tail is not a boundary worth preserving, so pin those too.

`transcriptSettled` was optional and defaulted to the gate it replaced, so any
caller that omitted it silently got the pre-fix behaviour. Required now, and
threaded through every harness.

* fix(mobile-native-chat): stop an unbounded send claiming an image turn already in the read

The image-preview pass runs before the rebase, so a send captured with no
boundary matched any image turn the settled read carried — binding the user's
freshly attached photo to an old one and retiring the bubble through
landedImagePendingIds, which short-circuits the retirement path entirely.
Pinning the tail in the rebase could not help: the claim was already made.
Such an entry now waits one tick and claims against a real tail.

* fix(mobile-native-chat): never move a boundary the send already captured

An unsettled read still shows this session's own retained history — a reconnect
or a failed read keeps the conversation on screen rather than blanking it — so
sends made across one already own a correct tail. The rebase overwrote it with
the tail of the read that followed, which sits at or after their own glued row,
so `turn.index <= segment.tail` rejected every turn and the pair stayed queued
for the session, blocking every later pair in the run. Pin only a send that
captured no tail at all.

A captioned image echo is now left alone entirely: it binds its preview by an
ordinal counted over the whole transcript, so supplying a tail without
recounting left it matching nothing, forever.

* fix(mobile-native-chat): supply a boundary only to a text-bearing send

An image echo reconciles by counting turns AFTER its tail and has no other
retirement path, so the tail supplied from a read that already carried its own
echo excluded the very row it was waiting for: the "Queued" photo bubble stuck
for the life of the session and the transcript row rendered as bare marker text
with no photo. A regression against main, and against the earlier revision of
this fix that pinned only captioned echoes.

The glue matcher is the only consumer a supplied tail helps. Everything that
reconciles relative to its own tail keeps whatever it captured.

* fix(mobile-native-chat): stop one unmatchable send freezing glue for the session

The match cursor only advanced on a hit, so a head that could never match —
a pair whose glued row arrived with the read, or a send the count pass claimed
against an older row — froze the run behind it and every later rapid pair
became permanently unretirable. Two cases previously disclosed as bounded were
not bounded at all. Slide past a non-matching head, keeping the cursor
monotonic so a later turn can never take a send an earlier one claimed.

The slide widens the search, so a span cap keeps the work linear in the run
length instead of quadratic; the existing budget test now asserts that bound
rather than the old one it silently broke. Re-fuzzed at 250k seeds: the
boundary guarantee still holds.

Also corrects a comment that claimed the preview-pass filter made a photo claim
against a real tail. It does not — an image echo keeps whatever tail it
captured, so a caption-less photo can still bind to an older photo turn, as on
main.

* fix(mobile-native-chat): stop the span cap stranding a long glued run

Capping each match attempt at 8 segments did not truncate a longer glue, it
rejected it outright: a row spelling 9+ sends exhausted the loop without
reaching the end of the text and returned zero, so none of the nine retired —
and each stuck send then inflated `earlierOutstanding` for the next send of the
same text. Nothing bounds how many sends pile onto the agent's input line;
accumulation ends when the agent accepts input again, not at any fixed count.

One inspection budget now covers the whole slide instead. The first attempt
spans the entire run and always fits, so a genuine glue is never truncated;
only a run of identical prefix-matching sends can exhaust the budget, which is
exactly the case that should be cheap. The in-flight attempt may overshoot the
remainder — that is what makes the guarantee hold — so the budget test asserts
the real ceiling. Re-fuzzed at 250k seeds with runs past the budget.
2026-08-17 01:11:24 -07:00
NeilandBaeTab 47a53b694b fix(ui): ignore IME composition Enter in the comment composer and replace field (#15057)
The keydown that exits a CJK composition carries isComposing: true (UI Events
3.6.5, row 5) and, when the IME is processing key input, keyCode 229 (7.3.1).
Two handlers acted on it:

- right-panel-comment-composer: Cmd/Ctrl+Enter posted the comment while the
  last syllable was still composing, so it went out truncated.
- RichMarkdownSearchBar: Enter ran replace-current, mutating the document from
  a keystroke aimed at the candidate list; Escape closed the bar instead of
  letting the IME cancel the composition. Same in the find field.

Guard all three with the existing isImeCompositionKeyDown helper, matching the
rename and title inputs already on it. The held modifier does not change
ownership: a composing Ctrl+Enter is still the IME's keydown, which is why
useImeEnterGestureOwnership also owns it and only lets the post-compositionend
redispatch chord through.

Co-authored-by: BaeTab <bhwoo48@gmail.com>
2026-08-17 01:05:35 -07:00
Neilandyeongjunyoo 876e5b88a4 fix(ime): scope the composition route and deferred newline to owned sessions (#15056)
Two ways a terminal composition went wrong, both from state that was not
scoped to the session it belonged to.

The route called preventDefault() before checking whether it owned the
session. The patched xterm treats that cancellation as "someone else will
deliver this" and skips its own triggerDataEvent, so a route installed
mid-composition — the connection effect re-runs, a reconnect swaps the
transport, StrictMode remounts — suppressed the insertion and then returned
without delivering anything. The commit vanished. Ownership is now decided
first; preventDefault stays for sessions the route does own, including the
ones it deliberately drops after a transport swap, since that drop is its
decision to make.

Pending-composition state was a bare per-element count, so a waiter could
only ask "is anything composing", not "is what I was waiting for still
composing". A composition the user starts after pressing Enter is behind
that Enter, not in front of it, but it kept the count non-zero and held the
newline anyway — `한` Enter `글` reaching the terminal as `한글\n`. The count
is now reference-counted per session id, and the deferred send snapshots the
sessions open when it starts and waits only for those. Reference counts
rather than a set, so two overlapping routes owning the same session cannot
clear each other's pending state.

Co-authored-by: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com>
2026-08-17 01:05:31 -07:00
Brennan Benson 6387e5b8d3 fix(folder-workspaces): keep the broken-folder marker when a host sends a new reason (#15027)
FolderWorkspacePathStatus is cast, not decoded, off the runtime RPC wire --
runtime-rpc-envelope declares result: z.unknown(), so unwrapRuntimeRpcResult hands
back whatever the host sent. Both title and description switch on status.reason with
no runtime guard, so a newer host publishing a fifth reason matched nothing and
returned undefined. FolderPathStatusIndicator's `!title` check then dropped the whole
indicator, and a broken folder workspace rendered as healthy -- worse than the blank
toast #15002 fixed, because there the warning was empty and here it is gone.

Guard before each switch, the shape #15002 landed. A default: arm is not available:
the type-aware config sets allowDefaultCaseForExhaustiveSwitch:false and rejects one
with switch-exhaustiveness-check.

Extract that guard into isHandledWireDiscriminant instead of hand-writing a third and
fourth copy, and move #15002's two bespoke guards onto it. It takes unknown and checks
typeof before Object.hasOwn -- hasOwn coerces its key, so a host that widened the field
to an array sends ['missing'], which a hasOwn-only guard admits before the switch drops
it straight back out. That was the P1 found in review on #15002; one implementation
makes it structural instead of tribal.

An unrecognized reason gets its own copy rather than reusing 'unavailable'. The
unavailable remedy -- "Check the runtime or SSH connection and try again" -- is a false
lead here: the host did check and reported the folder unusable, so retrying and
inspecting a healthy connection wastes the user's time. Update Orca is the real remedy.

Adding a fifth reason still fails typecheck in two places: TS2741 on the Record and
TS2366 plus switch-exhaustiveness-check on both switches.
2026-08-17 01:01:28 -07:00
Jinwoo Hong 7c798907c5 fix(skills): harden cross-host bundle installs (#15000) 2026-08-17 00:55:04 -07:00
SebastianandBrennan Benson 04e7f5c805 fix(cli): relativize absolute POSIX paths against UNC worktree roots in WSL (#11406)
* chore: ignore worktrees directory

* fix(cli): relativize absolute POSIX paths against UNC worktree roots in WSL

* fix(cli): prevent double-prefixing UNC paths in WSL path normalization

* fix(cli): gate the WSL path rewrite on a UNC worktree root

WSL_DISTRO_NAME is also set for a plain Linux CLI inside the distro, where
worktree roots are POSIX; prefixing there stranded every absolute path.
Rewrite only when the root really is a WSL UNC path, and cover the legacy
wsl$ alias, cross-distro paths, and the non-WSL case.

* test(cli): pin WSL_DISTRO_NAME absent for the whole file-path suite

Contributors run this suite inside WSL, where the inherited distro name
would flip the rewrite on for every POSIX-root case.

* fix(cli): never rewrite a Linux path that contains a backslash

Backslash is a legal Linux filename character but a separator once the
path reads as UNC, so `a\b.ts` relativized to `a/b.ts` — a different
file. Such a path has no UNC spelling; let it fail the match instead.

* test(cli): pin the WSL rewrite's negative space

Sibling-prefix roots, Linux-tail case sensitivity, and Windows
drive-letter workspaces all passed only by construction.

* test(cli): pin the distro-case fold from the CLI side

The negative-only case passed identically with the fold broken.

* fix(cli): name the WSL distro from the invocation cwd when the env is absent

WSL_DISTRO_NAME only reaches the CLI if interop forwards it across the
PowerShell bridge, which nothing in the launcher guarantees. ORCA_CLI_CWD
is set explicitly and its UNC form already names the distro.

* test(cli): match the launcher's real cwd spelling and fix an over-claim

wslpath -w emits a backslash UNC path; the fallback test now uses that
shape. The aliasing test's comment described a state the || guard makes
unreachable.

* refactor(cli): spell the WSL rewrite with the shared toWindowsWslPath helper

src/shared/wsl-paths.ts already owns "absolute Linux path in a known distro
-> its Windows form" and has five production callers; the handler hand-rolled
a fourth copy of the UNC template. Behavior is identical under the UNC-root
guard, and passing distro as a real argument makes the null check a compile
error rather than an untested branch.

* test(cli): drop a WSL case that pins the guard shape and kills no mutant

Deleting the distro null check left the case green — it asserts the same
passthrough as 'does not rewrite when the CLI is not running under WSL'. The
check is now enforced by the compiler instead.

* chore: keep WSL path fix scoped

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-17 00:52:38 -07:00
Jinwoo Hong 30b76e6edb fix(orchestration): enforce task dispatch state invariant (#14961) 2026-08-17 00:52:10 -07:00
Neilandrayim 453237cc57 fix(terminal): render the row tail the IME preedit overlay covers (#15014)
* fix(terminal): render the covered row tail inside the IME preedit overlay

Closes #12545.

Composing mid-line hid the character at the cursor for the whole composition.
The preedit overlay is an opaque box anchored to the cursor cell, and nothing
reaches the pty while composing, so those cells still held their characters —
the box simply covered them.

`CompositionHelper` now draws the rest of the row after the preedit inside the
view, so the composition reads as inserted text pushing the tail right. Four
details come with it:

- The view is start-anchored while it carries a tail, so the preedit stays put
  and the pushed tail clips at the right edge; alone, `rtl` still keeps a long
  preedit's end in view.
- It is themed from `options.theme` instead of the stock `#000`/`#FFF`, with any
  alpha dropped — the view masks the cells it draws over, so a see-through
  background would re-expose the very characters the tail stands in for.
- The helper textarea syncs to the preedit's own bounds, so IME candidate
  dialogs anchor to the composing text rather than past the rendered tail.
- A TUI can repaint the row under an open composition, so
  `updateCompositionElements` — which already runs on every render — re-reads
  the remainder and re-renders on change. A string compare adds no layout read.

The tail is read with an explicit end column: the cacheable form of
`translateToString` arms the line string cache's self-renewing idle-clear timer,
and the composition path must own no timers.

Geometry is not the cause. Two mature reference terminal implementations compose
marked text into the grid rather than into a floating box, and both still blank
the cells under it — one of them literally substitutes the marked characters
into the row's character array before rasterizing. Moving off the overlay would
not have fixed this report; rendering the covered tail is what does.

The e2e arm asserts the invariant an opaque overlay owes the grid: it must
render every committed cell its bounding rect covers. That is measured from the
real rect against the real cell grid, so it fails on the unfixed build with
`covers "하" / renders "가"`.

Known limitation: the rendered tail is plain-styled while composing (theme
foreground on theme background, no per-cell colors); colors return on commit.
This is inherent to the overlay, and drawing the preedit into the cell renderer
instead would be a far larger change.

Co-authored-by: rayim <rayim@fxy.global>

* test(e2e): assert the occlusion invariant, not the runner's cell width

CI covered four columns where this machine covers two — 34.4px over an 8.43px
grid against 12.3px over an 8px grid — so pinning the covered text verbatim
pinned the font metrics rather than the behaviour. Assert instead that every
committed cell the overlay covers appears in what it draws, which is the actual
invariant and holds at any cell width.

Still fails against main: covers "하" / renders "가".

* fix(terminal): keep the rendered tail's spacing on the grid

The composition view is white-space: nowrap, which collapses runs of spaces
exactly like normal — it only suppresses wrapping. So a committed tail carrying
padding drew its trailing glyph cells left of where the grid has them: measured
in Chromium with xterm's own rule, twenty spaces plus a border rendered two
cells wide instead of twenty-one.

The visible case is Orca's most common IME context — composing inside an agent
TUI input box, where the row is a prompt, padding, then a real border glyph the
trim cannot drop. A stray border appeared a cell after the preedit while the
real one stayed put.

xterm sets white-space: pre on its grid rows for this reason; the view was only
nowrap-safe while it held preedit text alone.

The existing fixtures are all space-free, and the e2e invariant is that the
overlay renders everything it covers — collapsing makes it cover less, so both
stayed green. Pinned with a padded-row fixture.

---------

Co-authored-by: rayim <rayim@fxy.global>
2026-08-17 00:18:11 -07:00
Brennan Benson 8e9b5c908c fix(github): fail closed instead of running client git against a remote repoPath when the SSH provider is unregistered (#14945)
* fix(github): fail closed when the SSH git provider is gone

getCurrentHeadOid and probeTrackedUpstreamBranches only routed through the
SSH provider when one was registered. With connectionId set but the provider
unregistered (dropped connection, not yet reattached) they fell through to
client-side git with cwd pointing at the remote repoPath — on a machine with
a same-named local path that silently answers for the wrong repository.

getCurrentHeadOid feeds shouldHideMergedImplicitPR, so a wrong OID changes
which PR the UI attributes to a worktree.

Both now take their existing unknown path (null / probeFailed) instead,
matching repo-default-branch.ts. Local and WSL routing is unchanged.

* fix(github): preserve PR state when SSH probes fail

* fix(github): keep failed SSH discovery unverifiable

* fix(github): propagate SSH identity failures

* fix(github): scope verified SSH identity probes

* test(github): preserve tolerant resolver calls

* fix(github): preserve indeterminate auth discovery

* fix(github): isolate SSH repository probe generations

* test(github): expose SSH probe generation in mocks
2026-08-17 00:12:14 -07:00
Brennan Benson 7afce2ea41 fix(ssh): stop reporting a confirmed kill when the SSH provider is gone (#14977)
* fix(ssh): stop reporting a confirmed kill when the SSH provider is gone

A detached relay PTY is designed to outlive the provider that addressed it
(it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider
is no longer registered" is lost contact, never evidence the remote process
stopped. Both stop primitives in the PTY controller returned `true` from
that branch, and every caller downstream reported the fabricated success:
the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped,
and — because the stop "succeeded" — the unstopped-PTY gate never ran, so
worktree removal walked straight past a live remote agent.

`kill`/`stopAndWait` now still tombstone the local lease but report an
unconfirmed stop and record why, using the three-verdict vocabulary the
worktree teardown gate already spoke (`live` / `unverifiable` / `exited`),
promoted out of that module into `src/shared/pty-liveness-verdict.ts`.
The close receipt, the CLI wording, worker-stop and the removal gate all
read that verdict instead of inferring an exit from silence.

The same rule fixes the mirror-image defect: the aggregate inventory only
enumerates registered providers, so a dropped relay clears `connected` for
every remote PTY at once. The sweep now separates the provider answering
"absent" (an exit) from no provider being able to answer (lost contact), so
worker-stop stops claiming `exited` from a disconnect.

The `connected` wire field is unchanged in meaning and shape.

* fix(orchestration): apply the same honesty to the federation stop path

The federation host runs its own copy of the worker observation and stop
logic, with the same two defects: `inspectRemoteAttachment` read a dropped
relay's `connected: false` as `exited`, and `federationStop` settled the
dispatch as stopped from a close it never confirmed — relaying a fabricated
success all the way home to the coordinator.

Two guards also had to move so the honest verdict does not become a new
refusal. `federationRead` gated on `status !== 'running'`, which would have
rejected a connected terminal the moment a stop lost contact with it; it now
gates on `status === 'exited'`, which is equivalent for every pre-existing
status given the two guards beside it. Local `workerStop` likewise still
attempts the close when the verdict is `unverifiable` — losing contact is a
reason to report the outcome honestly, never a reason to stop trying.

The show observations now carry the reason alongside the status, so a bare
`unverifiable` is actionable. Both are new optional fields.

* fix(ssh): preserve unconfirmed stop verdicts across consumers

* fix(ssh): use canonical live verdict wording

* fix(ssh): refuse wrong-host teardown verification

* test(orchestration): confirm worker release teardown

* fix(orchestration): negotiate honest worker stop receipts

* fix(agent-teams): fence uncertain teammate respawns

* fix(ssh): avoid duplicate missing-provider teardown

* fix(orchestration): preserve archives across release retries

* fix(ssh): preserve verdicts across synthetic kill exits

* fix(ssh): preserve liveness evidence across teardown

* fix(agent-teams): replace panes only after confirmed stop

* fix(ssh): distinguish host exits from relay loss

* fix(ssh): narrow concurrent inventory verdicts

* fix(orchestration): serve archives after uncertain release

* fix(orchestration): expose unverifiable read liveness

* test(ssh): align liveness assertions with verdicts

* fix(ssh): preserve host scope across inventory failures
2026-08-17 00:11:19 -07:00
Jinjing b279f66c96 Include descendants in Pinned section when parent is pinned (#15035)
Descendants of pinned parents now appear in the Pinned section without individual pins. When an ancestor's pin state changes, reveal the active descendant to maintain focus and visibility.
2026-08-17 00:02:36 -07:00
NeilandBrennan Benson 66b599399f fix(mobile): decide terminal preedit from the marked-text range, not a script table (#15007)
* fix(mobile): decide terminal preedit from the marked-text range, not a script table

The live terminal capture field decided what to withhold from the PTY with a
Unicode-block allowlist (Hangul jamo and syllables) and held exactly one trailing
code point. Kana and kanji are not in the table, so a Japanese reading streamed to
the PTY one fragment at a time and was repaired afterwards with DEL bytes (#7427).

A code-point table cannot work, and the counterexample is not exotic: Chinese
pinyin preedit is plain ASCII, and a Japanese romaji reading is one code point on
the first keystroke and three on the fourth. Preedit is a property of the FIELD,
not of the characters in it, so the only signal that identifies it is the text
system's marked-text range. That is what a reference terminal implementation uses
on every platform it supports - `hasMarkedText` there, the input-method context's
composing state elsewhere - and neither one classifies code points anywhere in the
input path.

So the mirror now takes the marked-text report per change and holds the whole
preedit region, whatever its length or script:
- Subscribe the capture field to `onChange`, not `onChangeText`; only the raw
  native event carries the report at all.
- A reported preedit is held entire and is never committed by the settle timer,
  because preedit is not text yet. Explicit boundaries still flush it.
- `isTerminalLiveHangulCodePoint` and its four ranges are deleted.

iOS reports the range but React Native drops it before JS, so the pinned patch
forwards `markedTextRange` into the change payload. It is three hunks and it
compiles because the app already sets `buildReactNativeFromSource` for iOS. The
same idea was proposed in #11450, which is where the patch comes from.

Android has no marked-text report in React Native at all, and a Kotlin patch would
not help: Android consumes the prebuilt react-android artifact, so node_modules
sources are never compiled. Until the report exists there, the fallback holds the
trailing non-ASCII run. It enumerates nothing, it covers kana, kanji and Hangul,
and ASCII keeps its zero-latency echo - but it cannot see an ASCII preedit, so
Chinese pinyin on Android still leaks its reading. Only a report fixes that.

Not-tested: no physical device or emulator was available, so no real IME drove
this path. Japanese, Chinese and Korean composition are covered at the model and
hook level only, and the iOS patch has not been compiled.

Co-authored-by: Brennan Benson <brennanb2025@users.noreply.github.com>

* fix(mobile): bound the fallback hold to text the pty has not received

The no-report branch walked the trailing non-ASCII run over the whole field and
ignored stableLength, unlike the reported branch directly above it. So after a
settle-timer commit the next keystroke re-held everything already delivered and
the caller erased it with DEL and retyped it — a nine-character Cyrillic word
cost a DEL per already-sent character, and for the 300ms before the re-send the
held text was the only copy, so a blur or reconnect destroyed characters the pty
already had.

Bound it the way the reported branch is bounded. Pinned by a test that drives a
settle commit between every keystroke and asserts no DEL reaches the wire.

---------

Co-authored-by: Brennan Benson <brennanb2025@users.noreply.github.com>
2026-08-17 00:01:24 -07:00
Neil 3bb87ff93b reland(shell): one portable Unix startup dialect, with both revert causes fixed (#15018)
* reland: portable startup-shell dialect, with the two revert causes fixed

Relands #14863 (reverted by #14975) with fixes for both regressions the
revert cited.

1. History GC deleted folder-workspace shell history. The live set was built
   from `getAllWorktreeMeta()` alone, but a folder workspace's PTY carries
   `folder:<id>` as its worktree id, so every live folder workspace looked
   orphaned. `getKnownWorktreeIdsForHistoryGc` now unions in
   `getFolderWorkspaces()`. Both consumers — the history-directory prune and
   the fish-history sweep — read that one set, so the fix covers bash, zsh and
   fish history alike. The directory prune had this gap since #1524; #14863
   only widened its blast radius to fish files.

2. A copied Codex resume command aborted under `set -u`. Its leading clear
   statement has to test `$fish_pid`, and that unbound expansion takes the
   whole line — including the agent launch — down with it. Copied text runs in
   a shell Orca never spawned, so nothing can seed that variable first. The
   removal now rides on the agent itself as `env -u`, which needs no shell
   syntax and no expansion. Verified byte-identical under `set -u` in sh,
   bash, zsh, dash, ksh and fish.

   `env` cannot run the `cd` builtin, and a child `cd` would not move the
   agent, so the prefix is placed on the agent rather than on the whole
   `cd … && agent` chain. cmd and PowerShell have no nounset hazard and keep
   their clear ahead of the `cd`, which preserves `cd … && agent` — a failed
   `cd` still cannot launch the agent in the wrong directory.

* fix(history-gc): stop three more paths from deleting live shell history

Found by adversarial review of the reland. All three are the same class as
the bug that caused the revert: a live set that is missing a category of
real workspace, so the GC reads it as orphaned.

1. Profiles. The history root is `userData/terminal-history`, which has no
   profile segment, but the Store the GC consults is per-profile. So after a
   profile switch the live set condemned every other profile's history — and
   fish history, which lands in the user's own fish data dir, is shared by
   every profile on the machine. The live set now unions in the inactive
   profiles' worktrees and folder workspaces, read from their data files. A
   profile whose ids cannot be read reports the empty set rather than one
   that condemns real history.

2. No empty-set guard on the tree scan. `sweepOrphanedFishHistoryFiles`
   refuses an empty live set because it cannot be told apart from a store
   that failed to hydrate; the directory scan, which deletes more, had no
   such guard. A store that fell back to default state would have taken
   every worktree's bash and zsh history with it, across all roots including
   WSL. Four existing tests passed `new Set()` and relied on "empty means
   everything is orphaned" — exactly the behavior being removed — so they
   now pass a real live set.

3. Relay fish history. The relay isolates its history tree under its own
   root but wrote fish history into the shared fish data dir under the
   desktop naming, keyed by the CLIENT's worktree ids. On a machine running
   both Orca and a relay host, the desktop sweep deleted remote sessions'
   history once it went stale. Relay files are now `orca_relay_<hash>`,
   which the sweep's pattern deliberately does not match; the relay still
   deletes them by exact name when the worktree goes away.

* fix(resume): enforce the env-removal invariants instead of documenting them

Both found by adversarial review; both were unreachable from today's callers
and silent if reached, which is exactly how they would survive to a caller
that does reach them.

- A pinned CODEX_HOME and the removal named the same variable, and `env -u`
  strips what the assignment just set — so the agent would have resumed
  against the real home and not found the session. The removal list now
  excludes any name the prefix pins, keeping the assignment authoritative as
  the old `clear…; CODEX_HOME=x agent` ordering did. Same fix in the git-bash
  twin. The PowerShell branch already clears before it assigns, so it was
  never affected.

- Placement was keyed on the platform while the grammar it selects is keyed
  on the shell, so `platform: 'linux'` with `shell: 'powershell'` emitted
  POSIX `env -u` into a PowerShell line. PowerShell now routes to the
  PowerShell builder whatever the host, and the POSIX/cmd split below asks
  the shell rather than the platform.
2026-08-16 23:51:53 -07:00
Jinwoo Hong 77ef6bb9ee fix(terminal): verify agent prompt submission (#14962) 2026-08-16 23:34:18 -07:00
Neil cc74436b3c fix(skills): keep an unanswered skill root's last known skills instead of reporting none (#15015)
* fix(skills): keep an unanswered skill root's last known skills instead of reporting none

An aborted or shed root scan degraded to `{skills: [], unavailable: true}`. Every
consumer derives "installed" from the skill list, so an unreadable root read as
proof the skill was gone and an already-installed skill offered Install again.

- discovery: retain the last answered scan per root (5 min, LRU-bounded, dropped
  on install/update invalidation and when a root answers as absent) and serve it
  when a later scan goes unavailable.
- source inventory: an unanswered root reports `exists: true`, so check
  `skippedReason` first rather than presenting it as a successful scan.
- useInstalledAgentSkills: when an in-scope root did not answer and the skill was
  not found, say the status may be incomplete instead of a bare "Not installed".

* fix(skills): do not let install verification accept an unanswered root's retained scan

Retention makes discovery serve a stalled root's last completed scan, which is the
state before an install wrote. Verification read that as proof, so reinstalling a
skill into a root that stalled could report success without reading what it wrote —
turning a retryable false negative into a false positive.

A match now counts only when a root that actually answered reached the skill, so a
symlinked placement co-owned by a healthy root still verifies.
2026-08-16 23:31:39 -07:00
Neil 2fdaa10fd1 fix(terminal): give the composing-chord deferral an owner and a ceiling (#15017)
The chord held for a live composition waits with `fallbackMs: null`, so nothing
but compositionend can end it. `sendTerminalInputAfterComposition` returned
void, so nobody could stop it either: the two listeners it puts on the terminal
element outlived the pane, and a later composition on that element flushed the
stale chord.

Return a disposer from the helper and put a sender in front of it that owns
every pending chord, so blur and pane teardown drop them the way the Enter path
already clears its state. The sender also bounds the wait — generous enough for
a conversion candidate window, and it discards rather than sends, because a
chord arriving mid-preedit is the corruption the wait exists to prevent.

The Enter path needs none of this: its 200 ms fallback always runs, so its
listeners cannot outlive it. Pinned so that stays true.

Fixes STA-4476
2026-08-16 23:09:46 -07:00
Jinjing 81d7f9b24e refactor: split db.ts under 400 lines (#14979)
* refactor: split db.ts under 400 lines

* rm plan

* fix(orchestration-db): add safety guards to database operations

Add status guards to UPDATE statements to prevent late operations from
overwriting changes made by concurrent requests. Validate mutation results
to surface silent no-ops. Extract circuit-break threshold, add transaction
wrapping, and sanitize untrusted input. Bump schema version to v28.

* Add transactional safety to dispatch and message operations

Wrap dispatch failures and batched message updates with SAVEPOINTs to
ensure atomicity and idempotency:
- Dispatch failures now check status guards and roll back if the
  related task update fails, preventing partial state corruption
- Message batches (across multiple 500-id chunks) roll back entirely
  if any batch fails, avoiding partial mutations
- Add tests verifying idempotency and atomicity under failure conditions

* Handle concurrent writes and improve transactional safety

- Remote question answering: add classification check before and after UPDATE to safely detect concurrent modifications. Prevents false success when the UPDATE loses a race.
- Transaction rollback: wrap in try-catch to prevent errors from masking the original failure.
- Question thread reset: use status update instead of deletion to preserve message references.

* test: add answer replay and race condition edge case coverage

Add test cases for answer replay idempotency, conflict detection, and a race condition between concurrent answer updates in federation relay. Also verify local question state transitions during orchestration reset.
2026-08-16 22:44:43 -07:00
Brennan Benson d21f63e6fc fix(browser): keep cookie-import warnings readable from newer hosts (#15002)
* fix(browser): keep cookie-import warnings readable from newer hosts

The import summary is cast, not decoded, coming off the runtime RPC wire, so a
newer host can publish a warning code or undecryptable reason this build has
never seen. Both switches then matched nothing and returned undefined, which
rendered as a blank toast.warning(undefined).

#14683 replaced the absorbing default: arm with case 'unknown' to satisfy
switch-exhaustiveness-check, which forbids default: on an exhaustive switch.
Guard before each switch instead: narrow the wire value against the handled set
and route anything else to a generic message. The handled sets are keyed by the
unions themselves, so a fourth member still fails typecheck here.

* fix(browser): reject non-string cookie-import warning discriminants

Object.hasOwn coerces its key, so the membership guards admitted any value whose
toString() matched a handled variant. A host that widened reason to an array
sends ['unknown'], which passed the guard and then fell straight back out of the
switch -- the same undefined-to-toast.warning blank the guards exist to prevent.

Take unknown and check typeof first, matching isTopLevelView and isTuiAgent. The
Record membership sets still fail typecheck on a new union member.
2026-08-16 22:42:54 -07:00
OrcaWinandm4air f660aaab0d Remap SSH leases per execution host, not globally (#15009)
* Remap SSH leases per execution host, not globally

SSH lease leaf IDs are now remapped within their execution host partition, preventing silently empty tabs when the same tab name exists on multiple machines after restart. Also preserve folder workspace paths exactly as provided without trimming whitespace.

* rm review file

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-16 22:22:09 -07:00
Neil 08bf209e40 fix(ci): run PR LoC scripts from the default branch, not PR head (#15016)
The PR test LoC job fetched .github/scripts/pr-test-loc-*.mjs from
pull/<n>/head and ran them with node while holding a GITHUB_TOKEN scoped
pull-requests: write, so PR-authored code executed under a write token.

Pin the fetch to the repository default branch. base.sha is not enough:
for stacked PRs it is an unreviewed feature-branch commit any collaborator
can push to, while main is gated by branch protection.

Also pass event data via env instead of shell interpolation, and add
set -euo pipefail so a failed download cannot leave a truncated script.
2026-08-16 22:18:50 -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
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
Jinjing f070033156 Revert "refactor(shell): one portable Unix startup dialect instead of shell d…" (#14975)
This reverts commit b6ea3f17a9.
2026-08-16 16:48:55 -07:00
Jinjing 9c4627d1c6 Refactor: split GitHubItemDialog into lifecycle-organized modules (#14931)
* refactor: split GitHubItemDialog.tsx under 400 lines

No intentional behavior change.

* refactor: group github-item-dialog into lifecycle folders

Reorganize the 50 flat files under src/renderer/src/components/
github-item-dialog/ into six lifecycle folders:

  load-item-details/     shared types, both caches, fetch/settle, state badge
  open-dialog/           dialog shell, headers, body, tabs, link copy
  discuss-item/          conversation tab, comments, composer, timeline
  edit-item-fields/      GH edit section, labels, assignees, status
  inspect-pull-request/  combined diff viewer, checks tab
  land-pull-request/     PR actions, merge menu, reviewers

No intentional behavior change. All 50 files moved verbatim; the only
edits are relative-import specifiers (sibling paths plus a depth bump
for ../../../../shared) and the hardcoded module paths in the two
source-boundary tests.

Import graph stays acyclic: zero mutual folder pairs, no file importing
4+ sibling folders, no dest file importing the public barrel, and no
per-folder index barrels.

* refactor: split item references and improve diff-viewer remount logic

- Break down full `GitHubWorkItem` props into discrete `itemId`, `itemNumber`, and
  `itemRepoId` in mutation and action functions to prevent over-memoization of callbacks
  and improve dependency clarity.
- Extract `getPRFilesCombinedDiffSignature()` and use it as a component key to safely
  remount the diff viewer when the PR revision changes, replacing generationRef tracking.
- Add `getKeyedCheckAnnotations()` and `getKeyedCheckJobs()` to generate stable,
  collision-resistant keys for check arrays that may contain duplicates.
- Consolidate interpreter timeouts into a single `SPAWNED_INTERPRETER_TIMEOUT_MS` constant
  and apply it via describe options rather than per-test values.

* refactor: improve github-item-dialog repo context and i18n coverage

- Add repoId prop to ConversationTab for explicit repo context override
- Internationalize UI strings in diff viewer and PR action components
- Improve error handling with cache rollback and guard cleanup on sync failure
- Enhance cache key validation for cross-window invalidation by repoPath
- Add repository access validation before rendering diff viewer
- Fix cross-platform issues: skip symlink test on Windows, normalize CRLF in test assertions

* Refactor check button i18n key and update text

- Replace hash-based key with semantic name for maintainability
- Change button label to "Open in browser" for broader context
2026-08-16 16:44:48 -07:00
Jinjing 1e63cfef06 Revert "fix(mobile): present pending Relay fallback accurately (#14922)" (#14976)
This reverts commit 3811881410.
2026-08-16 16:30:50 -07:00
Neil d6703552c9 fix(crash-reporting): prune Crashpad dumps at startup and bound signature parsing (#14968)
A dying main process never delivers process-gone, so a crash loop never reached
the only prune call site, and Crashpad's own pass runs in the handler child
after a delayed first sweep. Prune on startup instead of behind the coalescing
timer the loop outruns, and cap dump count alongside the byte budget.

Signature parsing stayed on the main event loop after a crash. Reject on ptype
before the whole-buffer scan, and bound the backward prefix search that could
otherwise walk the entire dump only to discard the result past 96 bytes.

Also keeps dumps already claimed by a persisted report from being pruned out
from under the report's minidumpPath.

STA-4544
2026-08-16 16:27:51 -07:00