Commit Graph
7507 Commits
Author SHA1 Message Date
github-actions[bot] c8dba6d72c release: v1.4.160-rc.5 v1.4.160-rc.5 2026-07-29 01:36:20 +00:00
Jinwoo HongandOrcaWin 0660ad9d6e fix(orchestration): reject legacy mail acknowledgment (#11227)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 18:27:52 -07:00
NeilandOrca 5c59c84c7a fix(plugins): close four trust-boundary holes in the plugin system (#11232)
* fix(plugins): close trust-boundary holes in the plugin system

Move five security decisions to their chokepoints rather than leaving them
enumerated at individual call sites.

- Kill-list revocation reaches content packs: PluginContentPackRegistry now
  takes an isKilled predicate and intersects it with any caller-supplied
  approval, so a killed plugin's VM recipes can no longer reach
  spawn(..., { shell: true }) through either reconcile() call site.
- Bound kill-list generatedAt to a 24h future skew at the parse chokepoint.
  A far-future timestamp previously made every genuine later list look
  "older" and disabled revocation permanently, persisted across restarts.
- Protect the whole auto.components.settings.Plugin* translation subtree
  instead of an enumerated prefix list, so language packs cannot forge the
  consent provenance badge or rewrite install-error security copy.
- Resolve manifest panel icons by own-key only; "constructor"/"__proto__"
  previously yielded non-component prototype members that crashed the
  right sidebar to its error boundary.
- Give panel liveness frames a reserved control budget so a panel that
  saturates its action budget can still answer the watchdog.

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

* fix(plugins): keep the kill-list future bound off the cache read path

The schema-level generatedAt bound re-judged the on-disk cache against the
device clock at every launch, so a client whose clock ran behind the last
genuine publication discarded its whole cached kill list and started with
zero revocations. Move the bound to the two fetch chokepoints instead.

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

* fix(plugins): remove the reserved-lane starvation window and the revocation TOCTOU

Review follow-ups on the trust-boundary fixes:

- The reserved liveness lane had a per-window count equal to the ping
  interval, so a panel's own pong-shaped traffic could spend it and drop
  the next genuine reply — reintroducing the starvation the lane exists to
  prevent. The lane is now size-bounded only; rate stays bounded because
  every pong is also charged to the data budget.
- Only schema-valid pongs take the lane now, so near-miss pong-shaped junk
  cannot drain it. readPanelPongId replaces the zod parse on this
  guest-controlled path (a rejected safeParse allocates an issue list, ~90x
  the accepted-path cost) and is pinned to the schema by a parity test.
- Re-read the kill list inside approveAtomically: approvedKeys is snapshotted
  before an awaited verification phase, so a plugin killed during that wait
  could still publish VM recipes and language packs.
- Assert the curated icon resolves to FileText; the old equality also passed
  when both sides fell back to Plug.

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

* fix(plugins): match zod's safe-integer bound in the pong reader

readPanelPongId used Number.isInteger, but zod's .int() rejects anything
above 2**53-1, so pingIds like 1e100 took the reserved lane the schema
would have refused. The parity test never probed that boundary.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:49:20 -07:00
NeilandOrca 3a67186623 fix: stop notification loss, credentialed cache reuse, and clipboard clobber (#11230)
* fix: stop notification loss, credentialed cache reuse, and clipboard clobber

Mobile catch-up (#8591): fetchMissed swallowed the RPC failure while
deliverLive kept advancing and persisting lastDeliveredSeq, so the next
successful catch-up asked from above the abandoned range and the desktop
cut it. Sessions are module-scope, so an unchanged epoch never resets it.
Quarantine the watermark at the last contiguously-delivered seq and hold
it there until some later catch-up actually drains — not just one retry.
A batch cut short by a teardown quarantines at the last event it settled.

Jira attachment cache: currentEpoch summed two independent counters, so a
site at siteEpoch 1 read the same value before and after a global clear.
The mid-flight guard passed and re-inserted credentialed image bytes that
"disconnect all" had just purged — resident for the process lifetime since
pruneExpired has no timer. One monotonic ticker, compared by max.

Web copy fallback: the handler registered in the capture phase, so xterm's
bubble-phase listener overwrote text/plain with the terminal selection
afterwards; served was already true, so the copy reported success. Every
Orca copy affordance over plain HTTP (Copy Pane ID, Copy Path, commit SHA,
PR URL) pasted the terminal selection. Bubble phase with
stopImmediatePropagation. Covers the secure-context retry branch too,
which shares the same helper.

* fix: roll back the persisted watermark on catch-up failure; cover stopImmediatePropagation

Adversarial review of a98d7f4d5d found two gaps.

1. The quarantine clamped only writes made AFTER the failure. getMissedSince
   waits up to 30s, so a live event routinely persists a higher seq while the
   request is still outstanding; that value stayed on disk, and the next launch
   read it back and resumed past the abandoned range -- the original bug,
   reached through a restart. quarantineCatchUpWatermark now re-persists the
   clamped seq, so the stored value never outlives the gap it guards.

2. web-clipboard-copy-terminal-selection's second test registered its "late"
   document handler BEFORE the fallback's, so it lost on registration order
   alone and stopImmediatePropagation was never exercised -- the test passed
   with that line deleted. Bubbling reaches the document before the window, so
   a window-level listener is what actually requires it.

* fix(mobile): mark a notification seen only once its show lands

A pre-marked seen key made a rejected show unrecoverable: the next
catch-up re-fetched the seq and the dedup guard dropped it, and the
first later event to drain the batch lifted the quarantine past it.
Also contains the rejection so it does not escape the un-awaited
'ready'/live handlers as an unhandled rejection.

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

* test(web-clipboard): pin stopImmediatePropagation with a same-target handler

Both existing cases passed with plain stopPropagation, and with the listener
back in the capture phase — neither half of the fix was actually pinned. The
window-level clobber is on a different target, so stopPropagation suppresses
it too. Registering the clobber on the document, ordered after the fallback's
own listener, is the only shape stopPropagation cannot cover.

Addresses the review comment posted after the last commit.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:38:31 -07:00
Brennan Benson 681c4ba458 fix(skills): stop calling the updater's own install a modified copy (#11249)
After a successful headless update the CLI installs source-repo HEAD,
which legitimately runs ahead of any shipped bundle. The scan classified
those bytes 'unrecognized', so the row went amber ('may be modified…
remove it') seconds after our own Update button ran, and the advice
looped: remove + reinstall lands the same newer content.

Scan half: a canonical/alias placement whose observed git tree sha
equals the updater lock's skillFolderHash is the CLI's own install, not
a user edit — reclassify it 'newer-known'. Display half: 'newer-known'
is recognized official content ahead of this build with nothing to fix,
so it no longer marks the copy blocked. Eligibility is deliberately
unchanged: ahead of the bundle means there is nothing this build can
update to, and offering one risks the provably-unperformable update
(#11110) when source HEAD still equals the lock.

Copies whose sha does not match the lock, copies with no lock entry,
same-name copies outside the placements the command writes, and
plugin-cache behavior all stay flagged exactly as before.
2026-07-28 17:27:26 -07:00
NeilandOrca 1d7e7656e3 fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales (#11241)
* fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales

7.1 ui.set rejected whole preference payloads on enum drift. The new
AssertNoMissingKeys guard is key-only, so it could not see that
LegacyWorktreeCardProperty omitted 'cli' (in DEFAULT_WORKTREE_CARD_PROPERTIES)
or that rightSidebarTab omitted 'workspaces'/'pr-checks' and every plugin tab.
UiUpdate is .strict(), so one bad value failed the entire batch and silently
dropped sidebarWidth/groupBy/sortBy/filterRepoIds riding the same debounced
write. Both enums now derive from the shared unions, AssertNoMissingValues
catches value drift by name, and UiUpdate drops an unknown value instead of
rejecting the batch around it. Unknown KEYS still reject.

7.2 The SSH shell-ready fallback moved from first-output to spawn, so a remote
shell needing >1.5s to prompt got the bracketed-paste startup command before
readline armed it, with no recovery afterward. The short deadline now applies
only once output proves the shell is talking; a silent-since-spawn shell gets a
longer budget and still delivers eventually.

7.3 The project picker armed in rank order but rendered in section order, so
with a folder group present the BOTTOM row was armed on open and Enter created
the workspace in the wrong place. Row keys now derive from the same sections
that render. The folders bucket also gains the recent-exclusion guard the
projects bucket has; that duplicate was unreachable, so this is symmetry, not a
live bug fix.

7.4 setBrowserPageZoomLevel now compares before writing, so a pane reasserting
a level the host already holds no longer emits a redundant host-wide
HostZoomMap write. The user-applied level also moved to a module-level map
keyed by page id: the guest webview outlives its React pane, so the pane-local
ref re-seeded from the shared Settings default on every remount and let a later
default retroactively hijack an already-zoomed tab. See PR notes on the part of
this finding that could not be fixed as prescribed.

7.5 A non-null sessionId short-circuited the live-work escape hatch, forcing
'loading' over hook 'working' and rendering an idle pane mid-turn: Send instead
of Stop, no typing indicator, no streaming preview. Status stays 'working'; the
empty-transcript loading SURFACE moves to selectNativeChatViewState, which keeps

7.6 #10770 merged from a base predating #8549, reverting 182-187 translated
strings per locale to English (es 182, ja/ko/zh 187) plus en.json's recipesHelp.
Restored by script, only where the English source is unchanged between the two
shas, so later legitimate edits are preserved: 0 keys added or removed, every
value sourced from 97e4776dfe, and the four other English-source changes since

7.7 Match highlighting indexed by UTF-16 code unit but rendered by code point,
so an emoji-named folder showed marks one glyph late. Cosmetic.

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

* fix(ssh): keep fast startup delivery on the short fallback deadline

The 15s no-output budget added for the shell-ready fallback was applied to
every SSH launch, including 'fast' delivery. Fast delivery waits for no
marker and pastes nothing prompt-sensitive, so it gained a 10x startup
delay for nothing.

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

* fix(rpc): generalize the ui.set value-parity guard to every shared key

Naming worktreeCardProperties and rightSidebarTab left the next field to drift
exactly as unguarded: dropping 'pr-status' from groupBy typechecked clean.
Check the value domain of every shared key instead, against z.input (what a
client may send) rather than z.infer (post-transform).

Also move the pure mergeNativeChatLiveSession suite beside the module it covers;
the hook's test file owns an IO harness and was at the max-lines cap.

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

* fix(i18n): re-apply only the locale strings still reverted at HEAD

#10770 merged from a base predating #8549, so its stale locale copies
overwrote ~185 already-translated strings per locale back to English. A
present catalog value always beats the English translate() fallback, so
those strings render English with nothing to signal the loss.

Since that finding was written, #11205 and other upstream translation
passes independently re-covered most of ja/ko/zh. Replaying #8549's
catalogs wholesale would now overwrite that newer work, so this re-applies
a key ONLY where all of the following hold at origin/main: it was
translated at 97e4776dfe, #10770 reverted it, its English source is
unchanged since, and no upstream commit has touched it since the revert.

  es 182  ja 32  ko 17  zh 55

Everything else is left to upstream. Verified zero upstream translations
reverted: every changed key still matches its #10770 value at main. Keys
upstream deleted are not resurrected, and keys whose English source was
edited since are skipped as legitimate source changes rather than reverts
(this is what keeps zh CPU on #11205's deliberate "CPU" over #8549's
"中央处理器"). Key count and order are unchanged in all five catalogs.

en.json's own recipesHelp was reverted by the same stale base and no
upstream commit has touched it since, so it is restored to match the live
source at EphemeralVmsPane.tsx:252.

* fix(rpc): keep null in the ui.set value-parity guard

NonNullable stripped null as well as undefined, so dropping .nullable()
from a `| null` field passed the guard while still rejecting the batch at
runtime -- the exact drift class the guard exists to catch. Proven: making
visibleWorkspaceHostIds non-nullable typechecked clean before, now errors
by name. Also pins the 15s silent-shell budget so it cannot silently
shrink back toward the short deadline.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:25:05 -07:00
NeilandOrca a721125d06 fix(perf): correct three 07-27 perf regressions (#11234)
* fix(perf): correct three 07-27 perf regressions

Traversal capacity cap no longer scales with worker concurrency
(#11026). retainWorkspaceSpaceScanEntry charged a traversal-wide entry
counter, so N workers each holding a listing multiplied the live charge.
At concurrency 48 a 48x2,100 tree (100,848 entries) hit the 100,000 cap
while 100x1,500 (150,100 entries, 50% more) passed, and scanLocalWorktree
treats the capacity error as terminal, reporting an intact worktree as
"Unavailable" with sizeBytes 0. The cap is now per directory listing --
the only quantity fixed by directory shape -- restoring the invariant
docs/workspace-space-scan-resource-bounds.md already states. Aggregate
live retention stays bounded by the unchanged 64 MiB byte cap.

Note: releasing each entry's charge at dispatch (the originally suggested
fix) was measured and does not help; the peak is set at admission, before
any entry is dispatched.

Repo image icons are no longer fully base64-decoded on every snapshot
publish (#11012). sanitizeRepoIcon reached decodeBase64Prefix, which
sized its buffer to the whole payload to read a 24-byte header, running
synchronously inside ipcMain.handle at a 250 ms throttle. Validation is
now memoized on source+src in a BoundedMap. Measured for 10 icons x
256 KB: 37.34 ms -> 0.67 ms per publish.

One over-long card label no longer discards the entire snapshot (#11012).
isDashboardSnapshot was all-or-nothing and dashboard-popout returned
early with no log while replaying lastSnapshot, so `orca terminal rename
--title "<1025+ chars>"` froze the pop-out board on its last good paint
with nothing surfaced. Labels are truncated at the producer, the
validator drops only the offending card, and both the rejection and the
drop are logged. The bound now lives in the shared snapshot contract so
producer and validator cannot drift.

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

* fix(perf): charge a scan listing's parent path once, not per entry

The 4.1 fix made the entry cap per-listing but left the 64 MiB byte cap
charging parentPath.length for every entry in a listing. Because a
listing's entries all share one parent-path string, that multiplied the
path by the directory's width, so the byte cap measured checkout depth
rather than live heap.

The reported symptom therefore still reproduced at the production default
limits: 48 x 2,100 @ concurrency 48 raised a capacity error once the
worktree path passed ~58 characters, while the same layout at concurrency
1 succeeded. The shipped regression test could not see this because it
passes maxRetainedBytes: Number.MAX_SAFE_INTEGER, disabling the only cap
still in play. Measured at a real 65-char worktree root, 3 of the report's
4 documented layouts still failed.

The parent path is now charged once per listing, with its first entry, so
an empty listing strands no charge. Per-entry overhead is unchanged at
512 B + name, which still dominates the estimate, so the OOM protection
the original PR added is preserved.

Adds a production-default-limits case covering the report's layouts under
a deep root, plus an assertion that a short and a deep root reach the same
verdict -- the path independence docs/workspace-space-scan-resource-bounds.md
requires and which no existing test enforced.

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

* fix(perf): prove the icon cache by decode count, not wall clock

The caching test asserted a per-publish millisecond budget, which failed
on CI at 5.64 ms against a 5 ms ceiling. Any threshold flakes on a loaded
box, so count real sanitizeRepoIcon entries instead: 10 repos x 20
publishes is 200 icon checks against exactly 1 decode. Added cases pin
the cache key (payload and source both re-decode; a cached image verdict
never answers for an emoji) and that a rejection is cached too.

Also drops budget.entries, which the per-listing cap left as a
traversal-wide counter no check reads -- exactly the shape a future
guard could reintroduce the concurrency bug from.

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

* fix(dashboard): bound the project filter label the whole board rides on

#11042 added snapshot-level filterOptions whose project labels are
repo.displayName -- the same unbounded source this PR already bounds for
card.repoName, but one level up where dropping a card cannot recover it.
An over-long project name would fail isDashboardFilterOptions and take
the entire snapshot with it, which is the exact frozen-board failure the
per-card drop was added to end. Workspace-status labels are already
capped at 32 by workspace-statuses.ts, so only projects needed this.

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

* fix(dashboard): disambiguate the repo icon cache key

The memoization key joined `source` and `src` with a space, but the
sanitizer's base64 pattern admits whitespace inside a valid `src`. A
rejected icon can therefore split the same concatenation differently and
inherit an accepted icon's cached verdict, reaching the pop-out's
`<img src>` without ever being sanitized. Length-prefix the source.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:08:14 -07:00
Brennan Benson 2db02562b6 fix(skills): tell the user how to fix a skill the updater cannot converge (#11248)
Re-lands #11129, which was merged into #11128's branch rather than main and
so never reached main. Content is identical to the reviewed and live-QA'd
head ac5ec5b0b0 (1775d83cf6 + ac5ec5b0b0, minus the intermediate merge).
2026-07-28 16:57:51 -07:00
Brennan Benson bb9ae78136 fix(macos): show TCC notice after first prompt (#11243) 2026-07-28 16:51:59 -07:00
Brennan Benson f790d9cbe8 fix(skills): stop the skill review dialog contradicting the badge that opens it (#11128)
* fix(skills): stop the skill review dialog contradicting the badge that opens it

A skill whose only fault was an edited copy or one Orca could not read turned the
setup-rail badge amber and offered Details — and Details opened a dialog headlined
"All installed Orca skills are up to date." over an empty list. The badge says
something is wrong, the dialog it points at says nothing is.

The grouping only returned skills with an out-of-date copy, so those two states
produced no row and the summary fell through to the all-clear headline. Include a
skill when a copy needs attention as well, using one shared predicate so the badge
and the dialog cannot disagree again. A plugin's own copy of a same-named skill
stays out: that is the vendor's, not the user's drift.

* test(skills): pin that a routine outdated copy raises no attention marker
2026-07-28 16:49:18 -07:00
Brennan Benson 747b241145 feat(main): record main-thread hangs so we can measure them (#10256)
A deadlocked main thread never crashes, so it leaves no crash report and no
artifact — incidence has been unmeasurable (n=1 confirmed, macOS 26.5.1,
FB24004458 / electron#52437). This forks a plain-Node watchdog sibling under
ELECTRON_RUN_AS_NODE that survives the deadlock, listens for a 2s heartbeat,
and after 45s of silence writes a marker to userData. The next launch consumes
it, records a durable crash breadcrumb, and emits a main_thread_hang_detected
telemetry event carrying unresponsive_ms and self_recovered.

Observes only — it never kills or relaunches the parent. A true positive
recovers nothing force-quitting wouldn't, while a false positive would SIGKILL
a live main thread mid-write. self_recovered counts exactly the stalls such a
killer would have gotten wrong, so recovery can be built on evidence if the
field numbers justify it.

macOS-only, packaged-only (ORCA_HANG_WATCHDOG_FORCE=1 to test), with sleep-gap
suppression and idempotent shutdown on will-quit.
2026-07-28 16:43:30 -07:00
Jinwoo HongandOrcaWin a6423d565b fix(macos): prevent stale UI surfaces after wake (#11226)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 16:37:13 -07:00
waryanandClaude 638c3ca5d9 fix(agent-status): prevent ghost sidebar row on completed split-pane detach (#10698)
* fix(agent-status): prevent ghost sidebar row on completed split-pane detach

Detaching a done-state split pane into its own tab migrated the agent
paneKey from oldTab:leaf to newTab:leaf. useRetainedAgentsSync only saw
the old key vanish and, finding no suppressor, resurrected it as an
unclickable duplicate sidebar row (and inflated the worktree count).

Plant a one-shot retention suppressor on the source key during
transferAgentPaneAuthority, but only when the source actually held a
live agent, so a suppressor is never leaked for a pane that had none.

Fixes #10675

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agent-status): annotate suppressor record type and condense retention comments

Type the migrated retentionSuppressedPaneKeys as Record<string, true> so a
computed-key `true` isn't widened to boolean, which broke the web typecheck.
Also condense the retention rationale comments per review.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-28 16:26:40 -07:00
Brennan Benson d3681f6306 fix(runtime): surface desktop RPC startup failures (#11037)
* fix(runtime): surface desktop RPC startup failures

* fix(runtime): isolate RPC failure telemetry

* fix(runtime): satisfy the changed-code quality gate and kill vacuous dialog tests

The `no-floating-promises` label span covers the whole `app.whenReady().then()`
callback, so adding lines inside it made a long-standing finding overlap changed
code. `void` is the linter's own suppression; no `.catch()` on purpose.

The startup-failure tests were vacuous: mutation runs showed the wait-for-show
deferral, the destroyed-window guard, the `closed` companion event, listener
cleanup, the cause walk, the cycle guard, and the truncation bound could all be
deleted with every test still green. The "not called yet" assertion ran before
any microtask, so it passed either way.

* test(runtime): de-brittle the desktop RPC-failure source assertions

Anchoring the slice on the full destructure and matching the whole dialog
call expression made an innocuous rename break the test with a cryptic
'expected -1'. Match the shape that is actually the contract instead.

* test(runtime): repair the silently-unbounded desktop startup slice

The desktopEnd anchor comment lost a word in 98b00d3a64, so indexOf returned
-1 and slice(start, -1) covered index.ts to EOF. Moving the dialog call to a
path that never runs at startup still passed. Anchor on code instead, and
assert both bounds so a future reword fails loudly.

* test(runtime): bound the attach anchors in the startup ordering slice

Round 3 bounded the desktop pair but left attachStart/attachEnd unguarded in
the same test: deleting the PTY startup barrier from attachMainWindowServices()
and breaking the rateLimits.attach(window) end anchor still left the case green.

* test(startup): bound the last two unguarded slice anchors in this file

Rounds 3 and 4 fixed the desktop and attach pairs; two instances of the same
class survived in the same file, both proven vacuous by mutation:

- it #3 never bounded readyEnd. Renaming the `pairing:` payload key makes it
  -1, widening readyPayload from 372B to ~52KB. Moving the reconciliation
  status out of the serve-ready payload (its whole point) but leaving it later
  in index.ts then kept all 6 cases green.
- it #2 bounded desktopWindowStart against reconciliationStart rather than
  serveEnd. An earlier `Promise.resolve(openMainWindow())` steals the anchor,
  collapsing desktopStartup to '' while every existing guard still passes, so
  its only assertion — a negative — succeeds against an empty string.

Both mutants now fail. `src/main/ipc/pty-startup-barrier-ordering.test.ts:11`
has the same latent shape; left alone as out of scope for this PR.

* fix(runtime): keep walking the cause chain past an unmapped code

getErrorCode returned the first code it found, so an outer wrapper carrying
an unrecognised code masked a nested EACCES/ENOSPC and classified it unknown.
Only a mapped code ends the walk now; every other input classifies as before.

Unreachable today (writeSecureFile rethrows raw fs errors with .code intact),
but the classifier's job is surviving whatever error shape reaches it.

* fix(runtime): tell the user what to fix, not just to restart

The dialog's only advice was "Restart Orca to try again", which is true for
address_in_use and wrong for the rest: permissions, a full or read-only disk,
and a missing data folder all survive a relaunch, so the user restarted, hit
the same failure and had no next step.

Route the error class we already compute into the copy so each cause names the
thing the user has to change. Guidance and telemetry now derive from the same
classifier, so they cannot drift apart.

* fix(runtime): guide users through long RPC paths

* fix(runtime): avoid false window listener warning

* fix(runtime): guard destroyed window before web contents
2026-07-28 16:05:05 -07:00
Brennan Benson 25da91d653 perf(dashboard): stop re-sending repo icon data URLs on every republish (#11089)
* perf(dashboard): stop re-sending repo icon data URLs on every republish

#11012 put repo icons on the dashboard snapshot keyed by repoId. Image icons
are data URLs capped at MAX_REPO_ICON_DATA_URL_LENGTH (400KB) and every repo
contributing a card ships one, while the snapshot republishes up to 4x/sec
(PUBLISH_THROTTLE_MS = 250) for as long as the pop-out is open. Icons change
about never, so that structured-clones megabytes per second across the window
boundary for bytes the pop-out already has.

Publish the map only when it actually changed, comparing by reference since
icons come off immutable store repo records. The two paths where the pop-out
could be starting from nothing — it opened, or it mounted and asked — still
force a full send, so the retained copy can never be the only one.

The pop-out keeps the last map it was given when a republish omits the field.
An explicitly empty map still clears, so removing an icon works.

repoIconsByRepoId was already optional on DashboardSnapshot and
isDashboardRepoIcons already returns true for undefined, so the main-process
validator needed no change.

* fix(dashboard): keep repo icons in the main-process snapshot cache

The bridge now omits an unchanged repoIconsByRepoId from republishes, so the
cached snapshot main replays to a mounting pop-out could be icon-less, blanking
the board's repo glyphs until the forced publish landed. Carry the last map
into the cache; the forwarded payload is unchanged.

Also covers the forced full sends (open, reopen, snapshot request) that no test
exercised.

* test(dashboard): pin the icon omit on the throttled trailing republish

* fix(dashboard): keep the popout bridge effect off the react-doctor gate

The changed-code quality gate reports react-doctor findings that overlap
added lines, and effect-needs-cleanup spans the whole publish effect — so
this PR's edits inside it turned a pre-existing false positive into a red
static-analysis check. Hoisting the store subscriber leaves the effect
owning one disposable; behaviour is unchanged.

* docs(dashboard): correct why watchSnapshotInputs sits outside the effect

The effect owns four disposables (offOpenChanged, offRequested, the store
unsubscribe, and the trailing timer), not one. State the real reason the
subscribe is hoisted so nobody inlines it back and re-reds the gate.

* test(dashboard): pin that the bridge subscribes only while the pop-out is open

The lazy wiring exists so an enabled-but-closed pop-out costs nothing — a live
subscriber would rebuild a cross-worktree snapshot on unrelated store writes.
Nothing pinned the unsubscribe on close.
2026-07-28 16:00:22 -07:00
Brennan Benson 13c193a00a feat(dashboard): add agent status search board (#11042)
* feat(dashboard): add agent status search board

* fix(dashboard): keep idle controls reachable

* chore: drop merge-only formatting drift

* fix(dashboard): compare sparse subagent snapshots safely

* fix(dashboard): satisfy settings handler lint

* fix(dashboard): address review feedback

* fix(dashboard): complete search and localized status copy

* fix(dashboard): pad active filter row

* fix(dashboard): keep idle control in board settings

* fix(dashboard): source filters from workspace state

* fix(dashboard): clarify PR and MR status filter

* fix(dashboard): preserve review and board parity
2026-07-28 15:51:43 -07:00
Brennan Benson 5753cf6c5c fix(updater): resume background checks after a local build session ends (#11223)
A local-build check (Option+click "Check for Updates" on macOS) pins
activeUpdateSource to 'local' for the rest of the process. The
'update-available' success path never restores it, and
runBackgroundUpdateCheck early-returns on it, so every wake-from-sleep
check, window-focus daily check and nudge poll became a no-op once a
local build reached 'available'. The one-shot automatic timer fired into
that early return and nothing re-armed it, so the scheduling chain died
too and lastUpdateCheckAt froze.

Restoring the source when 'update-available' fires would break the flow
the user just started — the pending download still needs the local feed
and allowDowngrade. Instead the release source is restored when the user
closes the offered card, which main previously never learned about, and
only while status is exactly 'available': downloadUpdate() flips status
to 'downloading' synchronously before it calls into electron-updater, so
this cannot fire once a download is under way.

The automatic timer now re-arms when a check is deferred rather than
launched, so a deferral can no longer end automatic checks for the
process lifetime.
2026-07-28 15:40:31 -07:00
Brennan Benson 3c0cd6069f fix(release): stop packaging plugin authoring examples into app.asar (#11087)
* fix(release): stop packaging plugin authoring examples into app.asar

electron-builder's `files` is an all-negation list, so its default `**/*`
packs anything without an explicit `!` entry. examples/ arrived with the
plugin system in #8549 and never got one, so 1.4.160-rc.3 shipped
examples/plugins/hostile-panel/panel.html — the adversarial fixture the
panel containment tests point at, complete with its fetch-exfiltration
probe — plus hello-orca, inside every user's app.asar. Verified against the
installed 1.4.160-rc.3 artifact, not just the config.

The two orchestration design docs landed at the repo root in the same span
and shipped the same way; fold them into the existing root-doc negation.

Neither has a runtime consumer: bundled plugins ship via extraResources
from resources/plugins/launch/, which is already excluded from the asar
for exactly this reason.

* test(release): assert the examples exclusion through the real file matcher

The added case mapped each negation to a bare top-level token, so it passed
under '!examples/README.md' — a pattern that still ships the whole tree. Drive
app-builder-lib's FileMatcher instead so the assertion matches the test name,
and pin the root anchoring so the negation cannot grow into '!**/examples'.
2026-07-28 15:40:28 -07:00
NeilandBrennan Benson a8126a0a92 fix(macos): explain the TCC prompts, and surface Full Disk Access only to users macOS is prompting (#9756) (#9910)
* fix(macos): add a Full Disk Access nudge to reduce recurring TCC prompts (#9756)

macOS shows the "Orca wants to access other apps' data"
(kTCCServiceSystemPolicyAppData) prompt and it can keep reappearing. The
reappearing loop is not a fixable app bug: it is TCC identity churn — an
unsigned local rebuild mints a new code identity each build, so macOS treats
each as a new app — and Orca's other-app reads are already gated behind opt-in
settings or explicit user actions.

The durable remedy for the population we can help (release users) is Full Disk
Access, a superset macOS grant that stops these prompts for a stable identity.
Surface it with an ambient, dismissable sidebar card that reuses the existing
developer-permissions IPC. macOS-only; probes FDA status at most once per
renderer session (the probe itself reads protected data, so it must not repeat
on focus/remount); "Open System Settings" opens the Full Disk Access pane;
permanent localStorage dismissal.

* fix(macos): stop the FDA nudge promising macOS will stop asking

The card said Full Disk Access makes "macOS stop asking", but the grant
covers this app while terminals are spawned by the detached PTY daemon
(daemon-init.ts forks execPath with ELECTRON_RUN_AS_NODE + detached:true,
reparented to launchd), which macOS treats as its own TCC identity. A user
who followed the card would grant FDA and still be prompted from terminals.
Scope the claim to reducing prompts and name the terminal caveat.

* fix(macos): drop stale focus refreshes in the FDA nudge

refreshFullDiskAccessStatus() applied whichever getStatus() round-trip
resolved last. Rapid blur/focus puts several in flight, so an earlier
pre-grant 'unknown' landing after a newer 'granted' un-hid the card and
also wrote 'unknown' into the module-level session cache, re-nagging a
user who already has Full Disk Access for the rest of the session. The
adjacent FullDiskAccessSetupPrompt already guards this with a refresh
sequence; mirror it here.

Also unmount React roots in afterEach: clearing document.body left them
mounted, leaking each test's window focus listener into later tests.

* test(macos): unmount the StrictMode FDA nudge root between tests

The afterEach unmount added in 5a0f717 only covers roots created through
renderNudge(). The StrictMode probe test builds its own root, so it was
never unmounted and its component stayed live for the rest of the file.
Today that component has no window focus listener, so nothing breaks; add
a CTA click to it and the same contamination 5a0f717 fixed comes back —
the two tests after it see extra getStatus() calls and fail. Register the
root so the fix covers every mount site.

* fix(macos): attribute the FDA prompts to agent activity, not Orca's own reads

The card said the prompts happen "when this copy of Orca reads protected app
data", but Orca's own reads are small and gated; #9756's trigger is agent
find/grep sweeps into ~/Library/Containers, which macOS bills to Orca because
Orca is the responsible process for every terminal child. Blaming Orca reads
as an accusation and hid why FDA works at all — the grant attaches to Orca
rather than to each churning child binary.

Name agents as the trigger, keep the "reduce" hedge and the terminal caveat,
and drop the "this copy of Orca" dev-build hedge that cost a clause. Assert
the causation wording so it can't silently regress.

* fix(macos): explain the TCC prompts on the settings row, drop the sidebar card

The sidebar nudge added in 344d466b was premised on FDA being reachable
"only inside onboarding". It isn't: Settings > macOS Permissions has had a
full-disk-access row all along (searchable), the Setup Guide hosts the same
prompt from both a settings pane and a re-openable modal, and the sidebar
already links to that modal via the "Onboarding checklist" entry. The card
added a fifth affordance to the same sidebar that already had the fourth,
so it bought prominence rather than access - shown to every macOS user
without FDA, most of whom never hit #9756.

Keep the part that was actually new. The settings row still described the
prompts as something projects and worktrees trigger, which is the same
misattribution the card carried: the reads come from the agents Orca runs,
and macOS names Orca only because it is the responsible process for every
terminal child. It also never mentioned that the grant has to cover Orca
Helper, or that the preserved daemon keeps stale TCC state until restart.

Non-English catalogs get the English string as a placeholder; the bootstrap
translators key their cache on the English value, so a changed string is
re-translated on the next run.

* feat(macos): nudge Full Disk Access only after macOS repeatedly prompts

The FDA hint is only worth showing to users macOS is actually prompting.
tccd emits one AUTHREQ_PROMPTING line per consent dialog it displays,
carrying the service and both identities, so a narrow log-stream predicate
detects the real thing without correlating across lines or guessing whether
a dialog appeared. Verified against a captured dialog: the predicate matched
1 line out of 1436 TCC lines in ~28s, because routine preflight checks - the
overwhelming majority of TCC traffic - do not emit it.

Count dialogs where Orca is the responsible process, persist across launches,
and tell the renderer on the third one. The event separates the accessing
binary from the responsible app, which is the crux of #9756, so the toast can
name the tool that triggered it rather than blaming Orca generically. One
toast per user, with a permanent opt-out; it deep-links to the FDA row in
Settings > macOS Permissions rather than restating the guidance.

macOS-only: the watcher no-ops elsewhere, the web client stubs the API, and
the child is killed on before-quit since log stream ignores a closed stdout.

* test(macos): pin the platform so the TCC watcher tests exercise the darwin path

start() is darwin-gated, so on Linux CI it no-opped and the stream/kill
assertions passed vacuously against a watcher that never spawned. Pin
process.platform per the existing convention (shared/secure-file.test.ts),
and cover the gate itself with an explicit non-darwin case.

* fix(macos): start the TCC watcher from app bootstrap, not the window wiring

attachMainWindowServices is called directly by its own unit test, so wiring
initTccPromptNotice there made `vitest src/main/window/` spawn real `log stream`
children that outlived the run - two orphaned watchers were left behind by a
single test session. Only the IPC handler registration stays there; the spawn
moves to the real app bootstrap in index.ts, which tests never execute.

Verified: running the suite that leaked now leaves the watcher count unchanged.

* fix(macos): clarify repeated permission notice

* fix(macos): keep TCC notice lifecycle safe

* fix(macos): retain pending TCC notice delivery

* fix(macos): acknowledge TCC notice delivery

* fix(macos): release failed TCC notice claims

* fix(macos): retry transient TCC notice display

* fix(macos): contain TCC notice IPC failures

* fix(macos): harden TCC notice renderer lifecycle

* fix(macos): contain TCC notice dismissal failures

* test(macos): satisfy promise executor lint

* fix(macos): detect helper-attributed TCC prompts

* fix(macos): align TCC watcher lifecycle and helper identity

* perf(macos): defer TCC log reader until first paint

* fix(macos): recover deferred TCC watcher startup

* fix(macos): recover TCC watcher from deferred quit

* fix(macos): localize recurring file access notice

* fix(macos): preserve TCC watcher and localized guidance

* fix(macos): avoid duplicate TCC watcher recovery

* fix(macos): wait for locale before TCC notice

* perf(macos): isolate TCC notice subscriptions

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-28 15:34:52 -07:00
Jinjing 8b57e6e180 fix(ssh): resync after watcher terminal retry (#10691)
* fix(ssh): resync after watcher terminal retry

* fix(ssh): resync after watcher terminal retry

- Coalesce repeated recovery resyncs within 5s to reduce SSH refreshes
  during link flaps
- Abort in-flight watcher installs when a replacement provider registers,
  preventing duplicate watchers from old and new transports
- Clear resync state when removing watcher snapshots or on provider change
  to prevent stale retry timers

* fix(ssh): resync after watcher terminal retry

Avoid logging spurious warnings when a remote watcher is already closed or
suspended. Move the console.warn call in handleRemoteWatcherTerminalError()
to after the early-return checks. Refactor createSender() in tests to
properly simulate the destroyed event for better coverage of retry-cancellation
behavior.
2026-07-28 15:30:07 -07:00
Brennan Benson 77ac0bd517 fix(codex): keep the stale-pane prompt when two accounts share a label (#11228)
The startup sweep asks main which panes are stale, and main answers by
account id. The renderer then threw that away: it resolved both ids to
labels and let the store's A -> B -> A collapse compare the strings. Two
accounts can share a label — doAddAccount has no duplicate-email check, so
one OpenAI login used in two ChatGPT workspaces gives both the same email,
and a failed roster read collapses every account to 'Codex account'. Either
way the notice was deleted for a pane that really is running under the
account the user switched away from.

The sweep then made it permanent: it marked every stale pane notified,
including the ones whose notice had just been dropped, and a notified pane
is suppressed for the rest of the session. Relaunching cleared the set but
the deletion recurred, so the prompt never came back and the pane kept
running on the other account's auth.json and quota, silently.

Carry the account ids into the notice and decide on them, falling back to
labels only for callers that have none; report which panes were left holding
a notice so a dropped one cannot claim suppression. The prompt also names
the ChatGPT workspace when that is what tells two same-email accounts apart,
which is what the two duplicated getCodexAccountLabel copies now share.
2026-07-28 15:29:57 -07:00
Brennan Benson 50f46889d9 fix(ai-vault): resume a bridged Codex session under the selected account's home (#11224)
* fix(ai-vault): resume a bridged Codex session under the selected account's home

The account session bridge hardlinks every rollout into each per-account
CODEX_HOME, and vault dedup keeps the lexicographically-smallest alias, so
Resume could pin an inline CODEX_HOME naming a peer account — running the
session under that account's auth.json and quota. At resume time the owning
host now substitutes the selected account's home when it holds the same
rollout at the same sessions-relative path, declining on any uncertainty so
resume degrades to today's behavior instead of failing.

* fix(ai-vault): repin dropped sessions without a cwd instead of resuming under the wrong account

The drag payload only carried sessionCwd when session.cwd was truthy, so a
null-cwd codex session dropped onto a pane silently fell back to the prebuilt
command - which pins the wrong account's CODEX_HOME, the exact defect this PR
eliminates on the other resume surfaces.

- Serializer always sends sessionCwd (null when the session has no cwd), so
  absence now only means an older-serializer payload.
- The repin rebuild accepts a null cwd (the builders already omit the cd
  prefix), matching the sidebar Resume/Copy paths which repin regardless of cwd.
- An unrepinnable payload (absent sessionCwd) now fails loudly with guidance
  instead of silently resuming under the wrong account's home.
2026-07-28 15:18:07 -07:00
Jinjing ca5a821600 Stop relaunching creation-time agents on workspace activation (#10647)
* fix(activation): stop relaunching the creation-time agent on workspace activation

Activating a workspace with zero renderable tabs launched the agent it was
created with, unprompted and in approval-bypass mode. Navigation is not consent
to start a process: the same fallback fired from post-delete focus handoff, the
jump palette, keyboard cycling, CLI/relay activation, and notification clicks.

The mechanism was superseded. #1814 added it when relaunching the created agent
*was* the resume feature; #4706 later added real provider-session resume six
lines above and left the fallback in place. What remained fired whenever a
workspace had no renderable tabs -- including when nothing had ever slept -- and
reported itself as `request_kind: 'resume'` while resuming nothing, discarding
any resumable session a plain tab close had already purged.

No caller depends on it. All seven intent-carrying callers pass an explicit
`startup` on the branch where they intend a launch, and every no-startup branch
either declined an agent, already has one running (host `didSpawnStartup`), or
is this same defect arriving over IPC.

Drops the now-orphaned imports, retargets the stale comment in
launch-work-item-direct that cited reopen-relaunch as the reason to persist
`createdWithAgent`, and moves the WSL default-args quoting assertion to
launch-agent-in-new-tab, whose launch path still resolves those args.

Regression tests are revert-sensitive -- all four fail if the fallback returns.

* test(activation): name the relaunch regression tests after what they reach

Three tests were named after scenarios they never invoked, which is the
failure mode that lets a coverage gap read as closed.

- The "host-originated" test's `notifyHostRuntime: false` is inert here: both
  gates resolve through `isWebRuntimeSessionActive`, false with no runtime
  environment seeded, so it was byte-identical to the plain reopen test. It no
  longer claims to cover the host `didSpawnStartup` leg, which lives in main and
  is unreachable from this layer.
- The "post-delete focus handoff" test never deleted anything and never touched
  `prepareActiveWorktreeFocusAfterDelete`. That caller is asserted directly in
  active-worktree-focus-after-delete.test.ts, which locks out any opts.
- The activate/close loop resets state instead of calling `closeTab`, so it does
  not exercise the sleeping-record purge its comment claimed.

Also folds the primary reopen test onto `seedEmptyActivatableWorktree` — the
fixture extracted for exactly that state, which its inline copy had drifted from
by hardcoding a POSIX repo path.

`preflight` is dropped from the launch-work-item-direct comment: the trust
preflight reads the create-time argument (worktree-remote.ts), not the persisted
meta. Removal safety and ownership do read the field and remain accurate.

Renames the ported quoting test to what it pins. Under vitest's node
environment `navigator.userAgent` carries no "Windows", so platform resolution
bails before the WSL branch and the WSL preference is inert — the real coverage
is single-quote escaping of user-configured agentDefaultArgs.

* transfer large terminal history seeds across bounded protocol messages

- Oversized cold-restore snapshots (>1MB) now upload via chunked startHistorySeedTransfer/appendHistorySeedTransfer protocol instead of inline, avoiding NDJSON line-size violations
- Checkpoints automatically trim oldest rows to fit within configured byte limit (200MB) before commit
- Protocol v30 required for chunked transfers; v29 daemons gracefully fall back to renderer-only recovery
- NDJSON encodeNdjson() validates line size and rejects oversized payloads; notifications silently swallow encoding errors

* fix(daemon): drop held output when teardown checkpoint fails to serializ

When a final snapshot checkpoint fails to serialize (returns retryable), the
pending output records must not be appended later—doing so would splice them
over the seq gap left by the failed snapshot, defeating gap detection. Drop
the records and retry the checkpoint instead.

* Bump daemon protocol version to 30

* Bump daemon protocol version to 30
2026-07-28 15:12:44 -07:00
Brennan Benson 930ff96152 fix(skills): stop the scan issue budget evicting a read failure (#11221)
The per-scan issue budget kept an issue only when it explained a candidate
or truncated the walk. Neither set intersects the attention set, so
'io-error' — the sole reason a plugin-cache scan can raise "Needs
attention" — was droppable. Once 16 ordinary issues filled the budget (16
'outside-root' vendor symlinks is an install shape the scan itself
documents as normal), a later read failure was evicted for a generic
'issue-limit' row that raises neither attention nor truncation, and the
dialog headline read "All installed Orca skills are up to date" over a
path that could be hiding a stale copy.

Attention issues now outrank the budget, capped at a small reserve so an
adversarial tree of unreadable folders cannot pin one issue per folder.
2026-07-28 14:41:27 -07:00
Brennan Benson a81f17c189 fix(skills): trust the updater's lock when a run installs content newer than the bundle (#11220)
skills update installs source-repo HEAD, which routinely runs ahead of the
revisions a shipped build bundles. The post-run re-scan hashed that content
'unrecognized' (the registry has never seen it) and the verdict counted it as
a failure — so a clean update reported "The update didn't finish / Updated 0
of N", and Retry repeated the false failure forever because the CLI now
no-ops (lock == source). The 'newer-known' escape hatch never fires: the
generator always points the manifest at the registry's newest snapshot, so no
observed content can hash to a revision newer than the bundle.

The verdict now computes the git tree sha of the observed bytes (a port of
the generator's hashing, verified byte-for-byte against git write-tree and
against every shipped skill's manifest gitTreeSha) and accepts an
unrecognized placement when that sha equals the lock's skillFolderHash: the
lock is the CLI's own record of what it installed, so disk matching lock
means the command did its job — the bundled registry simply has not seen
that revision yet.

Half-written bundles (sha mismatch), unreadable copies, removed skills,
degraded aliases, and outdated copies at the lock hash all still fail.
2026-07-28 14:37:31 -07:00
2b88931b93 Bug floating workspace shortcuts route to main w (#10433)
* fix(floating-workspace): route panel shortcuts to the floating panel, not the main window

Floating-workspace close/index keyboard shortcuts leaked to the main
window behind the panel. Route them through the floating panel across all
four keydown layers via an atomic focus signal, panel-owned indexed
switching with a tri-state outcome, an event-target-aware close guard, and
a floating-scoped guest IPC bridge.

Changes A-E and findings F2/F3/F4/F6/F7/F8/F9/F11/F-adv/F-dl/F-feas.

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: feelgom <littlestork4@gmail.com>
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>

* fix(review): clear stale floating-panel reclaim intent on panel close

The module-singleton reclaim intent (F3) is armed at an emptying-close but only
consumed by the visibleFloatingItemCount->0 effect. If a concurrent tab-create
keeps the panel from reaching 0, the intent stays armed and could survive to a
later empty-panel mount and steal keyboard focus. The !open release effect now
clears it (defense-in-depth), matching the outside-pointerdown/window-blur paths.

Flagged by 4 review personas (correctness, adversarial, julik-races, maintainability).

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

* test(floating-workspace): cover L1 index-chord yield and deferred-close reclaim-arm timing

Two additive R2-review tests for the #10288 floating-workspace shortcut
routing change set:

- createMainWindow: assert L1 yields the initial indexed-switch chord
  (tab-index and worktree-index) to the floating panel without
  preventDefault or dispatch, and contains held-key auto-repeats in main
  (preventDefault, no dispatch). Closes the untested Change B (F4) path.

- FloatingTerminalPanel: assert an emptying, panel-owned close whose
  closeTerminalTab defers/cancels (onClosed never fires) leaves the
  reclaim intent unarmed, so no later empty-panel mount can reclaim focus
  for a close that never happened. The prior mock fired onClosed
  unconditionally, so this arm-timing (F3) branch was uncovered.

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

* fix(review): resolve round-1 findings F-1..F-6

- F-1: re-derive panel emptiness from live store at arm time; clear stale
  reclaim intent on repopulating create so an unrelated later close can't
  consume it and steal keyboard focus from the main workspace.
- F-2/F-5a: single-source the panel's non-creation shortcut claims via
  matchFloatingWorkspacePanelShortcut(); shared isTerminalPaneCloseChord()
  predicate for L2/L3; App.tsx gate + both FloatingTerminalPanel call sites
  now call the SSOT so index/rename/max-min ownership can't drift.
- F-4: L2 keydown gate is event-target-aware (matches L1 yield) so an
  L1-yielded chord is still consumed during a transient panel blur.
- F-5b: export clearReportedFloatingFocusCache() + reset it in test setup.
- F-5c: split floating-workspace-item-actions.ts into focus-reclaim +
  guest-bridge modules (AGENTS.md file-naming).
- F-6: trim verbose design-code comments to single-line WHY.

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

* fix(floating-workspace): remove finding reference labels

These internal review labels (F1–F7) and change identifiers were used during development and are no longer needed in the code.

* fix(floating-workspace): preserve reclaim for deferred dirty closes

Dirty editor closes defer to the save dialog and complete asynchronously. The
reclaim-arm check must survive the queue and execute when the file leaves—
otherwise the next Cmd/Ctrl+T misses the floating panel entirely. Also resolve
browser guest page ids to their owning workspace for correct routing.

* perf(floating-workspace): single-pass shortcut match and stable listeners

Three hot-path cleanups with no routing behavior change:

- Match each keydown once. App.tsx's yield gate now calls one
  matchFloatingWorkspacePanelChord instead of scanning the creation table
  and the chrome table separately, and the panel splits dispatch into
  resolveFloatingPanelShortcut + applyFloatingPanelShortcut so the surface
  keydown preflight shares its resolution instead of re-matching.
- Pin the window-capture and guest-bridge listeners to [open] by reading
  the live closures (tab order, activate, close helpers, dispatch) through
  a ref, so a tab switch or reorder no longer re-subscribes them.
- Cache the per-tab TerminalPane ref callback so a parent render stops
  detaching and re-attaching every pane handle.

Creation chords stay target-gated and chrome chords stay ungated, matching
the two matchers the combined one composes.

Pre-commit hook bypassed: config/oxlint-react-doctor.json fails to parse
against this worktree's stale node_modules (oxlint 1.71.0 / react-doctor
0.2.10 vs the pinned ^1.75.0 / 0.9.1) for any file. oxlint, oxfmt --check,
tsc, the max-lines ratchet, and the targeted vitest runs were run manually.

* fix(floating-workspace): keep TerminalPane ref callback identity stable

The per-tab ref callback cache deleted its own entry on detach. After a
same-id remount (key is tab.id + generation) React detaches the old element
*after* the new render already read the cache, so the delete dropped the
entry that render had just written — every later render minted a fresh
identity and forced React to detach/re-attach the pane, the churn the cache
existed to prevent.

Move the cache into terminal-pane-handle-registry.ts: detach clears only the
handle, attach re-arms the cache entry, and dead tab ids are pruned from an
effect keyed on the live tab list. Unit-tests cover attach/detach identity
stability — FloatingTerminalPanel.test.tsx's React mock discards effect deps
and ref identity, so component tests can't catch this class of bug. Also
softened the combined-matcher comment: App.tsx's old `||` already
short-circuited, so that call site buys drift-safety, not fewer scans.

Gates: tsc (web), oxlint, oxfmt --check, max-lines ratchet, 332 focused
vitest tests. Pre-commit hook bypassed: config/oxlint-react-doctor.json
fails to parse against this worktree's stale node_modules (oxlint 1.71.0 +
react-doctor 0.2.10 vs the pinned ^1.75.0 / 0.9.1) on untouched files too.

* fix(floating-workspace): pure registry init for react-doctor

Replace null-guarded ref mutation during render with useState lazy init so
CI check:react-doctor:changed stops failing on FloatingTerminalPanel.

* fix(floating-workspace): drop unused registry type import

Satisfies oxlint no-unused-vars after pure useState registry init.
Local pre-commit react-doctor config fails on stale node_modules; CI has current plugins.

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: feelgom <littlestork4@gmail.com>
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-07-28 14:24:15 -07:00
Wooseong KimandBrennan Benson 55947a3557 fix(mobile): exclude proxy fake-ip addresses from pairing QR (#10498)
* fix(mobile): exclude proxy fake-ip addresses from pairing QR

Clash/mihomo TUN interfaces in 198.18.0.0/15 were enumerated as pairing
candidates and could become the default QR endpoint. Phones then retry an
unroutable address forever. Drop those addresses from the pickable list (#10404).

* refactor(mobile): keep fake-ip filtering local

* test(mobile): cover fake-ip range boundaries

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-28 14:14:13 -07:00
Brennan Benson 6d4e335001 feat(worktrees): support project-level worktree.sharedDirectories in orca.yaml (#10459)
* feat(worktrees): support project-level worktree.sharedDirectories in orca.yaml

Follow-up to #7549: `.worktreeinclude` copies gitignored paths into each new
worktree, which is right for `.env`/`.vscode/` but wrong for large rebuildable
directories. Copying `node_modules` per worktree is slow and duplicates disk,
and each worktree's install then diverges.

Adds `worktree.sharedDirectories` to `orca.yaml` — a versioned, in-repo list of
gitignored directories that are symlinked (shared) into every new local
worktree, so one install serves them all. Adds to, never replaces, the per-user
Worktree Shared Paths setting.

`createWorktreeSharedPaths` uses a new 'share' materialization mode that always
symlinks. The existing 'link' mode APFS clone-copies on macOS, which would give
each worktree an independent node_modules and defeat the point; 'link' and
'copy' behavior are unchanged.

Entries must exist as gitignored directories in the primary checkout; absolute
paths, `..` traversal, and `.git` are rejected. Resolution never throws, so a
malformed orca.yaml cannot block worktree creation. Remote (SSH) creation skips
this, as it does symlink paths and `.worktreeinclude`.

Closes #10451

* fix(worktrees): keep worktrees deletable after sharing a directory

A directory-only ignore rule (`node_modules/`, the common spelling) matches
the primary checkout's real directory, so the shared directory resolves and
gets symlinked — but it never matches the worktree's symlink, so Git reports
that link as untracked. Deletion only tolerated the per-user shared paths, so
every worktree in such a repo became permanently dirty: the clean preflight
threw "uncommitted or untracked changes" and `git worktree remove` refused
without --force.

Feed the configured `orca.yaml` shared directories into the same
tolerate-and-unlink machinery the per-user shared paths already use, at both
deletion call sites. The names are read unfiltered, since the create-time
resolver drops exactly the entry deletion needs most.

* test(worktrees): register createWorktreeSharedPaths in the runtime symlink mock

orca-runtime.ts imports createWorktreeSharedPaths, but the vi.mock factory for
../ipc/worktree-symlinks never listed it. Vitest resolves omitted exports
lazily, so this only stays green because no runtime test configures a repo with
worktree.sharedDirectories — the first one that does would fail on a mock
resolution error rather than on its own assertion.

* fix(source-control): don't count shared symlinks as uncommitted changes

A directory-only ignore rule (`node_modules/`) matches the primary checkout's
real directory but never the worktree's symlink, so Git reports the shared link
as untracked for the life of the worktree. That made every affected worktree
read as dirty: a phantom row in the diff view, and Create PR blocked with
`blockedReason: 'dirty'` telling the user to commit an entry they cannot
commit, because it is a symlink Orca created.

Status and the review-creation preflight now drop untracked entries that are
both declared shared (per-user shared paths or orca.yaml sharedDirectories) and
actually symlinks on disk. Both conditions are required, so a regular file at a
declared name, or a symlink nobody declared, still counts as user work. The
decision fails closed: anything not positively identified stays dirty.

The preflight moves to `--porcelain -z` so paths with spaces or non-ASCII bytes
are compared raw rather than C-quoted, with a parser that consumes the origin
field a rename emits instead of reading it as its own record.

Symlink detection moves to a leaf module: importing it from ipc/worktree-symlinks
would pull APFS cloning, and its child_process dependency, into the status graph.

SSH is unaffected and left alone — remote worktree creation skips the symlink
and shared-directory passes, so a remote worktree never has one.

* fix(source-control): wire shared links into local status

* fix(worktrees): resolve the status repo once and reject uncollapsed shared paths

`git:status` resolved the registered worktree's repo twice per call — once
inside `getLocalGitOptionsForRegisteredWorktree` and again for the shared-link
lookup — walking every repo's worktree meta on a polling path.

`apps/./web` also survived `sharedDirectories` normalization: `resolve()`
collapses it when the symlink is created but Git reports the collapsed path, so
every later comparison misses and the link reads as permanent untracked work.

Also stop resolving shared links for SSH repos in review creation: `repo.path`
names a path on the remote host.

Adds the missing wiring coverage for review creation and runtime status, plus
the untracked-only conjunct in both filters — all four were mutation-verified
to leave the suite green before these tests.

* test(worktrees): pin the resolver-to-status seam for shared directories

The resolver's output and the status filter were only tested apart — status
used a hardcoded `['node_modules']`. Feed the resolved directories back through
`getWorktreeSharedLinkPaths` into a real `getStatus` so a resolver that ever
returned a differently-spelled path can no longer leave the link showing as a
phantom untracked row.

* fix(worktrees): try a directory junction before a symlink on Windows

A plain `fs.symlink` needs Developer Mode or admin on Windows, so an ordinary
Windows user got EPERM, the per-path catch logged and continued, and the
worktree came up with no shared directory and no signal. A directory junction
needs no privilege, and the rest of the codebase already uses one for win32
directory links.

The symlink stays as a fallback rather than being replaced: a junction cannot
target a UNC path, and a WSL project's repo lives behind one, so replacing it
outright would trade the local-volume bug for a WSL regression.

Safe for the removal path either way — Windows reports a junction as both a
symlink and a directory, so the `isSymbolicLink()` unlink that runs before
`git worktree remove` still fires and still refuses to follow it.

* fix(worktrees): keep NUL bytes and tolerated links out of the removal error

The removal preflight switches to `git status --porcelain -z` whenever it has
shared links to tolerate, then attached that raw stdout to the error. `.trim()`
does not strip interior NULs, so the message reached the user as
`?? node_modules<NUL>?? precious.txt<NUL>` — raw control bytes, and it named the
shared link, the one entry that is not the user's work and cannot be committed
away.

Parse the NUL-delimited output once and use it for both the clean verdict and
the error text, so the two can never disagree about what blocks removal. The
`-z` switch stays: it is what keeps paths with spaces or non-ASCII names
comparable against the configured entry.

* chore(worktrees): drop stray reformatting and note why the SSH guard exists

Committing the merge staged 792 files, so lint-staged ran the formatter across
all of them and rewrapped three renderer files that were already unformatted on
main. Nothing was lost — they were byte-identical to main ignoring whitespace —
but they showed up in the pull request as unrelated changed files. Restored to
main's exact bytes.

Committed with --no-verify on purpose: the pre-commit formatter is what
introduced the rewrapping, so letting it run again would simply reapply it.
Every check it would have run was run by hand instead — lint, typecheck, and the
IPC and source-control suites all pass, and the three restored files are
expected to fail a format check because that is main's current state.

Also records why the connection guard on the shared-link lookup is not dead
code: the remote dirty check ignores those paths, so the guard's only effect is
avoiding a stray local read and the bad cache entry it would leave behind.

* refactor(source-control): drop a scan-everything guard and freeze the cached list

The dirty check built a filtered array only to read its length, so it always
scanned every status record; asking whether any record is untracked stops at the
first one and reads the same either way.

The cached shared-directory list was also handhanded out by reference, so a
caller that mutated it would corrupt every read for the rest of the cache
window. Marking the return readonly prevents that at compile time; copying on
return would work too but would allocate on the status-polling path, and there
is exactly one caller, which only spreads it.
2026-07-28 14:04:41 -07:00
70c81c4b32 fix: pr-bug-scan validated finding from #6471 (#6512)
* fix: address pr-bug-scan validated finding from #6471

stripTags now removes real markup (closing/self-closing/attributed/custom tags) outside the allow-list; only bare identifier-glued generics like Array<string> are kept. Blocks svg/center/custom leak a

* fix: address pr-bug-scan validated finding from #6471

stripTags now removes real markup (closing/self-closing/attributed/custom tags) outside the allow-list; only bare identifier-glued generics like Array<string> are kept. Blocks svg/center/custom leak a

* fix(mobile): harden markdown preview tag stripping

* fix(mobile): preserve angle-bracket prose while stripping tags

---------

Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-28 13:45:19 -07:00
Brennan Benson b41e813cb5 fix(native-chat): surface draft launch context in desktop and mobile chat composers (#9802)
* fix(native-chat): surface draft launch context in chat composers

Creating a workspace from a GitHub issue delivers the issue link only into
the agent TUI's input buffer (argv prefill or startup paste), so the chat
view showed no trace of it on desktop or mobile.

Desktop: draft launches now seed an in-memory launch draft keyed by tab id
(direct work-item launches, background GitHub work-item creates, quick-create
composer, and new-tab draft deliveries). The chat composer adopts the seed
once as its editable draft, declines permanently if the composer already has
text, and drops an untouched copy when any user turn lands (the one-line TUI
input means the prefill was submitted or deliberately cleared) or on its own
send, whose existing input pre-clear retires the TUI copy.

Mobile: the host publishes the draft as an optional launchDraft field on the
mobile terminal tab snapshot (additive, no protocol bump) and the mobile
composer adopts it with the same once-only/decline/resolve semantics. Mobile
chat sends now also pre-clear the TUI input line (Ctrl+U, desktop parity) so
a pending prefill cannot concatenate with the sent message.

Completion seeding resolves the launch tab from the synced store tabs when
the backend spawned the terminal and activation reports no primaryTabId.

Split the Windows shell-quoting tests into their own file to stay within the
max-lines budget.

* revert(mobile): drop incidental pnpm-lock churn from the launch-draft branch

The libc binding fields and the @typescript-eslint peer re-resolution came from
a local install, not from this change; mobile/package.json is untouched.

* fix(native-chat): resolve launch drafts without trusting cross-host clocks

The rule required a user turn stamped at or after the seed. Grok omits row
timestamps, so a Grok launch draft never resolved; and the seed time is a
renderer clock while the stamp comes from the executing host's JSONL, so a
remote workspace whose clock trailed never resolved either. Both left the
composer adopting an already-submitted prefill, which re-sends it as a
duplicate turn.

Resolve on any user turn that is not PROVABLY older than the seed (a launch
draft's session starts with zero user turns), with the existing cross-host
skew slack, plus a timestamp-free backstop for wider skew: a new tail user
turn since the draft was first observed. "Load earlier" prepends, so it
cannot move the tail and cannot over-resolve.

Split out of native-chat-pending.ts to stay under the max-lines ratchet.

* fix(worktrees): seed the launch draft on the agent's own tab, never on tabs[0]

Two defects in the completion seed:

- The tab was resolved by array position. buildStartupOpt returns undefined on
  the backend-spawn path, so applyDefaultTerminalTabs stamps launchAgent on no
  tab and the launchAgent guard was dead there. A repo with default terminal
  tabs ("dev server", "logs", ...) got the draft on a tab that runs no agent,
  and then published it to mobile as THAT tab's launchDraft. Correlate on the
  backend startup tab, then on a launchAgent-stamped tab, then on primaryTabId
  (which is the agent tab whenever the renderer owns startup); never tabs[0].

- Runtime-owned worktrees mirror their session tabs async, so tabsByWorktree
  was empty at seed time and the seed was silently dropped for that whole host
  class. Defer to the first mirrored tab via the existing delayed-delivery
  queue, which now holds every pending delivery for a worktree instead of one
  (setup/issue commands and the seed both wait on the same first tab).

* fix(store): evict nativeChatLaunchDraftByTabId on every teardown path

The new map was absent from all four paths its sibling
nativeChatLaunchPromptByTabId participates in: tab close, the orphan terminal
sweep, the bulk worktree purge, and the removeWorktree teardown. A stranded
entry is worse than a plain leak here because sync-runtime-graph keeps
publishing it to mobile as that tab's launchDraft.

* fix(native-chat): only seed single-line unsubmitted launch drafts

The unsubmitted-delivery branch seeded on every draft delivery, which also
caught the agent-session-fork path whose prompt is multi-line scraped context.
The chat send pre-clears the TUI with Ctrl+U (kill-to-start-of-LINE), so a
multi-line prefill cannot be fully cleared and its earlier lines would glue
onto the next message. The GitHub work-item draft this feature targets is a
bare issue URL, so narrowing costs it nothing.

Also assert the composer retires the seed after a send — deleting that call
previously failed no test.

* fix(mobile): stop the chat pre-clear from wiping a just-pasted image

The text write set clearInputFirst unconditionally. On the image path that
Ctrl+U lands AFTER pasteMobileNativeChatImagePaths already pasted the image,
so the agent receives the text alone while acceptSend still renders the
thumbnail on the sent bubble — silent image loss.

Desktop's image path clears exactly once, before the paste, and never again;
mobile now matches: pre-clear only when nothing was deliberately pasted first.
The image paste already leads with its own Ctrl+U, so a launch-draft prefill
parked on the input line still cannot glue onto the message.

Pinned at both levels: the controller test drives the real send hook and
asserts clearInputFirst per branch, and the send module asserts the wire text
carries no leading \x15. The image-attachments test injects its own baseSend,
so it structurally could not observe this.

* fix(mobile): hold the launch-draft prefill until the transcript settles

session.tabs delivers launchDraft before the transcript read resolves, so the
seed effect could run against an empty in-flight message list and miss the
user-turn decline. Launching from an issue, submitting the prefill in the TUI,
and never opening desktop chat (nothing else clears the host seed) then
prefilled the mobile composer with the already-sent issue link — a send tapped
before it retracted duplicated it to the agent.

Thread the session's loading state through and skip the seed while the read is
in flight. idle/waiting-session still seed: no session means no user turns.

* fix(runtime): publish a launch draft to mobile only for the tab's own agent

The publish had no agent check while the desktop consumer declines on
mismatch. The seed is keyed by tab id, which survives a pane's agent switch, so
mobile could adopt a draft desktop refuses — seed for claude, never open
desktop chat, switch the pane to Codex, and mobile prefills the Codex chat with
the Claude-era issue link. Align publish with the consumer.

* fix(native-chat): take the launch-draft baseline only after the transcript loads

The timestamp-free backstop snapshotted the transcript's user turns on first
observation of the draft, which can happen while the read is still in flight and
`messages` is []. A pane bound to a session that already had user turns then
backfilled above that zero baseline with a different tail id, so clause 2
resolved and silently dropped the seed — the launch context never appeared, and
the feature no-oped for exactly the panes it was meant to serve. Clause 1 was
already correct there (that history is provably older than the seed).

Gate baseline capture and resolution on the transcript read settling, the same
shape mobile's drafts hook uses. Clause 1 is unchanged; while loading the merged
list is empty anyway, and a pane with live appends is never reported 'loading'.

Also restore clause 1's short-circuit: it scans with .some() again and only
allocates the user-turn list when falling through to the backstop.

NativeChatView sat at exactly the 400-line cap, so the composer's two
launch-draft props are now spread from the hook result they already mirror.

* fix(native-chat): reject multi-line launch drafts inside the seed helper

The single-line guard lived in deliverLaunchPromptToAgentTab, so the two
other seeding entry points (worktree create, direct work-item launch)
bypassed it — and every Linear launch is multi-line by construction
("Linked Linear issue: STA-…" + url). The chat send pre-clears the TUI
with Ctrl+U, which kills to start of LINE, so those earlier lines stay
parked to glue onto the next message.

* fix(worktrees): keep the deferred agent seed off ambiguous mirrored tabs

The runtime-owned deferred path fell back to tabs[0], which the module's
own docstring forbids: with repo default tabs ("dev server", "logs") the
seed lands on a tab running no agent, where mobile withholds it and
desktop's agent check ignores it — the feature is silently dead for that
create and the entry leaks until tab close.

The queue entry is consumed before delivery, so there is no retry to fall
back on; accept the first mirrored tab only when it is the worktree's
only one and so unambiguously the agent's.

* fix(mobile): treat a launch-draft-only session-tab frame as a change

mobileSessionTabEqual's terminal branch never compared launchDraft, and
the route keeps `prev` when tabs compare equal — so a publish whose only
delta is the draft appearing or retracting was discarded and never
reached the composer. Live QA passed only because agentStatus happened to
change in the same frame.

MobileSessionTab's terminal variant did not declare the field either
(the controller read it through the structurally wider
MobileNativeChatTab), which is why TypeScript never flagged it.

* fix(mobile): judge a launch prefill only from its own settled transcript

Two ways the drafts hook was reading a transcript that was not the active
chat's:

- transcriptLoading came from `status`, a plain useState written by a
  passive effect declared before the drafts hook. On the commit where the
  tab identity changes it still holds the previous tab's value, so the
  guard was off on exactly the render that seeds: first entry saw
  status 'idle' with an empty list and seeded an already-submitted link,
  and a tab switch declined the new tab's prefill from the old tab's
  turns. The session hook now tracks the identity its messages describe
  and reports transcriptLoading until they agree; the retire effect gates
  on it too.
- Leaving chat view nulled launchDraft while draftKey stayed the same,
  which the hook could not tell from a host retraction — it declined the
  prefill permanently, so peeking at the terminal dropped the context.
  The controller now passes the raw field plus an explicit chatActive
  flag, and both effects hold their state when the tab is not on chat.

The controller wiring was previously unasserted: replacing both props
with constants left all 795 mobile session tests green.

* fix(native-chat): keep the launch-draft baseline across a transcript reload

baselineKey went null whenever the transcript was loading, and the null
branch DISCARDED an already-valid baseline taken from a settled read. It
was then re-taken from the fuller list, swallowing the very user turn
that resolves the draft — so a stale prefill gets re-adopted as a
duplicate turn. Key the baseline on draft identity alone and gate only
the capture.

session.status is also not a truthful read-in-flight signal: a live
'working' hook outranks 'loading', so the guard could be off over an
in-flight empty list. Expose the read phase itself and gate on that.

* test: cover the launch-draft reducers and the sync-key skip gate

Every consumer test injects the three launch-draft reducers as bare
vi.fn()s, so reducing markNativeChatLaunchDraftAdopted to a no-op left
2609 tests green — while in the app the composer would resurrect the
prefill after every manual clear.

canSkipRuntimeMobileSessionSyncKeyBuild had no launch-draft case either:
when it skips, the sync key is never even built, so the existing
getRuntimeMobileSessionSyncKey case cannot catch its removal.

* fix(native-chat): hold the launch-draft baseline in state, not a render-mutated ref

react-compiler rejects reading or writing a ref during render. Adjust the held
baseline with the sanctioned render-time setState instead, keeping the local
copy so the render that first sees a settled transcript resolves against it.

* fix(mobile): carry the transcript identity in the session read state

react-doctor flags the separate loadedIdentity state as an extra render for a
derivable value. Hold status alongside the identity it describes in one state
written by the subscription effect, so transcriptLoading derives from it.

* test(native-chat): assert the readPhase contract without the hook-status race

The test asserted status === 'working', which depends on liveStatusOverride
winning over ambient transcript state — green locally, red under CI load. The
contract is that readPhase stays 'loading' once live content unmasks status,
so assert exactly that; it still fails if readPhase derives from status.

* fix(mobile): derive pre-read chat status instead of writing it from the effect

react-doctor's no-derived-state-effect flags idle/waiting-session/loading being
set in the subscription effect: all three are pure functions of the props. Derive
them during render and keep state only for the genuinely async outcome, tagged
with the identity it describes.

The tag now gates `messages` too, so a just-switched tab never sees the previous
tab's transcript at all rather than seeing it behind a loading flag.

* fix(mobile): drop a settled chat read once its subscription is torn down

The settled outcome was only ever replaced by a newly arriving frame, so any
effect re-run that landed back on an already-settled identity resurfaced it over
a list the same effect had just cleared: 'ready' with no messages and
transcriptLoading false. Toggling out of chat view and back hit this every time
(the agent goes null, then returns), flashing the "start a chat" empty state over
a real conversation and opening the launch-draft seed's decline check on an empty
transcript. A reconnect did the same via the client dep.

Identity and client are the effect's only inputs, so tagging the read with both
and dropping it during render when either moves covers every re-run.
2026-07-28 13:15:31 -07:00
Jinjing 0388319a32 Improve translations for resource manager and related UI elements (#11205)
* Improve translations for resource manager and related UI elements

Standardize terminology ("daemon" vs "service"), complete missing translations, and refine wording across Spanish, Japanese, Korean, and Chinese locales for consistency and clarity.

* Improve translations for resource manager and related UI elements

* fix(test): update zh name-mode label expectation after translation fix

The resource-manager translation pass correctly changed the Chinese
"Name" label from 姓名 to 名称; update the localized options unit test
to match so CI passes.
2026-07-28 12:47:01 -07:00
JinjingandOrcaWin a40183389b feat: bound direct SSH reconnect fan-out and recovery (#11003)
* docs: design for direct SSH reconnect fan-out

Capture the implementation-ready plan for host-qualified, epoch-fenced
SSH reconnect recovery after two rounds of multi-model LLM counsel review.

* docs: reconcile SSH reconnect fan-out design

* docs: close reconnect design consistency gaps

* feat: implement bounded direct SSH reconnect recovery

* fix: bound direct SSH retry settlement

* fix: harden direct SSH reconnect authority

* fix: preserve split SSH retry ownership

* fix: preserve SSH split continuation authority

* docs: record final SSH reconnect validation

* fix: preserve SSH authority through retained and detached state

* fix: retain SSH authority across delayed split mounts

* fix: close SSH authority recovery gaps

* fix: fence stale SSH transport replacement

* fix: serialize SSH target teardown

* fix: settle SSH teardown failures before reconnect

* fix: retire failed SSH reset sessions

* test: reconcile current main E2E contracts

* fix: close direct SSH reconnect review gaps

* fix: fence stale SSH reconnect side effects

* fix: close final SSH reconnect lifecycle gaps

* test: stabilize current-main reliability gates

* test: prove plugin navigation containment

* test: make plugin navigation oracle authoritative

* test: make plugin navigation oracle deterministic

* ci: allow sharded e2e suite to finish

* test: wait for runtime pane publication

* test: classify pane readiness by error code

* test: select close persistence terminal by tab identity

* docs: mark reconnect implementation validated

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 12:33:17 -07:00
Neil 6fc05df985 feat(dictation): add stop button and shortcut hint to Listening indicator (#11152) 2026-07-28 12:21:28 -07:00
Brennan Benson 9e49708c07 fix(codex): re-confirm spurious shell readings before skipping the restart card (#11076)
An account switch decides pane eligibility from a single cached
inspectProcess read. When that read reports the pane's shell for a live
Codex session, the pane silently loses its restart card - no error, no
retry. Re-check shell readings on Orca-launched Codex panes with the
existing fresh-scan confirmForegroundProcess before trusting them; only
an affirmative codex answer flips the decision, so a genuine exit to the
shell stays uncarded and unsupported providers keep today's behavior.
2026-07-28 12:14:15 -07:00
Jinjing bd8640db06 chore: remove orchestration structured-output draft design doc from repo root 2026-07-28 11:30:10 -07:00
2d23217166 feat: add Trae CLI as a supported TUI agent (#10763)
* feat: [AI-GEN] add Trae CLI as a supported TUI agent

Closes #10579.

Wire trae-cli into the desktop and mobile agent catalogs following the
same integration pattern as other CLI agents (e.g. Ante, Devin):

- src/shared/types.ts, tui-agent-config.ts: register 'trae' with
  detectCmdAliases (traecli/trae-agent) and argv prompt injection,
  matching trae-cli's `trae-cli [prompt]` contract. The CLI's own
  third documented alias `ta` is intentionally excluded — too generic
  a 2-letter name to use as a PATH-existence detection signal without
  false-positiving on unrelated tools.
- src/shared/trae-headless-command.ts: recognize `--print`/`-p` and
  `--output-format json|stream-json` as one-shot headless invocations
  (same shape as claude-headless-command.ts) so they aren't mistaken
  for a live interactive session.
- agent-kind.ts, telemetry-events.ts, agent-status-types.ts,
  agent-type-label.ts, tui-agent-display-names.ts,
  tui-agent-permissions.ts (YOLO via trae-cli's own --yolo flag),
  tui-agent-selection.ts: standard per-agent registrations.
- agent-catalog.tsx, agent-favicon-assets.ts,
  mobile/src/tasks/mobile-tui-agents.ts,
  mobile/src/components/mobile-agent-icon-assets.ts: catalog entries
  and bundled favicon (fetched from docs.trae.cn, required by mobile's
  offline-icon invariant test).
- i18n: add the "Trae" label to all five locale catalogs (en/es/ja/ko/zh).
- Tests: agent-process-recognition, agent-status, tui-agent-startup.

Verified with `pnpm typecheck` (desktop + mobile), the relevant vitest
suites (869 tests across 12 files, all green), oxlint (clean), and a
real end-to-end launch of the actual trae-cli binary through Orca's
pty.spawn IPC path (confirmed via the OS process table).

* fix: [AI-GEN] point Trae catalog entry at the real CLI quick-start doc

docs.trae.cn/cli (what the installed CLI's own --help text prints as
its "User manual" link) soft-404s — the docs site restructured and the
working page is docs.trae.cn/cli_get-started-with-trae-cli (confirmed
by HTTP fetch: real page title "TRAE CLI 快速开始" vs the old path's
"404 - 页面不存在"). Addresses CodeRabbit's homepageUrl review comment.

* fix: [AI-GEN] detect Trae on traecli, not the ambiguous trae-cli name

Per @AmethystLiang's review: the open-source bytedance/trae-agent
project (MIT, ~12k stars) registers its own console script as
`trae-cli` (pyproject.toml: `trae-cli = "trae_agent.cli:main"`), an
entirely unrelated CLI with a different contract (`trae-cli run
"task"`, `-p` short for `--provider`). Detecting on bare `trae-cli`
would false-positive on that project's installs and break launch for
anyone who has it instead of the actual TRAE CN CLI.

- tui-agent-config.ts: detectCmd/launchCmd/expectedProcess -> `traecli`
  (TRAE CN's own installer symlinks this alias too, but the other
  project does not ship it). Dropped the `trae-agent` alias entirely —
  it's the colliding project's literal repo name, the highest
  false-positive string available.
- agent-catalog.tsx: cmd -> `traecli` to match; faviconDomain ->
  `www.trae.cn` (bare `trae.cn` 404s on Google's favicon service;
  `www.trae.cn` is the product-root domain that actually resolves).
- mobile-tui-agents.ts: faviconDomain -> `www.trae.cn` to match.
- Tests updated: agent-process-recognition now asserts `trae-cli` and
  `trae-agent` are NOT recognized as Trae (regression guard against
  reintroducing the collision); tui-agent-startup updated for the new
  launch command.

promptInjectionMode stays `argv` and the headless-command file stays
as-is — both verified against the real TRAE CN CLI's actual --help
output (pasted in the PR review thread), not assumptions.

* refactor: [AI-GEN] share one print-mode headless matcher across agents

trae-headless-command.ts was a rename-only fork of claude-headless-command.ts,
and ante-headless-command.ts carried a third copy of optionName. Collapse both
print-mode files into print-mode-headless-command.ts, dispatch from a
Partial<Record<TuiAgent, ...>> table instead of an if-chain, and compress the
Trae comments to the repo's one-line style.

* fix: [AI-GEN] terminate Trae flag parsing before the positional prompt

`traecli` is a Cobra CLI with subcommands, so an argv prompt starting with
`help`, `config`, `-…` was dispatched as a subcommand or flag instead of being
run as the task. Add `argvPromptSeparator: '--'` (same reason Grok has it), and
stop the shared print-mode headless matcher at `--` so a prompt that reads like
`--print` no longer drops the pane out of agent recognition.

* docs: [AI-GEN] name both Trae CLIs explicitly in the detect-name comment

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

* docs: [AI-GEN] drop the vendor tag from the Trae union comment

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

* fix: [AI-GEN] guard the nullable startup plan in the Trae separator test

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

---------

Co-authored-by: 陈泽榜 <chenzebang@jianzhikeji.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-28 11:18:49 -07:00
Jinjing 1bd80931bd fix(diff): stop file-tree navigation remounting combined diffs; make tree resizable (#11088) 2026-07-28 10:53:59 -07:00
Jinjing 21dee21a6d test(cli): lock CLI-compatible timeout parse contract (#11206)
parsePositiveSafeIntegerNumericText mirrors the CLI's own Number()
coercion on purpose: text like `600000.000000000000001` is the budget
the CLI will actually wait on, so rejecting it here would leave the
relay and SSH kill timers shorter than the CLI's and cut the request
short. Document that and pin it with regression cases.
2026-07-28 10:22:33 -07:00
Neil 0404f27b3f chore(i18n): drop orphaned cadence catalog keys (#11154)
Removes the lowercase hourly/weekly/custom entries from the AutomationSchedulePicker catalog node in all five locales. They were superseded by keys derived from the rendered labels in #11068, and the extractor only adds keys, so they stayed behind unreferenced.

Follow-up to #11068.
2026-07-28 02:36:43 -07:00
Neil 16dcf865ed fix(i18n): translate automation cadence labels (#11068)
The cadence picker now resolves Hourly, Daily, Weekdays, Weekly, and Custom cron through the renderer i18n catalog, with corrected zh/ko/es translations.

Supersedes #10044 (thanks @innocarpe) and #10045 (thanks @fsdwen).

Fixes #10043
2026-07-28 02:22:35 -07:00
OrcaWinandOrcaWin dca0db38c4 fix(orchestration): repair version-skewed run schemas (#11150)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 02:21:27 -07:00
1fa9ffb5ea ci(pr): run E2E when a PR touches tests/e2e paths (advisory) (#11131)
* ci(pr): run E2E when a PR touches tests/e2e paths

Regression specs under tests/e2e never ran on PR CI — only schedule and
release called e2e.yml — so a red regression test could merge green.
Path-filter and workflow_call the E2E suite when E2E-relevant files change.

Use merge-base diffs so base-branch drift does not false-trigger E2E, fail
the detector when git diff cannot compute the PR range, and pin
least-privilege contents:read on both the detector and reusable E2E workflow.

Closes #10518

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>

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

* ci(pr): make the E2E path gate actually block, and match the real config path

Two fixes to the new path-filtered E2E job.

The gate did not gate. pr.yml's `verify` job is the required check, and it
enumerates its dependencies explicitly — `e2e` was in neither `needs` nor the
result list, so a failing shard left `verify` green. That reproduces the exact
hole this job exists to close: a red spec merges green, just with a red box
further down the page. Add `e2e` to both.

Because the job is path-filtered, `skipped` is the normal result on a PR that
touches no E2E files and has to keep passing. That allowance is checked after
the strict loop rather than inside it, so it can never leak to the six jobs
that are always required.

The `playwright.` pattern matched nothing. The config is
tests/playwright.config.ts — beside tests/e2e/, not inside it — so no tracked
file starts with `playwright.` and editing the runner config would silently
skip E2E. Anchor it at `tests/playwright.`.

Adds a contract test alongside the existing release-e2e one. Verified it fails
when either fix is reverted, and simulated the gate across
success/skipped/failure/cancelled plus the skip-must-not-mask-a-real-failure
case.

* test(ci): close two gaps in the E2E gate contract

CodeRabbit was right on both counts — verified by reverting each and watching
the contract stay green.

The path filter was unasserted, so `e2e` could lose its `if:` and run on every
PR — the cost the filter exists to avoid — without failing anything.

The strict-loop check hardcoded four of the six required jobs, so dropping
GIT_COMPATIBILITY or SHELL_CONTRACTS left them unenforced while the contract
passed. Derive the list from verify.needs instead, so a newly added required
job that misses the loop fails here rather than silently going unchecked.

* ci(pr): land the E2E path gate advisory instead of blocking

The E2E suite is currently failing every scheduled run on main — 22 of the last
22 — so making verify depend on it would block any PR touching tests/e2e/**,
including the PRs that fix the suite. This PR's own run reproduced that: 3 of 12
shards failed on specs unrelated to it (agent-session resume, Jira linking,
plugin containment, terminal artifacts).

So the job runs and reports on E2E-path PRs but is left out of verify.needs for
now. The detector, the tests/playwright. path fix, and the contract tests are
unaffected — those stand on their own and were the substance of the review.

Flipping to blocking is a three-line change once the suite is green; the exact
wiring, including why the skipped allowance must sit outside the strict loop, is
recorded on verify's Require-successful-checks step. The contract test pins the
advisory choice so it reads as deliberate rather than as the unwired-gate bug it
originally caught, and still fails if the path filter, the strict-loop coverage,
or the config path regress.

---------

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-28 02:16:24 -07:00
Jinwoo HongandOrcaWin 0d6f9195d8 fix(orchestration): reveal worker terminals reliably (#11142)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 01:59:49 -07:00
Neil de162c632b fix(memory): retune image and orca.yaml ceilings that rejected valid input (#10815) 2026-07-28 01:51:22 -07:00
OrcaWinandOrcaWin 380034edf9 fix(macos): avoid scene deadlock on app reactivation (#11055)
* fix(macos): avoid redundant focus on app activation

* test(macos): cover passive app activation

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 01:45:59 -07:00
NeilandOrca d548641f1d fix(sidebar): restore legible selected-workspace fill in dark mode (#11139)
#8321 mixed the selected card's wash into the opaque --worktree-sidebar
surface, lifting dark mode to 16% (#4b4b4b). Card text lost too much
contrast against it.

Return both modes to a translucent wash (light 8%, dark 10%) so the
brighter selection border added by #8321 carries the selected state
instead of the fill.

Co-authored-by: Orca <help@stably.ai>
2026-07-28 01:43:23 -07:00
Jinjing efcc015d69 docs: add WeChat group 6 QR with overflow guidance
Show group 5 and group 6 QR codes side by side so people can join group 6 if group 5 is full.
2026-07-28 01:36:06 -07:00
Neil 89f32a121b fix(lint): restore nested config discovery in changed-code gate (#11130) 2026-07-28 01:30:52 -07:00
OrcaWinandOrcaWin 77d4c64f7a Improve orchestration migration safety for live legacy workers (#11107)
* fix(orchestration): clarify legacy migration safety

* fix(cli): sanitize legacy formatted messages

* test(runtime): allow near-cap fuzz under shard load

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 00:36:25 -07:00
YoyoandNeil df9f55990b fix(browser): keep the address bar editable in a narrow toolbar (#11098)
* fix(browser): keep the address bar editable in a narrow toolbar

Every other browser toolbar control is shrink-0, so the address bar was
the only flexible item and absorbed the entire squeeze: below roughly
420px of pane width it collapsed to the leading globe icon with a
zero-width input. Clicking it only opened the suggestion dropdown, which
inherits `--radix-popover-trigger-width` and so rendered at icon width —
there was no way to type or edit a URL in that tab.

Focusing a squeezed bar now lifts the form out of the toolbar flow and
overlays the row edge to edge, giving a full-width editable field that
navigates on Enter (and a full-width suggestion list for free). A
measured slot stays in flow so the overlay cannot feed back into its own
width, and the slot keeps a min width so the globe remains a real hit
target instead of being overlapped by neighbouring buttons.

Fixes #11090

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

* fix(browser): use the documented floating shadow for the expanded bar

STYLEGUIDE.md defines exactly three elevation levels and forbids a
fourth; shadow-md was not one of them. The overlaid address bar is a
floating surface, so it takes the documented floating shadow already
used by the other floating surfaces in this pane.

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

* test(browser): make the narrow-toolbar regression deterministic

The spec passed only from a clean profile. Two preconditions it set once are
actively undone by the app:

- BrowserPane re-focuses a blank tab's address bar across several animation
  frames plus the blank-url did-finish-load handler, so a single blur() was
  reverted and the bar never reached its squeezed resting state.
- Startup paths re-open the right sidebar. At a fixed 700px window that leaves
  the pane ~70px, so the overlay had nowhere to go and the field measured 0px.

Settling these separately let whichever settled first drift back while the next
one ran. Re-assert them in one loop until they hold simultaneously, and size the
window from the chrome actually measured instead of assuming a fixed 700px.

Verified 8/8 green, and still fails at the overlay assertion when the fix is
disabled, so the regression coverage stays real.

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-07-28 00:22:22 -07:00