Commit Graph
7985 Commits
Author SHA1 Message Date
Brennan Benson aa7e76ba61 perf(tabs): index tab agent status by tab instead of scanning the global map (#12413)
* perf(tabs): index tab agent status by tab instead of scanning the global map

resolveAnyCompletedTabAgent and its live/retained twins scanned the whole
agentStatusByPaneKey map and parsed every pane key, once per tab per render —
~10^5 parsePaneKey calls per render pass with 200 tabs. Cache a per-tab pane
index on the map's identity (the store replaces it on every write) so a render
pass scans once instead of once per tab. Insertion order is preserved because
the resolvers return the first match.

* test(tabs): lock agent status index scan count
2026-08-04 17:05:12 -07:00
Brennan Benson 8c65dd5094 perf(runtime): keep PowerShell ACL work and a second auth off the remote command path (#12451)
* perf(runtime): keep PowerShell ACL work and a second auth off the remote command path

Two costs sat on the remote authentication path on Windows:

- The E2EE handshake persisted `lastSeenAt` inline, and every secure-file write
  spawns PowerShell synchronously twice to reapply the registry ACL, so the
  client's `e2ee_authenticated` waited on both spawns.
- Every remote CLI command except `status.get` opened a second full WebSocket
  connection just to re-read status for the protocol-compat check, doubling the
  authentications per command.

The first sighting of a device still persists inline (rotation drops entries
disk says were never scanned); later refreshes update memory now and coalesce
onto one deferred write. The compat verdict is saved against the runtime's
per-launch `runtimeId`, so a restarted or upgraded runtime retires it.

* fix(runtime): preserve compatibility on one remote auth

* fix(runtime): flush registry after transport shutdown
2026-08-04 17:04:51 -07:00
Neil 7c26cceaf1 fix(workspaces): add space after emoji picker selection
Add a Slack-style trailing space after selecting an emoji from the create-worktree colon picker, while preserving existing separators and caret placement.
2026-08-04 16:56:56 -07:00
Brennan Benson 08abb758fa perf(renderer): bail out of identity-equal terminal layout and cache-timer writes (#12420)
* perf(renderer): bail out of identity-equal terminal layout and cache-timer writes

setCacheTimerStartedAt and setTabLayout spread a fresh object and returned it
unconditionally, so every redundant call published a new AppState and ran every
zustand subscriber's selector across all mounted panes. Both have a real
redundant cadence: parked-terminal-byte-watcher writes a null cache timer on
each agent working/exit/stale-title transition, and TerminalPane re-persists an
identical layout on pane-title churn.

Extract the existing terminalLayoutEqual comparator out of web-session-tabs-sync
into a shared module and use it to gate the layout write, and dedupe the
remote-runtime layout IPC against the last snapshot pushed per tab.

* fix(renderer): retry failed remote pane layout pushes

* test(renderer): cover stale remote layout failures

* test(e2e): cover remote pane layout retry
2026-08-04 16:56:43 -07:00
Brennan Benson a528b689a9 Prevent Command Code output from hijacking agent icons (#12573)
* fix terminal agent icon ownership

* fix terminal output ownership gaps
2026-08-04 16:06:54 -07:00
Kaynan Sampaio de Camargoandkaynan 7948e46db8 fix(mobile): open the Resume workspace through a mounted host stack (#12001)
* fix(mobile): open the Resume workspace through a mounted host stack

Tapping Resume on Home landed on a blank host screen instead of the
session. A cold push straight into the nested /h/[hostId] navigator
resolves to the host index route without the dynamic id, so
HostProtocolGate mounts with hostId undefined and never connects.

Home already worked around this for the host editor (#11635) and Tasks
(#11853) by mounting /h/[hostId] first and replacing it once the stack
is committed. Extract that mechanism into host-stack-navigation so
Resume uses the same transition instead of a direct push.

The previous Resume fix (#11876) swapped the manual href for a typed
dynamic href, but expo-router's encodeParam already applies
encodeURIComponent to dynamic segments, so it resolved to the same URL
the manual string produced and left the cold-navigator path unchanged.

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

* fix(mobile): harden the host-stack transition after bot review

- match a host route committed as the encoded segment it was pushed as,
  so an id containing `/`, `#`, or `%` still triggers the REPLACE
- share one pending transition across the Home entry points; per-hook
  refs let a Tasks tap and a Resume tap arm two pushes that could not
  cancel each other
- assert the source markers before slicing in the Resume wiring test

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

* test(mobile): lock the host-stack transition state machine

Assert the replace waits for the host mount (zero dispatches before it,
exactly one after), and cover cancel/retarget — the paths the shared
pending transition relies on.

* test(mobile): model listener removal in the navigation harness

A no-op unsubscribe let setState keep calling a canceled listener, so the
teardown assertions only exercised the active guard. Dropping the
unsubscribe call from dispose now fails the suite.

---------

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
2026-08-04 16:06:37 -07:00
Brennan Benson 535b5a594d fix(mobile): keep paged chat history coherent across reconnect replays (STA-3333) (#12494)
* fix(mobile): keep paged chat history coherent across reconnect replays

The transport replays nativeChat.subscribe with its original params after an
in-place reconnect, and the session hook treated every snapshot as a fresh
base — so a socket blip truncated paged-in history back to the initial 40.

A replay snapshot that extends a contiguous retained tail now merges in by id;
a disjoint replay (long outage, compaction while away) still replaces, since
stitching would leave a silent gap. Only a genuinely replaced window resets the
grown read limit and paging cursor, and any snapshot invalidates an in-flight
older-page request so a stale cursor result cannot land on the new window.

Refs STA-3333.

* test(mobile): pin the replay contiguity-scan rejection branches

The scan's three rejection rules were unpinned: deleting the
`sawNewMessage` guard, the ordering check, or the retained-tail anchor
each left the whole suite green. Cover the interleaved-new-row,
reordered-id, and short-of-tail cases, and the replaced-window hasMore
fallback. Each new test is mutation-proven to kill exactly one mutant.

* test(mobile): pin replay paging-metadata and base-snapshot authority

Two more branches of the replay logic were unpinned. Adopting a replay's
`beforeOffset` when it starts partway into paged-in history would make the
next loadEarlier re-fetch on-screen rows and prepend duplicates; treating a
post-replacement snapshot as a replay would retain a row the authoritative
window dropped. Both mutants now fail exactly one test.

* test(mobile): pin the replay removal boundary and base-snapshot bookkeeping

Two branches introduced by this PR survived the suite unpinned:

- `firstIndex > 0` was only pinned one-directionally. Weakening it to
  `firstIndex > 1` kept all 28 tests green, so an off-by-one would silently
  retain one row the host had already dropped.
- `snapshotSeenRef` is set only for snapshot frames. Setting it
  unconditionally is invisible in normal flows, where the first frame is the
  snapshot, but demotes the real base snapshot to a replay when a live append
  lands first.

Each new test kills exactly one of those mutants and nothing else. No source
change.

* test(mobile): pin the older-page fence against a cursor-re-cutting replay

The snapshot arm of `if (applied.windowReplaced || frame.type === 'snapshot')`
was unpinned: deleting it kept the suite green.

It is load-bearing. A replay that merges cleanly can still carry a new
`beforeOffset`, which the hook adopts via `replayStillStartsAtOldest`. The page
already in flight was addressed with the old offset, so without the fence it
lands and writes its own stale cursor back over the fresh one, leaving the next
`loadEarlier` addressed from a byte offset that no longer describes the file.
Row order alone stays correct, which is why the ordering-only reasoning missed
this.

Sole failure under the mutation. No source change.
2026-08-04 16:04:14 -07:00
NeilandOrca fb27702100 feat(updater): restart hourly build numbers per version, restyle the timestamp (#12587)
The number answers "which build of 1.4.163 is this", so carrying it across
versions made it meaningless — 1.4.164 opened at 38 for no reason a reader
could see. It now counts titles matching the base version being built, so a
version bump restarts the series at 01.

Deriving it moves from workflow jq into the script, because the number depends
on the base version and only the script knows which base the published tags
resolved to.

Timestamps go from `07-31 13:54` to `Jul 31, 1:54PM`, still Pacific.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 15:54:17 -07:00
Brennan Benson 9deee5ad2f perf(worktrees): delete worktree directories after the removal returns (#12416)
* perf(worktrees): delete worktree directories after the removal returns

`git worktree remove` deleted the whole checkout inline, so the remove IPC held the
watcher/PTY gate for the entire recursive delete (prod traces: worktree.remove.git_remove
p50 8-14s, p90 29s, max 34.7s). Local removals now rename the checkout into a hidden
sibling trash root, clear Git's registration for the missing path, and delete the moved
tree in the background. Renames that cannot run (WSL, other volume, Windows open handles)
fall back to the previous in-place removal unchanged.

* test(worktrees): keep no empty trash root when the rename cannot run

* fix(worktrees): harden deferred trash cleanup

* fix(worktrees): keep WSL trash on its owning host
2026-08-04 15:53:11 -07:00
Brennan Benson 40ea4ece1a Track Claude models from the installed CLI per host (STA-3330) (#12369)
* feat(native-chat): track Claude models from the installed CLI per host (STA-3330)

The Claude seed no longer pins version labels to aliases that resolve
differently across CLI versions, and the catalog now defines listModels
backed by a one-shot list_models control request over --print stream-json.
Hosts whose CLI predates the request answer with a control error and keep
the seed. Discovery also feeds Source Control AI via the commit-message
spec, and the /model echo detector matches resolved model names.

* fix(native-chat): preserve discovered Claude capabilities

* fix(native-chat): tolerate malformed Claude model entries

* fix(native-chat): discover models in folder workspaces

* fix(native-chat): trust discovered Claude capabilities

* fix(native-chat): remove Claude model fallbacks

* fix(native-chat): keep the Claude model picker rendered

The Claude picker rendered nothing until the per-host `list_models` probe
returned, so it popped in ~1s after mount and never appeared at all when
the probe failed — an old CLI without `list_models`, no `claude` on PATH,
or an older remote runtime whose response omits `catalogOrigin`.

Restore the version-neutral family seed as the starting list; discovery
still replaces it wholesale on success, so a host with a real catalog
never shows an obsolete hardcoded row.

Separately, the tracked model could fall outside the active list: the
terminal header scrape yields family ids (`opus`) while a current CLI
lists `opus[1m]` and no plain `opus`. That blanked the picker trigger and
dropped the model's effort and fast-mode controls. Reconcile the tracked
id into the active list once, so the snapshot, the appliers, and typed
command recording all see a labelled, operable row for it.
2026-08-04 15:47:29 -07:00
Brennan Benson 9ee359550b fix(mobile): make native-chat file links and path citations tappable (STA-3331) (#12364)
* fix(mobile): make native-chat file links and path citations tappable (STA-3331)

- Linkify POSIX absolute paths in chat prose (leading-/ regex alternative;
  URL guard now keys off the char before the matched slash)
- Parse agent-style path:line(:col) citations in prose, code spans, and the
  open flow; line/column ride into the mobile file preview route
- Route non-web markdown hrefs (file: URIs, relative/absolute paths) to the
  file opener instead of silently dropping them; unknown schemes stay dead
- Resolve chat paths against the worktree root, not the terminal's live cwd
- Reuse the terminal tap-to-open flow for chat taps (haptic, preview route,
  tab activation with retries) via a shared identity-stable hook, and toast
  on misses instead of silent no-ops
- Keep snake_case paths whole (intraword underscores are literal text),
  scan bold/italic/strike spans for paths, split trailing punctuation off
  autolinks, and let taps land while the composer keyboard is up

* fix(mobile): harden chat file tap handling

* refactor(chat): share native chat href routing

* fix(mobile): detect files directly under path roots

* fix(mobile): keep inline tokens and dunder paths intact around emphasis

Review follow-ups on the chat file-link work:

- A rejected intraword `_` token left the scan index past its closing
  underscore, so every inline token between two snake_case words was
  swallowed and rendered as literal source — including markdown links,
  which became untappable. Rescan from just past the opening delimiter.
- Treat a path separator as an intraword flank so `src/__init__.py` and
  `a/__tests__/x.ts` stay whole; previously they rendered as bold plus a
  remnant that the new absolute-root pattern turned into a tap on `/x.ts`.
- Bound the `:line(:col)` tail so `src/app.ts:1e3` and `:80%` no longer
  parse a line number, while a cited range still opens its first line.
- Route chat tap failures through the composer banner (toast fallback):
  chat taps happen with the keyboard up, which covers the toast.
- Drop the tap-handler mirror's dep list; the call site rebuilds its
  accessors every render, so it could never skip on a route that
  rerenders per keystroke.

* Revert "fix(mobile): keep inline tokens and dunder paths intact around emphasis"

This reverts commit 308bfaf22b.

* fix(mobile): preserve chat file-link parsing and feedback
2026-08-04 15:46:08 -07:00
Brennan Benson b3a4a4f929 fix(terminal): coordinate reveal atlas recovery (#11864) 2026-08-04 15:31:08 -07:00
847c8c852d fix(agent-status): correlate manual Claude compact hooks (#12332)
Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 15:03:57 -07:00
Brennan Benson d52df52eea fix(mobile): open editor for disconnected hosts (#12575) 2026-08-04 14:45:11 -07:00
Jinjing 2073f7eeb2 fix(startup): omit Linux-only package mgrs from PATH on non-Linux (#12566)
Snap and Linuxbrew don't ship installers for Darwin or BSD, so seeding
their PATH entries on those platforms adds phantom directories every
spawn must stat. Keep them on Linux only, while preserving Nix and
Homebrew across all platforms as they have multi-platform support.
2026-08-04 13:45:50 -07:00
github-actions[bot] eca3ed72b6 release: v1.4.169-rc.0 v1.4.169-rc.0 2026-08-04 20:44:34 +00:00
Jinjing 999e3a3a6d feat(sidebar): link Linear issues from Edit Worktree Details (#12380)
* feat(sidebar): link Linear issues from Edit Worktree Details

The Issue field only accepted GitHub numbers, so a workspace tracking a
Linear issue had no way to say so from the dialog — the link could only be
set at creation time or through `orca worktree set --linear-issue`.

Replaces the field with one provider-aware row: a chip suffix inside the
input selects GitHub or Linear, and pasting a URL flips the chip to match.
A bare key never steers the provider — Linear and Jira issue keys are
byte-identical in shape, so shape alone cannot decide one.

One issue per workspace. A changed field displaces the other provider's
slot and the row names what Save is about to unlink. GitLab and Jira links
are left alone: the row cannot display them, and nothing else in the UI
could restore one it dropped.

- Folder workspaces read-only (their link is creation-time only)
- Remote runtimes assert the capability before writing or clearing, since
  `worktree.set` parses in strip mode and would silently drop the keys
- `updateWorktreeMeta` now reports failure so the dialog can stay open
  instead of closing over a save that refetch reverted
- Parses are length-bounded — `matchGitHubItemPath` strips trailing
  slashes with an unanchored regex that is quadratic on a large paste

* fix(sidebar): respect one-issue-per-workspace rule conditionally

Only clear displaced issue links when they actually existed, preventing
unnecessary Linear keys in GitHub-only workspaces. Skip comment updates
when unchanged to avoid workspace reordering. Add accessibility to
displacement messages and improve folder workspace error handling.

* fix(sidebar): resolve workspace ambiguity and improve Linear issue linki

The same workspace ID can exist under multiple hosts — the owner index reports
this as ambiguous rather than guessing. Dialog callers now pass their repoId so
lookups are unambiguous. Linear identifiers without an org key are resolved
across all workspaces (not just the active organization). Added race-condition
protection for async issue lookups and better change detection to avoid clearing
work-item titles when re-saving an identifier in different spelling.
2026-08-04 13:42:25 -07:00
Jinjing 88549619d2 feat(sidebar): inline SSH reconnect control on workspace cards (#12396)
* feat(sidebar): inline SSH reconnect control on workspace cards

Replaces the blocking SshDisconnectedDialog with an inline pill in the
workspace card title row, and unifies the SSH connect vocabulary across the
sidebar card, terminal overlay, host-header menu, and status-bar row.

- new src/renderer/src/ssh/ modules: typechecker-total status predicates
  (recoverability), a shared in-flight connect registry, the promoted UI
  connect timeout, and the shared connect verb table
- WorktreeCardSshHostControl: one 16px pill shape for every state, icon-only
  in compact/new card modes, passive glyph for connected/null/removed hosts
- migrates the four duplicated status predicates to the shared module
- deletes SshDisconnectedDialog (and its window-capture Enter handler)

* fix(ssh): address review round 1 on the inline reconnect control

- reconnect surfaces get a 180s UI connect fence instead of the composer's
  20s: main allows 120s for an interactive passphrase before the 30s connect
  even starts, so the short cap toasted "timed out" and ran the stale-metadata
  resync against a host that was about to connect fine
- carries the existing es/ja/ko/zh translations onto the shared connect verbs
  (they were en-only, regressing four locales) and drops the dead
  SshDisconnectedDialog key namespace
- migrates the three remaining copies of the connecting predicate
  (SshStatusSegment, SshTargetRow, external-automation-source-availability)
- SshTargetStatusRow and SshTargetRow now join the shared in-flight registry,
  so a connect started on one surface disables the others immediately
- drops the per-card aria-live region that duplicated the button's own label

* fix(ssh): address review round 2 on the inline reconnect control

- guard sshTargetRemoved on isRuntimeOwnedSshTargetId: runtime-owned targets
  are filtered out of ssh:listTargets, so absence is not evidence of removal
  (every ephemeral-VM card otherwise read "SSH host removed")
- drop the card-root SSH dim: an ancestor opacity composited the control's
  destructive tint and spinner down to an illegible alpha
- aria-disabled instead of disabled while connecting, so the pill stays
  pointer-reachable for its tooltip and focus
- stop Enter/Space propagation so WorktreeList's container key handler does
  not steal activation
- prefer a live connection over a stale removal tombstone
- reclaim title width in labeled mode (no leading icon, no min-width floor)
- register composer connects in the shared in-flight registry

* temp checkin of files

* fix(ssh): hold connect lock for backend request, not UI timeout

Replace manual begin/end pairs with trackSshConnect wrapper that holds
the lock for the full backend request duration. Previously, the lock
was released after the UI timeout fired, even though the backend was
still dialing — a second click on any surface for this host would
trigger a second connect and a duplicate credential prompt on
passphrase-gated targets. The wrapper survives unmount, unlike a
finally block in a component handler.

* fix(ssh): scope connect locks to their acquisition, not target

A tracked request settling after its lock was cleared (via reset or
explicit end) must not unlock a newer connect on the same target. Lock
IDs ensure only the owning acquisition releases, preventing stale
settlements from clearing active locks.
2026-08-04 13:37:04 -07:00
Brennan Benson 1816d3eee2 test(terminal): pin IME chord survival across mid-composition re-renders (STA-3291) (#12357)
Busy panes render mid-composition (title updates drive renders), and the
modified-Enter chord owner must survive those renders or held-modifier
CJK input leaks newlines. Wires the hook TerminalPane-shaped (dep objects
rebuilt per render) and interleaves re-renders through hold, composition,
and auto-repeat; fails under pre-fix churn wiring (verified: redispatch
not absorbed), passes with memoized actions.
2026-08-04 13:10:07 -07:00
Brennan Benson d27d69fbff fix(mobile): serialize native chat PTY writes and fence superseded ask keystrokes (STA-3333) (#12502)
* fix(mobile): serialize native chat PTY writes

Two composed native-chat write sequences into one PTY interleaved their
bytes: the per-terminal send-in-flight guard lived inside the image
attachments hook, so ask answers, permission choices, and question
answers wrote straight past it. Move the guard to a shared module-scope
write lock (the terminal outlives any one screen) and take it on all
four paths.

Ask answers additionally queue behind the prior chain's RPC rather than
racing it, and a superseding answer is fenced once a key has actually
landed: an accepted or ambiguously-delivered keystroke already moved the
remote selector, so a replacement's from-scratch key plan would answer
the wrong question. A superseding answer inherits the cancelled chain's
hold (refcounted, last chain out releases) so changing your mind
mid-answer still works, and Stop/cancel stay unguarded so an interrupt
can never deadlock against the send it cancels.

Refs STA-3333.

* fix(mobile): report a fenced native-chat answer instead of dropping it

The fence added for superseding Ask answers returned false with no
onSendError, and the card re-enables on a false result — so a queued
answer vanished with no banner and no toast, looking exactly like a dead
button. Every other bail in answerAsk reports. Keep the generation-
mismatch branch silent: a newer answer owns the error surface.

Also covers the hold-count release, which had no test at all: replacing
it with an unconditional release left all 58 tests green while silently
reopening the terminal mid-sequence — the exact interleave this PR fixes.

* fix(mobile): stop a superseded answer from clearing the fence banner

A chain that finished its key plan reported `true` even after a newer answer
superseded it. The route sends through useNativeChatAcceptedAction, whose
accepted callback retires the send-error banner — and that callback runs after
the successor's fence report, because finishTurn() fires in `finally`, before
the chain's own promise settles. So the successful predecessor deterministically
wiped the fence message the successor had just raised: every healthy write took
that branch, which made the previous commit's report vacuous exactly where it
mattered.

A superseded chain now reports no success, matching every other supersession
checkpoint in this hook.

* fix(mobile): fence on the turn slot, not the generation counter

The supersession guards added in c36f2ce14e read `generationRef`, but
`cancelPending()` bumps that counter from three callers with no successor
chain: Stop, ask-cancel, and the lease effect that fires on every
disconnect. An answer whose key had already landed then reported false —
`fail()` never runs on an accepted write, so the card stayed up silent
with Submit re-enabled, and the retry wrote the same key into a selector
that key had already advanced.

Test the turn slot instead: a successor takes it synchronously before
its first await and cannot resolve ahead of this chain, so it means
"a successor took over" exactly, without catching bare cancellations.

* fix(mobile): report a fenced answer when a dropped lease bumps the generation

The fence-report guard tested generationRef, which answers "was I
cancelled", not "did a successor replace me". Stop and ask-cancel both
write an Escape that clears the ask card, so their silence is harmless.
A dropped input lease bumps the same counter and writes nothing: the
card stays up with Submit re-enabled while the predecessor's option key
has already advanced the live selector, so the retry double-steps it.

Gate on the turn slot, matching the two success returns.

* test(mobile): cover the turn-slot release after a landed answer

The slot delete in the finally was the one line in this hook no test
killed: without it a landed answer's resolved-false turn stays parked,
so every later answer on that handle reads a fenced predecessor and the
ask card dies after its first use.

* test(mobile): keep PTY write locks terminal-scoped
2026-08-04 13:01:39 -07:00
Brennan Benson 9759bd2e76 fix(mobile): center native chat checkmarks (#12565) 2026-08-04 12:40:36 -07:00
Jinjing e138d28fa6 Fix Linear filter chips showing UUIDs after dropdown closes (#12564)
Fetch metadata when filters are selected, not only while the popover
is open, so chip labels remain readable after closing the dropdown.
2026-08-04 12:37:59 -07:00
Brennan Benson e4aadcceff fix(mobile): keep repeated-prefix native chat replies streaming (STA-3333) (#12501)
* fix(mobile): keep repeated-prefix chat replies streaming

Text alone can't tell "the transcript caught up with this stream" from
"a new reply repeats the previous turn's prefix", so the old suppress-on-
prefix rule swallowed genuine repeated replies. A stateful gate remembers
which transcript tail predates the current stream segment and hides the
bubble only when that tail moved during the segment, scoped to the active
host/workspace/tab/session so a swapped chat can't inherit a baseline.

Refs STA-3333.

* fix(mobile): keep the streaming gate alive across chat/terminal toggles

The gate lived in MobileNativeChatView, but MobileNativeChatOverlay returns
null whenever the user peeks at the terminal — that unmounts the view and
throws the baseline away, so the repeated-prefix reply was swallowed again on
the way back. Move the gate (and the fold memo it reads) up to the overlay,
which stays mounted across those toggles.

While hidden the transcript is empty and the throttled stream reports no text,
which the gate would have read as "idle" and re-anchored on. Pass the agent's
working state so a textless tick inside a live segment holds the baseline
instead. The scope key is now keyed off the tab rather than the view-gated
chat resolution, so it survives the toggle too; streamIdentity keeps its exact
previous value because the delayed-send guards compare against it.

Also drops a dead disjunct in the caught-up test: a null baseline is already
unequal to every real tail id.

* test(mobile): model the real re-show ordering in the streaming-gate tests

The overlay regression test replayed the transcript before the stream text on
the way back from the terminal view. That ordering is backwards: the session
withholds `messages` until a fresh read settles (an RPC round trip) while the
throttled stream text returns in ~50ms — and with the transcript already back,
a gate that got discarded on the toggle still passes. Replay the real order,
which pins the gate's lifetime as intended.

Swaps the hidden-gap duplicate case for the in-view one (a tool frame clears
the assistant text mid-turn), which is where the hold actually earns its keep;
the hidden-gap direction stays covered at the gate level.

* fix(mobile): stop the streaming gate adopting a reply as its own history

A textless status tick was re-anchoring the gate's pre-stream baseline, so
two paths still rendered wrong:

- The reply's transcript push beats its throttled status text whenever the
  pane stays `working` past the turn (a live subagent or background task).
  The tick in between adopted the just-landed reply as history, and the
  status text that followed rendered it a second time — a duplicate bubble,
  and a regression against main's suppress-on-prefix rule.
- Peeking at the terminal between turns empties the transcript. That empty
  tail was adopted as the baseline, so the next repeated-prefix reply was
  swallowed again — the bug this PR exists to fix.

Only a tick that carries a real tail and sits outside a live turn anchors
now, with an exception for a gate that has never anchored: mounted mid-turn,
the first real tail it sees is the best history it will ever get.

Also drop `buildMobileNativeChatData`, a test-only builder this PR had wired
the new gate into; its green test asserted the exact suppression this PR
removes. Its fold/pending/image coverage moves to the builder the view calls.

* test(mobile): pin the textless anchor's text reset

Mutation testing found the `prevText` reset on an anchoring textless tick
unpinned: keeping the previous turn's text there reads the next turn's
opener as a new segment, re-anchors onto the reply that just landed, and
renders it a second time — the same duplicate-bubble class already fixed
twice on this branch.
2026-08-04 12:36:07 -07:00
Jinjing bc1feb5a4f Add search by name, project, and prompt for automations (#12561)
* Add search by name, project, and prompt for automations

Split the monolithic automations page into focused modules: extract dialog logic, list panel rendering, search functionality, and utility helpers into separate files. Introduce deferred search matching to keep the input responsive, with proper bounds checking to reject oversized pastes. The page stays unfiltered when search is inactive or too large, preserving the original list view in those cases.

* fix(i18n): add missing automation search localization keys

Sync en.json keys used by AutomationListSearchField and the no-matches empty state so static analysis localization catalog check passes.

* Localize remaining automation strings and optimize search

- Add 12 i18n keys for automation labels, counts, and usage display
- Extract AutomationPaneTab and SelectedExternalRunPage types to shared automation-page-state module
- Optimize search fingerprint by truncating prompts to indexed prefix for bounded performance per tick
- Improve escape-key handling in search field to clear input before blurring
- Remove deprecated getAutomationListSearchQuery function
2026-08-04 12:13:40 -07:00
Neil 4734428654 fix(i18n): stop the repair policy de-localizing CJK UI labels
The build-time repair layer carried overrides that rewrite already-correct CJK
values back to English. Most are inert against today's catalogs but fire on the
next regeneration, so they read as latent regressions rather than policy:

- zh workspace status picker (Play/Flag/Zinc/Rose/Emerald/Amber/Violet/Sky/
  Blue/Neutral) and `sheet`/`page` were pinned to English while every sibling
  option, and ko/ja/es, stay translated — half a Chinese picker.
- The zh `蓝色的`/`琥珀色`/`中性的` phrase fixes correctly flagged the adjectival
  的 form but replaced it with English instead of the bare color noun.
- ja `Play` was pinned to English though the catalog already reads 再生.
- A value-wide zh `Open: '进行中'` mapped every "Open" to "in progress",
  including the button that opens an MCP config file. "Open" is a verb (打开)
  on buttons and a state (开放) beside 已关闭, so no single mapping fits.

Catalog corrections in the same area:

- The GitHub/PR state picker key override read 진행 중 / 进行中 for ko and zh
  while ja already had the correct オープン; now 열림 / 开放, matching 닫힘 /
  已关闭 on the sibling entry.
- The terminal cursor-color group is the on-screen cursor, not the Cursor
  editor; ja already had カーソル, ko/zh now get 커서/光标 instead of "Cursor".
- Tailwind swatch labels 天空 (the sky) and 锌 (the metal) do not read as
  colors; now 天蓝/锌灰, and ja 空 becomes 空色.
- The ko disk-usage heading was pinned to bare "Space" while its own
  description says 저장 공간; both now use 저장 공간.

Two policy tests pinned the Play de-localization. They diagnosed the input
correctly — 玩 / 遊ぶ are wrong for a play icon — so the expectations move to
播放 / 再生 rather than English.

Destructive drift (localized -> English on regeneration) drops from 40 to 21
for zh, 6 to 5 for ja, and 5 to 3 for ko. What remains is deliberate: search
qualifiers, path and filename literals, and product names.
2026-08-04 03:56:33 -07:00
김태윤andOrca cc9eb9e572 fix(i18n/ko): standard loanword transcription and terminology fixes (#8816)
Cherry-picked the unambiguous subset of #8816:

- 디렉토리 -> 디렉터리 and 쉘 -> 셸, the standard loanword transcriptions
- Milestones: 이정표 (a signpost) -> 마일스톤, the Linear product noun
- Permission granted: 허가 (a licence) -> 권한
- "privacy envelope" rendered as 봉투, a paper envelope
- commentTooLarge: 너무 커서 (too bulky) -> 너무 길어, across 9 composers

The rest of the PR is left out: 83 leaves are agent -> Agent recapitalizations
that fight the catalog convention, 17 are reverted by the repair policy, and
several change meaning (viewed -> 읽음 "read" on the GitHub file checkbox,
which means seen; "resolve PR base" read as fixing a problem; 차이점 -> diff,
which also breaks ko search recall).

Co-authored-by: tykimseoul <tykimseoul@gmail.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-04 03:56:33 -07:00
JinjingandOrca 1aa5e7f899 fix(i18n): localize the remaining pull-policy and PR-action reasons (#5640)
Most of #5640 landed independently, but 12 strings were still falling through
to English: the ko "Diverged" pull-policy notice, and the ja source-control
primary-action blocked reasons.

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-04 03:56:33 -07:00
174011dd1e fix(i18n): format relative times with the configured UI language (#8662)
Eight renderer call sites built Intl.RelativeTimeFormat(undefined, ...), which
resolves to the OS locale, so relative timestamps rendered in Korean on a
Korean-locale machine even with the UI language set to English. Unlike
DateTimeFormat, RelativeTimeFormat emits language words, so it must follow the
UI language.

Rebased onto main: the formatter now resolves through getIntlLocale() (#12105)
rather than i18n.resolvedLanguage, so the synthetic plugin<hex> resource
language cannot reach the constructor and throw. GitHubItemDialog and
PullRequestPage no longer own their formatter — main moved it into
work-item-state-presentation, which is converted instead, along with the newly
added site there.

Co-authored-by: moseoh <azqazq195@gmail.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-04 03:56:33 -07:00
Turtle-HwanandOrca f9a5920954 fix(i18n): stop repairCatalog crashing on un-bootstrapped locale keys (#11728)
en.json carries ~190 keys per locale that the locale catalogs have not been
bootstrapped with yet, so every repair-locale-catalog run threw a TypeError
before doing any work. Skip missing leaves instead.

Split out of #11728 so the crash fix can land without the catalog
regeneration, which still needs native-speaker review.

Co-authored-by: Turtle-Hwan <turtlehwan@gmail.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-04 03:56:33 -07:00
Neil aa001101f2 fix(i18n): repair the stale SshTargetForm ko override
The override for SshTargetForm.137e88ce8d held a truncated relay-TTL sentence
ending mid-clause at "최대:", which renders "Timeout after disconnect
(seconds)" — the sibling key 55c56cf2c7 — not its own English source. #12192
only corrected the terminals token inside that wrong sentence. Point it at the
value ko.json already ships, so a catalog repair cannot overwrite the correct
string with the wrong one.
2026-08-04 03:56:33 -07:00
AnddyAgudelo a202d92772 fix(localization): stop the repair policy rewriting translated generic terms to English (#12192)
Fixes #12113.

shouldPreserveEnglishValue keyed on the English value, so any key whose source
string equalled a NEVER_TRANSLATE_VALUES entry was forced back to English on
every repair run — agent, commit, repo, terminal and Continue were all on that
list. Measured on a clean checkout: ko 279, ja 461, zh 1180, es 479 values
rewritten, the large majority destroying translator work.

17 generic terms move into locale-generic-ui-terms.mjs, and the brand revert now
skips a term's canonical rendering, so genuinely nonsensical forms (zh 回购,
ja/zh 端子, es Comprometerse) still fire while 터미널/커밋/エージェント survive.
Brand, path, and code tokens are untouched — MD -> 医学博士 and HEAD -> CABEZA
are why that list still earns its keep.

No catalog values change; every file is under config/scripts/.

Co-authored-by: AnddyAgudelo <44873492+AnddyAgudelo@users.noreply.github.com>
2026-08-04 03:56:33 -07:00
ShinSungkyu 705e17a2a0 fix(i18n): correct semantic errors in Korean UI copy (#11169)
Nine ko values said something other than the English source. The riskiest is
SourceControl.6d7f2a47e5 "Discard folder", rendered as 폴더 삭제 ("delete
folder") next to a sibling delete-untracked action. Others: "Only branches Orca
named itself" read as "branches named Orca"; "staged changes" as 단계적
("phased"); "first-party cloud" as the mojibake 1方클라우드; "discard the
deletion" as "the deletion is deleted"; "Stage all changes" as a sentence
meaning "prepare"; and Recipes as 조리법 (cooking recipes).

EphemeralVmsPane.skillTitle is dropped from the PR's test and override — the
key was renamed to cloudVmSkillTitle on main, so the assertion would resolve to
undefined.

Co-authored-by: ShinSungkyu <kxu4583@naver.com>
2026-08-04 03:56:33 -07:00
Iris-Fla d4f2ae1454 fix(i18n): correct ja Push/Pull button translations (#12301)
The source-control primary action rendered 押す ("press") and 引く ("pull a
physical object") for Push/Pull. プッシュ/プル match the sibling フォースプッシュ
and 同期 labels. A guard test pins both so bootstrap re-translation cannot
silently regress them.

Co-authored-by: Iris-Fla <103801589+Iris-Fla@users.noreply.github.com>
2026-08-04 03:56:33 -07:00
MumuTW 38490ee6c1 fix(i18n): localize browser load-failure and certificate copy (#10672)
The browser.loadFailure.* keys were still raw English in es/ja/ko/zh. en.json is
untouched; every {{value0}} token and the Orca/HTTPS brand terms are preserved.

13 of the zh keys were already covered by #12368, so only the 6 it did not
reach are taken here.

Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com>
2026-08-04 03:56:33 -07:00
闲人andjake 1b1743c3e0 fix(i18n): translate remaining zh.json strings (#12368)
185 zh values were still verbatim English, plus the 6 VoiceMicrophoneSetting
keys were missing. Placeholders, "X of Y" counts, and key ordering are
unchanged; the only edits to already-translated values are punctuation.

Applied at key level rather than as a branch merge — the PR was cut from an
older base and conflicted only on JSON context, with no value drift against
main.

Co-authored-by: 闲人 <38777313+qiuyongjin@users.noreply.github.com>
Co-authored-by: jake <qiu5630@163.com>
2026-08-04 03:56:33 -07:00
Neil 5adc5d06c8 fix(i18n): pin the Orca Mobile "New" badge override and correct ja
The badge value is pinned in locale-key-overrides.mjs, so the ko/zh fix from
#10664 would have been reverted by the next catalog repair. ja carried the same
defect — 新規 reads as "create new" — and is corrected alongside.
2026-08-04 03:56:33 -07:00
Jinjing 1cc84b9f6f fix(i18n): correct the ko and zh Orca Mobile "New" badge (#10664)
SidebarNav.c86d83b5c3 is the onboarding pill rendered beside Orca Mobile, so
"New" marks a new feature. Both locales had translated it as a create action —
ko 새로 만들기 ("create new"), zh 新建 ("create new") — which reads as a button.

The PR's other hunk (zh GH PR) already landed on main and resolved to a no-op.

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-04 03:56:33 -07:00
5Hyeons eed74724ac fix(i18n): localize automation contextual tour (#12270)
The shared Automation tour copy was rendered without passing through
translate(), and the overlay surface hardcoded its default Next and Done
labels. Copy is keyed off the step id rather than its position, so inserting a
step ahead of them cannot shift the text onto the wrong step.

Co-authored-by: 5Hyeons <ohs2251@naver.com>
2026-08-04 03:56:33 -07:00
조재중 6bf34421d2 fix(i18n): localize the Link Routing description in browser settings (#9444)
The description was assembled from a bare template literal, so it stayed
English under every language pack. It now lives in the catalog as two entries —
description with a {{shortcut}} placeholder and descriptionBase for the
invert-on variant — following the copy-module pattern from #10991.

Co-authored-by: 조재중 <126754298+m-a-king@users.noreply.github.com>
2026-08-04 03:56:33 -07:00
Evgenii 4e8f136052 fix(i18n): localize the integration card status pills (#12208)
Every card under Settings -> Integrations built its status pill from bare
literals inside the JSX call, so the pill stayed English beside a translated
description. audit-localization-coverage.mjs inspects JSX attributes and object
properties, not conditional expressions handed to a prop, so the gate stayed
green while the strings shipped untranslated.

The three token-configured cards share tokenProviderStatusLabel(); Gitea passes
optional: true because it works read-only without a token, which is why its
unconfigured state reads Optional setup rather than Not configured.

Co-authored-by: Evgenii <kumiro@me.com>
2026-08-04 03:56:33 -07:00
Evgenii 439a8c46cf fix(plugins): let language packs translate plugin chrome (#12455)
protectedTranslation refused every language-pack key under
auto.components.settings.plugin*, which caught 104 keys that carry no trust
meaning — section titles, empty states, Refresh, Add path. A 35-path exact
allowlist opens those while consent, provenance, and every *Failed string stay
protected; anything new stays protected until it is added deliberately.

PluginsSettingsSection.experimental is held back from the contributed
allowlist: the "Experimental" chip is a trust badge, which the module's own
boundary comment places out of scope.

Co-authored-by: Evgenii <kumiro@me.com>
2026-08-04 03:56:33 -07:00
Evgenii 17d88e098d fix(i18n): format the stats date with the selected UI language (#12105)
formatTrackingSince() used the OS locale rather than the language chosen in
Settings. getIntlLocale() resolves the active i18n language to a BCP-47 tag,
mapping the synthetic plugin<hex> resource language that Intl rejects back to
the pack's real locale.

Co-authored-by: Evgenii <kumiro@me.com>
2026-08-04 03:56:33 -07:00
5Hyeons 02afd3557c fix(sidebar): keep the toolbar labels in the selected UI language (#11889)
SidebarToolbar is a React.memo boundary whose props are shallow-equal on a
language switch, so without its own useTranslation() subscription it kept the
English copy it rendered at boot — the persisted locale is applied
asynchronously, after the lazy catalog loads.

Co-authored-by: 5Hyeons <ohs2251@naver.com>
2026-08-04 03:56:33 -07:00
Jinjing 7c4c021aa6 docs: update Android APK link to v0.0.37 (#12515) 2026-08-04 03:12:23 -07:00
github-actions[bot] 9058059154 Update README downloads badge 2026-08-04 09:51:50 +00:00
2548b816c0 Keep the app responsive when security software slows process creation (#12217)
* fix(ports): keep the app responsive when security software slows process creation

Orca ran the workspace port scan's probe commands (lsof/ps on macOS,
netstat + powershell.exe on Windows) directly in the Electron main process.
libuv performs process creation inline on the calling event loop, which in
the main process is the browser UI thread, so an endpoint-security module
hooking CreateProcessW froze the whole window for the length of the spawn.

The same stall also produced a false diagnosis: the 4s command watchdog was
armed before execFile (local-workspace-port-scanner.ts:389 -> :410), so its
deadline had already passed by the time the command started. Every scan on a
hooked host reported a command timeout, tripping the 60s -> 5min backoff and
the "Port scanning is temporarily paused after a command timeout" banner even
though the commands themselves were healthy.

Probe commands now run on a lazily created, unref'd worker thread with FIFO
one-at-a-time dispatch, and the watchdog is armed after execFile returns so it
measures the command rather than the spawn. Node's own execFile timeout kill
(killed: true) is classified as a command timeout, keeping the backoff working
for genuine hangs. A scan that observes a stalled spawn skips its optional
metadata commands for that cycle, capping a hooked-host scan at roughly one
stall instead of three.

Closes #11161

* fix(ports): keep advertised URLs when a stalled spawn skips port metadata

Review follow-up on #11161. The stalled-spawn early return handed
scanWorkspacePorts raw ports with no cwd/commandLine, so every port failed
attribution and reconcileAdvertisedUrls told the watcher each worktree's
listeners had vanished. shouldEvictAfterScan then deleted every cached
advertised URL and broadcast a removal event; those URLs are only ever
captured from live PTY output, so the dev-server link was gone until the
server restarted.

The scanners now report metadataAvailable, and reconciliation is skipped for
a scan that never gathered attribution evidence. The skip is also no longer
self-perpetuating: on an EDR-hooked host every spawn stalls, so gating purely
on the current scan's spawnMs made every port permanently external (Stop
refused with 'Only workspace-owned local processes can be stopped here.').
Metadata is now re-probed on the scan after a skip, matching what the comment
and test name already claimed.

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

* test(windows): stop a temp-dir lock from failing the CLI launcher smoke test

The native launcher assertions passed on windows-latest, but teardown's
rmSync raced Windows' release of the image handle on the exe the test had
just executed and threw EPERM, failing the job.

Cleanup now retries and, on Windows only, tolerates a residual lock code
instead of reporting it as a launcher regression.

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

* fix(ports): scope the metadata skip away from attribution-dependent scans

The metadata skip was a process-wide parity flag, so Stop and the
localhost-label allowlist could land on a degraded cycle and reject a
port the panel had just shown as workspace-owned. Give those callers an
explicit requireMetadata option, and carry the previous cycle's listener
metadata forward so a skipped background scan no longer republishes
workspace ports as external.

Also pin the watchdog ordering: the stall in the execution test was
shorter than the watchdog budget, so a watchdog armed before execFile
still passed.

* build: guard worker-thread entries against electron imports (#11161)

Electron's module is not registered on worker threads, so
require("electron") throws "Cannot find module 'electron'" inside a
main-process worker and kills it at startup (verified on Electron 43.1.0).
plain-node-entry-guard covered only forked plain-Node entries, so the five
worker entries relied on hand-written "must stay electron-free" comments.

The port-scan probe worker is one import away from
port-scan-command-client.ts, which deliberately contains require('electron').
A violation there fails closed at runtime while every unit test still passes,
because the client's require is try/caught on the main thread.

Covers stt-worker, warp-theme-parser-worker,
session-scanner-opencode-sqlite-worker-entry, main-thread-hang-watchdog-entry
and port-scan-command-worker-entry. The scan is transitive over the emitted
chunk graph, so a shared chunk that reaches electron is caught too.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>

* test(windows): retry teardown for main's duplicate-PATH launcher fixture

Main's new csc-compiled harness runs an exe from the temp tree, which is
exactly the image-handle/AV lock the merged-in removeFixtureTree retry exists
for; its bare rmSync would report a teardown lock as a launcher failure.

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

* test(ports): pin the packaged-asar worker entry path

resolveWorkerEntryPath's packaged branch never runs in dev or e2e, so the path construction had no coverage. Split the electron read out of it and unit-test both layouts.

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

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 02:03:40 -07:00
Neil e1071f59e9 Say why an update install failed instead of stalling for three minutes (#12224)
Surfaces the real install-failure cause instead of letting a failed elevation stall silently, and keeps the reconnect wait inside its total budget by recomputing the remaining time after each awaited RPC.

Relates to #11906 — this fixes the observability half. The functional half (a .deb/.rpm host cannot elevate and can never self-update) is unchanged, so the issue stays open.
2026-08-04 02:02:53 -07:00
Jinwoo HongandOrcaWin e9cf106769 fix(relay): make fenceAndCloseNow stop the liveness safety net (#12482)
The 5-minute liveness tick from #12432 survived fenceAndCloseNow(), so a
tick landing between the pre-sign-out fence and the profile wipe could
briefly resurrect a broker (benign but soft — the entitlement check
bails afterward). The fence now clears the interval; the next auth
mutation re-arms it via refreshDemand, and the safety net otherwise
behaves identically. Found in release review of the #12432 cherry-pick.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 01:38:10 -07:00
Jinjing 00867f06e2 fix(ssh): handle owner displacement and graceful shutdown (#12367)
* fix(ssh): handle owner displacement and graceful shutdown

SSH connections can reconnect with valid session proof after network loss or
device sleep. When the incumbent owner is still half-open, allow the
reconnecting client to displace it outright rather than wait for socket closure
— a window that may never close. Retain displaced deliveries for the new owner
to rotate. During app shutdown, drain SSH sessions without terminating recovery
operations, and retry pending owner grants in case a replacement commits mid-drain.

* fix(ssh): handle owner displacement and graceful shutdown

Make QuitTeardownStartGate a shared singleton so SSH connects use the
same shutdown fence as the main quit path. Track test-connection probes
to ensure they complete before final teardown. Guard owner displacement
to prevent stale owners from clearing recovery state claimed by newer
owners.

* fix(ssh): fix flaky test sync and add error code safety check

Test was using tick-based Promise.resolve() loops which don't guarantee
the async operation has started. Replace with signal-based synchronization
that waits for the actual lease flush. Also add nullish-coalescing to
error code check to prevent crashes if error is null or undefined.

* fix(ssh): fence reset transport opens during shutdown

* fix(ssh): keep recovery leases stable across reconnects

* fix(ssh): close transports owned by cancelled connect attempts

When a connect is cancelled after its transport has opened, that cancelled
attempt still owns the transport and must close it — otherwise it leaks. Add
disconnectConnection() to close by identity (not by target ID) so a cancelled
attempt closes only the transport it minted, without tearing down its
replacement's live transport. Track priorConnection to detect whether this
attempt opened a new transport or reused an existing one, and close only on
abandonment if this attempt owns the session.

* fix(ssh): fence old owner proofs and close superseded transports

When an owner reconnects with a new proof while an old one is still live,
the old proof is now fenced with SUPERSEDED_ERROR instead of retrying
indefinitely. The relay also closes stale transports to signal that their
recovery generation has been overtaken by a newer one.

This ensures overlapping reconnect scenarios complete with the newest proof
rather than getting blocked by stale recovery attempts.

* fix(relay): re-pin stdin/stdout fds after closing to prevent recycling

When the relay closes stdin/stdout to signal EOF to the SSH peer, the OS
can recycle those fds (0 and 1) for new sockets or files. If Node still
treats process.stdin/stdout as those numbers, subsequent operations
corrupt socket clients and trigger shutdown errors. Re-pin the fds by
opening /dev/null to keep them occupied and prevent recycling.
2026-08-04 01:37:25 -07:00
Neil 8f29f58ab7 Keep showing your workspaces when a project is too slow to answer instead of reporting zero (#12222)
A stalled per-repo git scan no longer publishes a healthy-looking empty catalog. Adds an execution-host ownership gate so a degraded host cannot republish another host's worktree rows under its own id.

Relates to #11869 — this fixes the stall-publishes-zero half. The issue stays open for the remainder.
2026-08-04 01:26:52 -07:00