Commit Graph
5414 Commits
Author SHA1 Message Date
Brennan Benson e567d6f2c0 Merge branch 'main' of https://github.com/stablyai/orca into brennanb2025/tcc-local-build-bundle-id 2026-07-22 21:10:21 -07:00
Jinjing fc05769edf fix(ssh): allow blank user in VS Code authority (#10072) 2026-07-22 21:10:05 -07:00
NeilandOrca 8448557be9 perf(startup): overlap catalog/session disk reads with the worktree scan (#9841)
* perf(startup): overlap catalog/session disk reads with the worktree scan

App.tsx hydrated repos, project-groups, folder-workspaces, worktrees, then
session-get strictly serially, even though only worktrees depends on repos.
Once repos is loaded, run fetch-worktrees, session-get, and the (internally
ordered) local project-group/folder-workspace catalog chain concurrently so
the two disk reads hide behind the O(repos) worktree git scan (the startup
long pole). list-runtime-session-hosts now overlaps the repo scan too.

Ordering preserved: repos before worktrees/session; project-groups before
folder-workspaces; hydrate-session-stores still runs only after all settle.
fetchAllWorktrees({hydrationPurge:'defer'}) returns before its folderWorkspaces
read, so it needs no catalog ordering at startup.

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

* perf(startup): don't serialize the worktree scan behind host discovery

Address CodeRabbit review: awaiting runtimeHostsPromise before the Promise.all
made fetch-worktrees + the catalog chain wait on list-runtime-session-hosts, even
though only session-get needs the host ids. Chain session-get off the host promise
inside the Promise.all instead, so the worktree scan and catalog reads start
immediately and the host-list IPC only gates session-get.

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

* test(startup): assert concurrent hydration graph in source-order guard

The #18 startup change runs worktrees/session-get/catalog concurrently in a
single Promise.all, so the guard's old serial folders<worktrees assertion (and
its session-get slice terminator) no longer describe the code. Assert the real
invariants: UI hydrates before any local read, the catalog chain stays ordered,
worktrees+session start after repos, and all three are joined in one Promise.all.

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

* fix(startup): join concurrent hydration with allSettled so recovery can't race in-flight tasks

The concurrent worktrees/session/catalog join used fail-fast Promise.all, so a fast
rejection from one branch dropped into the catch/recovery path (which reconnects
terminals and flips readiness) while a sibling hydration task was still in flight and
mutating catalog/worktree state — a race the old serial flow could not hit. Use
Promise.allSettled and surface the first rejection only after all three settle, so
recovery still triggers but nothing is left writing to the store. Guard test updated.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 20:41:52 -07:00
NeilandOrca 414bdc9f86 perf(main): gate idle git-common polling + coarse-FS add/remove fix (re-open of #9882) (#10074)
* perf(main): gate idle git-common polling

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

* fix(main): enumerate git-common worktrees every tick to catch coarse-FS add/remove

The worktrees-dir readdir was gated on the dir's mtime:ctime:ino:size signature, so on
a coarse-mtime/FAT filesystem a same-granule add+remove (all four fields collide) went
undetected until the ~30s index backstop. A single readdir of a small dir is negligible
next to the per-entry structural stats that already run every tick, so always enumerate —
the listing is the authoritative add/remove signal. The expensive per-entry index read
stays gated on each entry's own dir signature; onFullScan now reflects the ungated
index-metadata backstop fan-out (the real periodic cost) rather than the readdir.

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

* fix(main): don't fabricate worktree deletions on a transient git-common readdir failure

Follow-up to always-enumerating the worktrees dir: the readdir catch-all treated ANY
error as an authoritative empty listing, so a transient failure (EIO/ESTALE/EMFILE,
network/SSH hiccup) emitted a false delete for every linked worktree (and a false create
next tick) — and enumerating every tick widened that exposure. Only ENOENT (dir truly
absent) now yields an empty listing; other errors retain the known entries so per-entry
stats still run and a real removal surfaces as that entry's own stat miss. Adds a
regression test (readdir → ENOTDIR) asserting no false delete.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 20:41:30 -07:00
NeilandOrca 63bd3ff189 perf(main): park worktree metadata pollers while the window is hidden (#9849)
* perf(main): park worktree pollers while hidden

The per-repo worktree metadata pollers ran 24/7 with no window-visibility gate
(~50 fs.stat/sec at 20 repos on macOS while the window is hidden). Park the poll
timers while the window is hidden and resume losslessly (immediate fresh-vs-retained
snapshot diff) on reveal, via an injected WorktreePollerWindowVisibility mirroring
the ssh-port-scanner pattern.

Only the poll timers are gated; the darwin native worktrees/ fsevents watch and its
teardown/re-arm lifecycle stay always-on (push-based, ~0 idle cost; #8732 race).

A window is parked only once it has actually been shown and is now hidden: a live
never-shown window (ORCA_E2E_HEADLESS keeps one) and null/destroyed windows stay
always-visible, so a windowless/headless host never permanently parks the poller.

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

* fix(main): keep start-to-start poller cadence after the interval→timeout change

Parking the pollers moved them from setInterval to a one-shot setTimeout chain, but
scheduling the next tick a full pollIntervalMs AFTER each scan completed turned the
cadence into gap-after-completion — every visible refresh landed ~one scan-duration
late per tick. Measure from tick start and schedule max(0, interval - elapsed) so the
cadence is start-to-start like the original interval, while keeping the one-shot chain
that park/resume needs.

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

* fix(main): clamp poller reschedule delay to [0, pollIntervalMs]

Date.now() is not monotonic: a backward wall-clock jump (NTP correction) during a scan
makes (now - startedAt) negative, so the start-to-start delay pollIntervalMs - elapsed
would exceed one interval by the adjustment — suppressing visible metadata refreshes for
minutes/hours, unlike the former setInterval. Cap the computed delay at pollIntervalMs
(upper) as well as 0 (lower) in all three pollers.

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

* refactor(main): drop redundant notifyTimer guards to stay under max-lines

The visibility-parking change pushed worktree-base-directory-watcher.ts to 303 code
lines (limit 300). clearTimeout tolerates null/undefined, so the two truthy guards
around it are redundant — remove them (coalescing null→undefined for the type). No
behavior change; back under the limit without an eslint-disable.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 20:38:35 -07:00
NeilandOrca 059a80b298 feat(review): add assignable "Send Review Notes to Agent" shortcut (#10027) (#10070)
Adds a keyboard command that opens the "Send notes to an agent" picker for
the active worktree's AI diff-review notes, enabling a fully keyboard-driven
review flow. Unbound by default; users assign it in Settings → Keyboard
Shortcuts.

- New `sourceControl.sendReviewNotes` command (scope global, unbound). Set
  `conflictGroup: 'editor'` so Settings warns on collisions with editor chords
  (e.g. Add Review Note), not just global ones.
- Dispatched from App.tsx's existing global capture handler so it respects the
  terminal-shortcut policy, the shortcut-recorder guard, and defaultPrevented.
- Store thunk `openDiffNotesSendMenuForActiveWorktree` reveals Source Control
  and requests the notes send menu open; no-op when there are no unsent notes.
- Menu opens via a nonce-based store request consumed on mount, TTL-bounded so
  a request the menu never consumed can't reopen it on a later remount.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 20:38:10 -07:00
NeilandOrca d152039e9d fix(terminal): never sweep descendants of an already-exited PTY root (hardening, #9191 investigation) (#10069)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 20:31:16 -07:00
Zuz666andYuris Auzins ffe54a59a3 fix(skills): make skippedReason switch exhaustive (#9594)
The type-aware switch-exhaustiveness lint rule requires explicit cases
for 'current', 'duplicate', and undefined; they fall through to the
existing generic skipped message, so behavior is unchanged.

Co-authored-by: Yuris Auzins <zuz666@users.noreply.github.com>
2026-07-22 20:04:41 -07:00
OrcaWinandOrcaWin a2e440b308 fix(sidebar): identify remote server workspaces (#10061)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-22 23:03:39 -04:00
Jinjing bae29fd1e2 fix(native-chat): handle reordering with timestampless transcripts (#10050)
Grok transcripts carry no timestamps. Previous logic excluded these rows
from matching, leaving pending sends and launch prompts unmatched and
causing the seeded bubble to appear rank-pinned at the list tail — which
reads as conversation reordering.

Now pending sends, launch prompts, and their pruning rules treat null
timestamps as matching-eligible, allowing echo suppression and proper
cleanup of delivered messages.
2026-07-22 19:48:55 -07:00
NeilandOrca 099003188d fix(file-explorer): stop italic ignored filenames clipping ".md" to ".ma" (#10047)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 19:47:02 -07:00
Brennan Benson 4fce2de494 fix(mobile): keep native chat from resizing the covered terminal PTY (#9988)
* fix(mobile): keep native chat from resizing the covered terminal PTY

Native chat reads the agent transcript stream and never renders the
terminal grid, but two paths still pushed phone dimensions into the
covered PTY, reflowing the desktop terminal for no benefit:

- The covered lease-only subscribe carried the cached viewport, and
  handleMobileSubscribe phone-fits the PTY whenever a viewport is
  present. The lease now omits the viewport so the host keeps the
  desktop baseline and late-binds on return to the terminal tab.
- useTerminalViewportRefit measured the still-mounted WebView under
  the chat overlay and sent terminal.updateViewport on rotation,
  keyboard, text-scale, reconnect, and iOS-resume triggers. Refits
  are now suppressed while native chat covers the active terminal;
  the triggers already mark the viewport stale, and the
  return-to-terminal resubscribe re-measures.

* fix(mobile): harden native-chat resize suppression
2026-07-22 19:36:22 -07:00
Jinjing 6d55c7fa16 rename: rebrand user-facing Native chat to Chat UI (#10036)
Update desktop experimental settings, mobile settings/onboarding, i18n
(en/zh/ja/ko/es), and user-visible error strings. Keep internal APIs and
identifiers as nativeChat.
2026-07-22 19:14:44 -07:00
OrcaWin 0326594d52 Update paired Orca servers from the active client (#9839) 2026-07-22 18:52:37 -07:00
Neil 7a422712b1 fix(settings): stop React #185 in Voice speech-model dropdown (crashes 7f93d89c, 362b566d) (#10028) 2026-07-22 18:37:49 -07:00
OrcaWin 41751dd90d fix(runtime): route HUB-owned SSH worktrees through owning runtime (#9994) 2026-07-22 18:25:05 -07:00
OrcaWin 3b86405201 fix(terminal): stop overlay fallback measure-fit loop (#10026)
Coalesce identical and sub-pixel fallback overlay measurements so ResizeObserver and xterm fit cannot sustain a render feedback loop, while preserving precise committed geometry.

Adds regression coverage for stable measurements, sub-pixel jitter across integer boundaries, and genuine resizes.
2026-07-22 18:22:53 -07:00
Jinjing 72a0a4dbb8 Revert "Show current branch in Source Control header (#9787)" (#10032)
This reverts commit 56a31a5af0.
2026-07-22 17:57:32 -07:00
Brennan Benson 3f8953681f fix(macos): keep standalone local identities isolated 2026-07-22 17:48:17 -07:00
NeilandOrca bc301de7be fix(worktree-palette): guard undefined review title in matcher (crash c5d87873) (#10024)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 17:43:14 -07:00
Brennan Benson 9b16fdc2de Merge branch 'main' of https://github.com/stablyai/orca into brennanb2025/tcc-local-build-bundle-id 2026-07-22 17:30:08 -07:00
Brennan Benson c8381f3ea7 Preserve Codex [tui] settings across managed CODEX_HOME remirrors (#9475)
* fix(codex): promote [tui] settings so they survive the managed-home remirror

Codex TUI preferences (/statusline, theme, terminal title) are written into
the [tui] table of the managed runtime config.toml, but the write-back
promotion allowlist only covered four top-level scalars — so the next mirror
pass rewrote the runtime config from ~/.codex and silently discarded them.

Extend promotion to the [tui] keys the Codex TUI persists (status_line,
status_line_use_colors, terminal_title, theme), keyed as structured tui.*
paths so the same three-way merge (runtime vs baseline vs ~/.codex) applies:
in-Codex changes promote into ~/.codex before the mirror, and outside edits
to ~/.codex still win over stale runtime values.

The byte-preserving upsert moves to codex-config-settings-upsert.ts (max-lines)
and learns [tui] placement: replace an existing bare or dotted key in place,
insert into the first [tui] body, insert dotted beside existing dotted tui.*
keys, or create one [tui] table at EOF — never defining tui twice, including
when the system config holds an inline tui = {...} table.

* Add codex-config-settings-upsert to the CLI tsconfig file list

* fix(codex): keep tui upserts out of array tables

* fix(codex): handle quoted tui config paths during promotion

* fix(codex): harden tui promotion writes
2026-07-22 17:29:14 -07:00
9080205812 fix(rate-limits): authenticate OpenCode Go through a session cookie jar (#8047)
* fix(rate-limits): use session cookies for OpenCode Go

* fix(rate-limits): harden OpenCode session setup

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-07-22 17:21:30 -07:00
Jinjing adc020393e feat(ssh): open SSH workspaces in VS Code Remote-SSH (#10005)
Allow the existing "Open in" entries to launch a configured VS Code
launcher against an SSH-backed worktree via Remote-SSH:

    code --remote ssh-remote+<authority> <remote-path>

- Split the blanket SSH/runtime block into a capability model: file
  managers and non-VS Code launchers stay local-only (disabled with
  "Local only" metadata); a recognized VS Code command is enabled and
  forwarded with connectionId over a typed object IPC.
- Main process stays authoritative: rejects active/owned runtimes,
  resolves the SshTarget from the persisted Store, derives the authority
  (config alias, or username@host on port 22, or ssh-alias-required on a
  non-default port), validates POSIX/Windows absolute remote paths without
  local stat/normalize, and rejects non-VS Code and compound commands
  before spawn.
- Authority and remote path are passed as separate argv; getSpawnArgsForWindows
  remains the cmd/bat shim boundary and fails closed on metacharacters.
- Same capability rules across the worktree menu, Explorer overflow, and
  the source-control entry context menu.

Refs STA-2386
Closes #9999
2026-07-22 17:17:59 -07:00
Brennan Benson 407d7afc11 feat(telemetry): classify codex trust-grant fallbacks and attribute grant lane (#10001)
* feat(telemetry): classify codex trust-grant fallbacks and attribute grant lane

* fix(telemetry): tighten codex trust-grant classification
2026-07-22 17:12:22 -07:00
Brennan Benson db56a9b7f3 fix(types): use concrete Node stat result type (#10017) 2026-07-22 17:11:55 -07:00
Jinjing 43ae014a64 Enable accessibility tree (ax) command on iOS emulator sessions (#10007)
* Enable accessibility tree (`ax`) command on iOS emulator sessions

Fetch the accessibility tree from serve-sim's /ax endpoint, which requires an
active session but provides the same UI snapshot capability as Android's
uiautomator output. Derive the endpoint from the stream URL when not explicitly
provided by the helper, and route through the bridge to pass session context to
the backend.

* Add ax command routing and backend integration tests

Tests verify accessibility tree routes through EmulatorBridge,
Android backend ignores iOS-specific ax URLs, and ax endpoints
are derived from serve-sim stream URLs.
2026-07-22 17:10:54 -07:00
NeilandOrca 5d3b968283 perf(runtime): arm the WS-server heartbeat only while clients are connected (#9885)
* perf(runtime): idle websocket heartbeat without clients

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

* fix(runtime): probe immediately when the WS heartbeat arms

Arming the heartbeat on the first accepted connection started a fresh interval,
so the first liveness ping was a full interval (~15s) out — a socket that died
right after connecting went unprobed for that window. Run one sweep synchronously
in start() so the first ping goes out at arm time; the seeded socket is pinged
(never reaped on the arm sweep) and reaped on the next tick only if it never pongs.
Tests updated for the earlier first probe.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 16:21:54 -07:00
NeilandOrca b65c5939a2 perf(web): pause the runtime heartbeat while the window is hidden (#9883)
* perf(web): pause runtime heartbeat while hidden

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

* fix(web): preserve inbound-liveness baseline across a hidden heartbeat re-arm

The visible re-arm rebaselined lastInboundFrameAt=now, so a socket that went
silent while the window was hidden looked freshly-heard-from and its death was
masked for another full idle window (~25s). Move the fresh-connect baseline into
startHeartbeat (the real 'we just connected' moment) and have the visible re-arm
only reset the tick clock + clear an in-flight probe, preserving lastInboundFrameAt
so the next visible tick probes a stale connection promptly and closes if unanswered.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 16:21:51 -07:00
Brennan Benson b63d4cde28 feat(agent-status): question glyph for "needs you" state everywhere (#9996)
* feat(agent-status): show question glyph for needs-you state everywhere

Replace the amber attention dot with the dashboard's MessageCircleQuestion
chat glyph for the waiting/permission "needs you" state across all
surfaces: the agent dashboard cards, in-app dashboard rows, the sidebar's
Agent Dashboard quick-indicator counts, the worktree-level status dot, and
the shared agent-row/terminal-tab indicators.

On the dashboard card the header glyph is suppressed when a question
summary pill is present so "needs you" reads once, not twice.

* test(agent-status): assert amber question glyph, not amber dot

The needs-you unification replaced the amber dot with the amber
MessageCircleQuestion glyph, so update the remaining state assertions in
DashboardAgentRow, WorktreeCardStatusSlot, and TerminalTabLeadingIcon to
match (lucide-message-circle-question + text-amber-500).

* test(agent-status): cover needs-you glyph surfaces
2026-07-22 16:15:25 -07:00
Brennan Benson 682d336cb9 fix(macos): harden package identity selection 2026-07-22 16:14:42 -07:00
a356b9d5c2 fix(worktrees): show CLI-created local worktrees in the sidebar while a remote runtime is active (#6628)
* fix(worktrees): refresh local worktrees in the sidebar while a remote runtime is active

When a remote runtime is active, a local `worktrees:changed` event for an
unbound repo was dropped by the renderer guard in useIpcEvents. Worktrees
created outside Orca for that repo (e.g. `orca worktree create` from a CLI or
automation flow) therefore stayed invisible in the sidebar until an app
restart, even though their sessions were already running.

The guard existed because an unbound repo's list fetch routes to the active
runtime (settingsForKnownRepoOwner's unbound fall-through), so refreshing with
local worktree ids could query — and purge against — the remote host.

Instead of dropping the event, pin the refresh to the local host
(forceLocalOwner): fetch the worktree list against the local owner and merge
additively. The merge is host-scoped and the deletion-purge is skipped on this
path, so it only ever adds local-host worktrees and never overwrites the active
runtime's worktree state. A genuinely-removed local worktree is reclaimed by
the next unguarded full refresh.

* test(e2e): regression — CLI-created worktree visible while a remote runtime is active

Drives the real `orca worktree create` path: the CLI RuntimeClient calls
`worktree.create` over the app's socket, registering a managed worktree and
firing the `worktrees:changed` IPC the renderer listens for. Stages a remote
runtime as active by injecting `activeRuntimeEnvironmentId` into the renderer
store, so no real remote host is needed. Fails on the prior behavior (the
worktree never appears while a runtime is active) and passes with this fix.

* fix(worktrees): pin local lineage refresh during runtime activity

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

* review: trim comments to house style, normalize queue coalescing to booleans

* review: sweep rename-grace expiry before early returns in worktrees:changed handler

* review: document accepted workspace-space gap, drop imprecise 'additive' wording

* fix(worktrees): route duplicate local repo events locally

* fix(worktrees): tag local worktree events at origin, gate purge skip on runtime overlap

* test: pin origin-based forceLocalOwner with a no-runtime local event assertion

---------

Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-22 16:10:53 -07:00
Brennan Benson bc57cf8787 fix(sidebar): use automation icon for Hide automation-created filter (#10000)
The workspace-options filter row used the Workflow icon while the
Automations nav item and page use CalendarClock. Match them so the
filter clearly maps to automation-created workspaces.
2026-07-22 16:09:19 -07:00
OrcaWinandOrcaWin b606117eed Fix paired remote terminal parse backpressure (#9683)
* fix(terminal): defer remote output ACKs until parse

* test(terminal): document synchronous credit claims

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-22 16:03:42 -07:00
OrcaWin 0d82cc3dbb Fix legacy worktree lineage projection after stable updates (#9913) 2026-07-22 15:52:42 -07:00
Brennan Benson 8dc1ca4920 fix(daemon): retire macOS daemons whose login session died (#7936) (#9826)
* fix(daemon): retire macOS daemons whose login session died (#7936)

A daemon that survives a full macOS logout is unsalvageable: its PAM
context can no longer host login(1) spawns (every new PTY becomes a
'Login incorrect' prompt zombie) and its Mach bootstrap namespace has
lost the system DNS resolver, so terminals it hosts have no egress.
Today it also keeps the #9301 preflight's cached 'accepted' verdict, so
it keeps wrapping spawns in login(1) forever; only a manual daemon
restart recovers.

GUI-spawned daemons now watch for login-session death from the inside:
a fresh cache-bypassing PAM probe (triggered by PTY-exit bursts, fresh
client hellos, and a slow periodic timer) must conclusively reject
three consecutive times AND the in-process system resolver must be
degraded; then the daemon exits crash-style so session meta stays
unclean and the replacement daemon cold-restores scrollback. A
conclusive rejection also flips the spawn-wrapper cache off
immediately.

Headless serve/SSH daemons never get the watch (they must survive their
spawning session ending), and a session that never conclusively
accepted login(1) never arms it — a PAM anomaly alone can't kill a
healthy daemon (fast user switching keeps accepting, so switched-away
sessions are preserved).

* test(daemon): e2e seam to drive login-session death oracles from a verdict file

A dead macOS login session cannot be fabricated without root (PAM owns
audit-session teardown), so live lifecycle QA drives the death watch's
probe and resolver oracles from ORCA_E2E_LOGIN_SESSION_PROBE_FILE:
'alive' → accepted/healthy, 'dead' → rejected/unhealthy, anything else
inconclusive — with compressed watch timing. Mirrors the existing
ORCA_E2E_DAEMON_INIT_DELAY_MS seam; inert unless the env var is set.

* fix(daemon): close the hang-shaped gap in login-session death detection

The conclusive-PAM-verdict trigger had one blind failure shape: login(1)
hanging at the prompt past the probe bound (killed → inconclusive
forever → the watch never fires). Three changes close it:

- The death-watch probe gets its own 4s bound (the 500ms preflight bound
  exists for spawn-path latency, which doesn't apply off-path), so a
  slow-but-answering PAM stack isn't misread as a hang.
- An inconclusive pipe probe escalates to a PTY-hosted probe via
  script(1) — a dead session's PAM stack may only misbehave under a real
  tty (the pipe-vs-PTY fidelity limit the preflight documents).
- A streak of timeout-killed probes (which a live session never
  produces) is a second retirement trigger, at a higher threshold (5)
  and still gated on the degraded resolver, logged with a distinct
  cause so field logs discriminate the two paths.

Every dead-session behavior — fast reject, prompt-then-EOF, or hang —
now fires retirement; all inconclusive states still fail toward
preserving the daemon.

* fix(daemon): keep login-session retirement conclusive

* fix(daemon): stop login watch before clean shutdown

* fix(daemon): make login-session PTY probe reliable

* fix(daemon): close login-session watch races

* fix(daemon): ignore health probes for login watch activity
2026-07-22 14:45:03 -07:00
Brennan Benson 4062ed5a73 perf(worktrees): back off worktree scans for agent-scratch repos (#9985)
Production crash diagnostics measured ~128 `git worktree list` execs/min
(9,400 in one 80-minute session, ~16% of wall-clock in git subprocesses):
the resolved-worktree scan fans out over every registered repo on a 30s
cache TTL, and most registered repos on the affected installs were
agent-CLI scratch repos (~/.codex-tmp capsules, vendor imports, skill
checkouts) that need no freshness.

Classify agent-scratch repo roots with a curated shared matcher and stamp
their scan-cache entries with a 5-minute TTL instead of 30s. Orca-driven
mutations still bypass the TTL via the per-repo generation bump, so only
passive pickup of external changes slows for scratch repos. Expected
steady-state reduction on the measured install: ~82% fewer git spawns.
2026-07-22 14:42:58 -07:00
Jinjing b0e36169d8 fix: return native promise from awaitClick on Zone.js pages (#9995)
Zone.js patches the global Promise with a non-native thenable. When a bare
`new Promise(...)` crosses the Electron executeJavaScript boundary, it's
serialized as-is, losing { page, target } and exposing __zone_symbol__*
fields instead. Wrap in an async IIFE to return a native promise that
Electron always unwraps correctly.
2026-07-22 14:37:34 -07:00
NeilandOrca 8cfb8a2a2b perf(emulator): park device stream when the window is hidden (#9842)
The iOS MJPEG and Android scrcpy device streams are gated only on the pane
being the active tab (isActive, PR #7382). When the emulator tab is frontmost
but the whole Orca window is hidden/minimized/occluded/display-asleep, the
full-fps pipeline keeps running: main-process socket read + JPEG/H.264 decode
+ IPC + renderer decode. Renderer background-throttling (#9395) cannot stop it
because the pipeline is IPC-push driven from main.

Gate showStream additionally on window visibility via a new occlusion-safe
hook that honors the terminal stale-visibility latch (so a display-sleep
occlusion wedge can't freeze the emulator on a black frame) and delays the
visible->hidden park by 500ms so a quick Cmd+Tab round-trip doesn't renegotiate
the device stream.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:33:03 -07:00
NeilandOrca 17e471c51e perf(editor): memoize the markdown preview render pipeline (#9843)
react-markdown's <Markdown> has no internal memoization: it rebuilds the whole
unified remark->rehype->highlight->katex processor and re-parses the document on
every render. MarkdownPreview re-renders on internal state that does not affect
the rendered output — most visibly, every keystroke in Find (query/match-index
state) — so a large doc re-ran the full parse + syntax-highlight + KaTeX pass per
keypress, making Find laggy.

Hoist the two fully-static plugin arrays to module scope (a fresh array identity
per render would defeat the memo) and render the body through a React.memo'd
MarkdownBody keyed on content + components. The pipeline now re-runs only when the
rendered content or the components map actually changes; Find/review-pulse/copied-
note re-renders skip it. The components map was already memoized, so its identity
is stable across those re-renders.

Behavior unchanged: 106 existing MarkdownPreview tests pass.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:32:34 -07:00
Jinjing 034aeb15e4 feat(editor): seed Find from selected text (#9982) 2026-07-22 14:11:19 -07:00
Brennan Benson d6c9fcd537 feat(mobile): surface pairing-auth failures on desktop and mobile (#9782)
* feat(mobile-pairing): surface unpaired-device auth failures instead of silent 4001 loops

Desktop: when a phone repeatedly fails direct-transport E2EE auth with a
token missing from the device registry (pre-v1.4.106 pairing-path bug left
desktops that regenerated their registry rejecting paired phones forever),
throttle to one notification per session and show an actionable toast
pointing at Settings -> Mobile to re-pair.

Mobile: map a bare 4001 close onto the existing auth retry budget (the
encrypted e2ee_error is undecryptable when the desktop keypair changed, so
the close code is the only surviving signal) instead of looping the generic
reconnect forever, and make the auth-failed verdict say 'Pairing invalid -
re-pair with your desktop' instead of a bare 'Auth failed'.

* fix(mobile-pairing): handle stale keys and startup notification races

* fix(mobile-pairing): isolate auth notification failures

* fix(mobile-pairing): keep recovery alert actionable
2026-07-22 14:05:37 -07:00
OrcaWinandOrcaWin 8f40419800 fix(agent-status): cap the live agent-status map to stop Windows renderer OOM (#9872) (#9936)
The live `agentStatusByPaneKey` map had no size cap. `setAgentStatus` rewrites it
with a full spread copy on every status ping and keeps rows until a pane/tab
teardown event removes them; a missed teardown (agent killed without a Stop hook,
pane/tab closed while its status lingered) orphans a row forever. On long
multi-agent sessions orphaned heavy rows accumulate without bound, and because
each ping spread-copies the whole map, once it is ~1.9 GB one ping transiently
doubles it past the 3586 MB old-space limit -> renderer crash (exit -36861).

Cap the live map at MAX_LIVE_AGENT_STATUSES=500, shedding only rows whose pane is
provably gone (a mounted tab's rooted layout no longer lists the leaf) or long
idle, and never an open pane's row (any state, incl. needs-input waiting/blocked).
Rootless/empty-snapshot, not-yet-hydrated, and no-renderer-tab (orchestration
worker) rows are unprovable: kept while a fresh agent could own them, shed only
past the stale window or by a hard-cap fallback that guarantees the bound.
Eviction bumps the status/sort epochs so the retention sync snapshots disappeared
done rows. Cost is one Object.keys length check on the reducer's already-O(n)
spread under the cap; the layout walk + sweeps run only on the rare over-cap ping.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-22 14:04:17 -07:00
Neil 53e018a1a6 fix(terminal): make SSH reconnect UI non-blocking (#9928) 2026-07-22 13:46:05 -07:00
Brennan Benson 56a31a5af0 Show current branch in Source Control header (#9787)
* feat(source-control): show current branch in header

* fix(source-control): keep header focused on branch

* fix(source-control): compact detached head identity

* fix(source-control): make branch identity keyboard accessible

* fix(source-control): keep create review in checks
2026-07-22 13:29:45 -07:00
Brennan Benson 24b8fcc918 fix(mobile): restore the last-open tab when returning to a worktree on mobile (#9801)
* fix(mobile): persist per-device tab selection so worktree return restores the last open tab

A phone's tab selection lived only in the host's in-memory
ClientSessionTabSelectionStore. Any host restart wiped it, and the
per-device projection then fell back to deterministic topology, so
returning to a worktree on mobile always landed on the first tab
instead of the tab last opened on the phone.

Persist the per-device selections in the Store (keyed deviceId ->
worktreeId), hydrate them when the runtime constructs, and guard
projection so an early empty snapshot after restart cannot wipe a
hydrated selection before tabs arrive. Selections are pruned with the
worktree/repo and on device revoke, and malformed persisted payloads
degrade to empty instead of throwing.

* fix(mobile): harden persisted tab selection cleanup

* fix(mobile): preserve tab selection across worktree rename
2026-07-22 13:12:38 -07:00
Brennan Benson 4bbfa35563 fix(macos): isolate all local package identities 2026-07-22 12:50:10 -07:00
Brennan Benson 2a1c2b0af1 build(macos): use a distinct bundle id for local packaging
Local 'pnpm build' carried the release appId com.stablyai.orca under a
different (ad-hoc/local) signature; macOS TCC keys grants to bundle id +
signature, so local builds collided with and corrupted the installed
release's grant identity. Non-release packaging now uses
com.stablyai.orca.local, gated on ORCA_MAC_RELEASE — the same env that
enables release signing — so release artifacts are byte-identical.
Knock-ons covered: sidecar peer-trust allowlist, notification-status
sidecar bundle id, and the notification settings deep-link now trusts
the launchd-provided id.

Refs #9756
2026-07-22 12:21:48 -07:00
Brennan Benson 0121f571e4 fix(agent-status): map codex request_user_input questions to Needs You (#9861)
* fix(agent-status): map codex request_user_input questions to waiting

Codex 0.145 asks user questions via the auto-allowed request_user_input
tool (experimental default_mode_request_user_input): PreToolUse fires
while blocked on the answer with no Stop, so Orca showed the pane as
working/idle instead of Needs You. Map that PreToolUse to waiting
(mirrors grok's ask_user_question), exempt question waits from the codex
yolo auto-approval suppressor, and deliver native-chat answers to the
digit-commit selector by option number (typed labels are ignored and
Enter commits the highlighted first option). Older codex versions emit
no such event and are unchanged.

* fix(native-chat): preserve codex question answer semantics
2026-07-22 12:00:14 -07:00
JinjingandOrca 4c2bb508c3 feat(settings): make Language setting findable by native-language search terms (#9967)
The Language setting's native word (语言 / 언어 / 言語 / Idioma) only reached
settings search via the localized title in that word's own UI locale — so a
Chinese speaker on the default English UI could not find it by typing 语言.

Always-index the native word for 'language' in every supported language (plus
the previously-omitted Spanish native name Español), so speakers can locate and
switch to their language from any starting locale. Native words are
locale-invariant constants, so they are plain keyword literals with reviewed
localization-coverage allowlist entries.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 11:52:49 -07:00