Commit Graph
3367 Commits
Author SHA1 Message Date
Jinjing 7caa582841 Show coding agent icons on mobile terminal tabs (#6792)
* Show coding agent icons on mobile terminal tabs

Move agent title decoration and terminal title parsing utilities
from the desktop renderer to shared code for reuse on mobile.

* Extract agent title stripping and terminal agent resolution to shared
* Implement mobile agent identity resolution and title-cleaning helpers
* Render agent icons on mobile terminal tabs when an agent is active
* Strip leading status glyphs from tab titles when showing an icon

* Suppress PTY resize on resume for mobile-driven terminals

Avoid reasserting the PTY size on resume if desktop resizing is
suppressed. This prevents overriding the intentional drift from
desktop dimensions for parked or mobile-driven terminals.
2026-06-29 16:06:27 -07:00
Brennan BensonandOrca 53e2582cbe Keep new terminal tabs open during startup (#6796)
Co-authored-by: Orca <help@stably.ai>
2026-06-29 16:04:31 -07:00
Brennan BensonandOrca 08e824dda9 Hide Jump to Worktree for sessions in the current worktree (#6791)
Co-authored-by: Orca <help@stably.ai>
2026-06-29 16:03:24 -07:00
Brennan Benson 52d51fe828 Keep mobile-fit terminals at phone size during post-spawn size sync (#6794) 2026-06-29 15:24:50 -07:00
Jinjing a978222184 Clarify auto-merge UI and errors when direct merge is available (#6777)
GitHub rejects enabling auto-merge on a PR that is already mergeable
with a "Pull request is in clean status" error.

* Suppress "Enable auto-merge" option in the UI when direct merge
  is available, while retaining "Disable auto-merge".
* Translate the GitHub "clean status" GraphQL error into an actionable
  message recommending direct merge.
2026-06-29 14:40:53 -07:00
3f39d7548b Recommend Tailscale when the remote Orca runtime is unreachable (#6637)
* Recommend Tailscale when the remote Orca runtime is unreachable

When a remote-runtime connection fails (RemoteRuntimeClientError "Could not
connect to the remote Orca runtime."), append an actionable Tailscale hint to
the user-facing error, branched on whether the endpoint is already on a tailnet:

- Non-Tailscale endpoint: recommend connecting both devices over Tailscale and
  pairing with its Tailscale address, with a download link.
- Tailscale endpoint (*.ts.net or 100.64.0.0/10): point at the real causes —
  server offline on the tailnet, or Funnel reverted to tailnet-only — and note
  that already-paired devices reconnect without re-pairing.

Applied at the desktop transport chokepoint (status probe, in-use calls, and
subscriptions — connection failures reject, so the hint is applied to the thrown
error, not just ok:false responses) and at the web client's connect/timeout
sites. New pure shared helper mirrors withMacTailscaleDnsHint.

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

* Address review: scope CGNAT hint to IPv4 literals, track re-paired endpoint

- isTailscaleEndpoint: gate the 100.64.0.0/10 check on a full IPv4 literal so
  DNS names like 100.64.0.1.example.com no longer get tailnet-specific advice.
- callRuntimeEnvironment: capture the endpoint the queued closure actually used,
  so a re-pair between enqueue and dispatch can't append the wrong hint.

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

* Recognize Tailscale IPv6 endpoints and trailing-dot FQDNs in hint

The remote-runtime Tailscale hint classified IPv6 Tailscale nodes
(fd7a:115c:a1e0::/48) and trailing-dot FQDNs as non-Tailscale, so a
user already reaching their server over Tailscale by IPv6 literal was
wrongly told to 'connect both devices to Tailscale'. Pairing endpoints
can carry bracketed IPv6 literals (resolvePairingEndpoint), so this is
a reachable path. Normalize the extracted host (strip brackets and the
trailing FQDN dot) and add an IPv6 ULA-range check.

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

---------

Co-authored-by: s546126 <268420947+s546126@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 13:41:13 -07:00
Jinwoo HongandOrca 22b00a7cd2 fix(terminal): report PTY's applied size so dropped resizes self-heal (split-mount desync) (#6785)
Co-authored-by: Orca <help@stably.ai>
2026-06-29 13:35:43 -07:00
Brennan Benson 5f3f45354d Keep worktree hover popovers responsive while a workspace is deleting (#6780) 2026-06-29 13:35:07 -07:00
Nicholas Zolton 4febefd454 feat(browser): add Helium as a Chromium cookie-import source (#6768)
Register Helium (imput, bundle id net.imput.helium) so it is auto-detected
and importable from the picker on macOS like Chrome/Edge/Arc/Brave/Comet,
instead of only via the "From File..." JSON path. Reuses the existing macOS
Chromium decryption path unchanged.

Helium quirks (verified on a real install): its Keychain entry is service
"Helium Storage Key" / account "Helium" (not the usual "<Browser> Safe
Storage"), it stores data under its bundle id, and it keeps cookies at the
legacy <Profile>/Cookies path. Mac-only for now; Windows/Linux data dirs are
unverified so winRoot/linuxRoot are omitted and the source defaults off there.

Claude-Session: https://claude.ai/code/session_01SZzX6ktZaaCUg5PWnzniuC
2026-06-29 13:21:20 -07:00
Jinwoo HongandOrca 6e2b3911c1 fix(native-chat): gate view toggle to supported agents (Claude/Codex) (#6781)
Co-authored-by: Orca <help@stably.ai>
2026-06-29 13:04:48 -07:00
a56da57bb8 Add worktree labels for localhost ports (#6424)
* Add worktree labels for localhost ports

* Fix localhost label test fixtures

* Extend localhost worktree labels to agents

* Address localhost label review feedback

* Remove localhost label page injection; stream responses untouched

Drop the title/favicon HTML injection from the localhost label proxy.
The proxy now only relabels the hostname and pipes responses straight
through, so app CSP/cookies/bodies are preserved and large/streamed
responses are no longer buffered in memory.

- Normalize wildcard bind hosts (0.0.0.0 -> 127.0.0.1, :: -> ::1) before
  using them as a proxy connect target.
- Delete the favicon SVG generator and dead repoIcon/badgeColor plumbing.
- Remove orphaned LocalhostLabelMock i18n keys.

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

* Harden localhost label proxy from review

- IPC register: restrict proxy target to loopback or a matching scanned
  workspace port (close open-proxy/SSRF surface from untrusted renderer).
- Proxy: guard against ERR_HTTP_HEADERS_SENT on mid-stream upstream error;
  add error/cleanup listeners on client request/response and upgrade socket.
- labelLocalhostUrl: fall back to the raw URL when the proxy rejects a
  target (e.g. https) instead of throwing.
- Port label route: include worktreePath so button-open and terminal/CLI
  paths produce the same label.
- Terminal OSC link hover: discard stale async tooltip results via a hover
  token, matching the WebLinks path.

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

* Match default-port advertised hosts in localhost label target check

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

* Consolidate duplicated localhost label helpers

- Move the loopback host set, loopback-URL parser, and wildcard
  connect-host normalizer into shared/localhost-worktree-labels.ts; proxy,
  runtime, terminal link routing, and the IPC guard now share one copy.
- Extract the port -> repo -> worktree -> project label-route lookup into
  workspace-port-localhost-label-selector.ts (a hook plus an imperative
  resolver), replacing the block triplicated across the ports surfaces.

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

* Extract command-code prompt-status seed to its own module

Keeps launch-agent-in-new-tab.ts under the max-lines limit after the
localhost-hint additions, without a lint disable.

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

* Remove agent-facing localhost mechanism

Orca does not mutate user prompts or inject prompt snippets, so drop the
localhost-open agent hint entirely:

- Remove appendLocalhostOpeningHint / includeLocalhostOpeningHint and the
  hint constant from tui-agent-startup; agent prompts are no longer rewritten.
- Remove the ORCA_LOCALHOST_OPEN env var from local agent terminals (it was
  only discoverable via the now-removed hint).
- Remove the orca localhost label|open CLI commands, their workspacePorts RPC
  methods, and the runtime labelLocalhostUrl/openLocalhostUrl methods.

The feature is now purely structural: the loopback label proxy plus the
ports-panel and terminal-link 'Open in Browser' paths, which surface a
clickable labeled URL without touching agent prompts.

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

* Make localhost worktree labels opt-in (default off)

Serving a dev app under a different host than localhost:<port> can break
apps that bind cookies/sessions to localhost, so the feature should not
change existing users' Open-in-Browser behavior on upgrade.

- Default localhostWorktreeLabelsEnabled to false.
- Flip the gates to enable only when explicitly true (=== true / !== true)
  instead of treating undefined as enabled.
- Update the setting switch to checked only when explicitly enabled.
- Drop the now-unused runtime store settings field.

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

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 12:27:30 -07:00
2739310839 fix(terminal): converge post-spawn PTY size reconcile to fix split-mount column desync (#6725)
* fix(terminal): converge post-spawn PTY size reconcile to fix split-mount column desync

Follow-up to #6644/#6649. Those added a post-spawn PTY reconcile but capped
it at a FIXED 12 requestAnimationFrames whose counter advanced even on frames
where the pane was unmeasurable or the split layout had not yet equalized. When
a tab MOUNTS with a split layout already present (a new worktree opened with the
side split panel on), the real narrow split width settles AFTER frame 12, so the
reconcile gave up while xterm had reflowed narrow and the PTY stayed pinned at
the wide spawn width. The corrective xterm onResize is dropped during the hidden
mount window (isRendererPtyResizeAuthoritative() is false), so process.stdout.columns
stayed wide and interactive TUIs (Claude Code) rendered garbled until a manual resize.

Extract the reconcile into pty-size-reconcile.ts with an authoritative-gated
convergence loop instead of a fixed frame budget:
- While the pane is hidden (onResize dropped), the reconcile is the SOLE corrector:
  it keeps polling and forwarding every grid change (its transport.resize bypasses
  the visibility gate). Hidden frames never advance the settle counter.
- Once visible AND stable for SETTLE_FRAMES, it hands off to the live
  onResize/ResizeObserver path, which reliably catches any later reflow.
- Hard cap (MAX_FRAMES) guarantees termination; mobile-fit/locked frames are
  skipped; the reconcile handle is cancelled on dispose.

Harness: pty-size-reconcile.test.ts (14 tests) drives the loop with a deterministic
frame scheduler; the desync-repro tests fail against the old 12-frame logic and
pass on the fix. Adds an e2e "MOUNTS with a split layout present" test.

Caveat: headless Electron does not reproduce this layout-settle-after-rAF race
(the existing golden e2e passes even against the old buggy logic), which is why
#6644/#6649 merged with green e2e yet the bug persisted. The unit test is the
real regression harness; the e2e tests are integration smoke.

Made with [Orca](https://github.com/stablyai/orca)

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

* fix(terminal): re-assert PTY size on visibility resume to heal stubborn column desync

Follow-up within the same fix: the user reported "sometimes even resizing
doesn't fix it." Root cause beyond the mount-timing race — the renderer forwards
resizes fire-and-forget and dedupes on the size it THINKS it sent, but never
learns the PTY's actual size. A resize dropped main-side (the pane was hidden,
a mobile take-back resize-suppression window, or a provider no-op) leaves xterm
and the PTY silently diverged; a later same-cols layout fires no onResize, so it
never self-corrects.

Expose the PTY's last-APPLIED size to the renderer and re-assert on show:
- New read-only IPC pty:getSize -> ptySizes.get(id) (the size written only when
  a resize actually lands or at spawn — the authoritative "what the PTY believes
  it is"). Wired through preload (window.api.pty.getSize) + api-types.
- On visibility resume (noteVisibilityResume), the pane re-fits, reads the PTY's
  real size, and re-asserts via forwardPtyResize ONLY on genuine drift — so no
  spurious SIGWINCH on an already-synced resume (which would jar alt-screen TUIs).
  Routed through forwardPtyResize so the authoritative/mobile gates are
  re-checked at send time; remote-runtime PTYs (separate viewport channel) are
  skipped; overlapping resumes coalesce to one query.

Also register pty:getSize in the registerPtyHandlers removeHandler cleanup block
so re-registration (macOS re-activate / new window) doesn't throw on a duplicate
ipcMain.handle, and make the pty IPC test mock throw on duplicate channels like
real Electron so this class of omission is caught going forward.

Tests: 7 resume-reassert cases (drift / match / null / remote-skip /
mobile-fit-skip / hide-during-hop / overlap-coalesce), all non-vacuous. Full
terminal-pane + pty IPC suites green (1494 tests); typecheck (web+node) + oxlint
clean; e2e desync specs pass against a fresh build.

Made with [Orca](https://github.com/stablyai/orca)

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

* Stub PTY getSize API and skip redundant Wayland GPU sandbox tests

- Implement PTY `getSize` stub in `web-preload-api.ts` to satisfy API
  requirements for the web-preload environment.
- Skip the unfixed Wayland GPU sandbox negative control test if the
  target base branch already contains the sandbox workaround.

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-06-29 12:12:35 -07:00
Jinjing 1c30d28113 Surface GitHub check suites awaiting approval (#6717)
* Surface GitHub check suites awaiting approval to unblock merge

- Query the check-suites API endpoint to find suites with an
  "action_required" conclusion, which are often workflows awaiting
  "Approve and run" and do not have any associated check runs.
- Map the "action_required" status distinctly instead of treating it as
  a standard failure or omitting it entirely.
- Update the UI to render these suites with a warning icon, a dedicated
  "Action required" label, and a localized hint explaining that manual
  approval is required on GitHub.
- Count "action_required" checks as failed/blocking when deriving overall
  PR and task statuses so the UI does not report all checks passing.

* Enhance visibility and handling of action-required PR check suites

* Include check suite IDs in pending approval check names and URLs to
  allow navigating directly to the specific workflow run.
* Add an "action required" count badge to PR dialog and page checks tabs.
* Prioritize action-required checks in the checks preview summary.
* Use correct check run state for the action-required fallback hint in
  the right sidebar details panel.
* Add translations for the new status across all supported locales.
2026-06-29 12:05:57 -07:00
Jinjing 82d275c3ac fix: preserve edited PR workspace names (#6774) 2026-06-29 12:04:40 -07:00
Miguel Echávarri cd03928e94 fix(i18n): correct Spanish "PR" mistranslated as "relaciones públicas" (#6749)
Machine translation rendered the abbreviation "PR" (pull request) as "relaciones públicas" (public relations) across 32 Spanish UI strings — e.g. "Reabrir relaciones públicas" (Reopen PR), "Crear relaciones públicas" (Create PR), "Abrir cheques de relaciones públicas" (Open PR checks).

Adds the missing `es` rule to LOCALE_PHRASE_FIXES. Because "relaciones públicas" is a real Spanish phrase (unlike the CJK-only ko/zh patterns 홍보/公关), the rule guards on the actual `PR`/`PRs` token via a new optional `whenEnMatches` RegExp guard in applyPhraseFixes, so it never fires on unrelated English that merely contains a "pr" substring (approve, preview, press). `whenEnMatches` is backward compatible — existing `whenEnIncludes` rules are unchanged.

Regenerates the 32 affected es.json leaves. Adds a vitest spec covering the rewrites and a negative case proving the token guard ignores genuine "public relations" strings.
2026-06-29 11:56:26 -07:00
JinjingandOrca 630c1b45d7 fix: resolve PR start point at submit when checkout resolver is pending (#6772)
Co-authored-by: Orca <help@stably.ai>
2026-06-29 11:45:54 -07:00
Jinjing eb9f0efb21 Reload working-tree combined diff views on external file changes (#6769)
* Reload working-tree combined diff views on external file changes

Combined diff views (e.g. "Changes" and "All Changes" tabs) display the
working-tree diffs for all modified files but do not match any single
file path. As a result, the file system watcher previously did not
notify them when individual files were updated.

Check for active working-tree combined diff tabs once per event payload,
and trigger a reload notification on file changes so the diff sections
refresh automatically when files are modified externally.

* Reload combined diff view when matching single-file tab is dirty

When an external file change occurs and the matching open file is dirty,
reloading the individual file tab is skipped. However, if a combined
diff view (like the uncommitted Changes tab) is open, it still needs
to be refreshed to reflect the external changes.

Ensure we schedule a debounced external reload for the combined diff
consumer even when there are dirty matching files.
2026-06-29 11:27:09 -07:00
de00bd9d71 Clear stale agent identity when a pane is reused (codex vs claude) (#6543)
* Clear stale agent identity when a pane is reused (codex vs claude)

When a terminal pane is reused for a different agent, its launchAgent
goes stale. resolveTabAgentFromSignals ignored the terminal title while
launchAgent was set and ranked the stale launch ahead of the live title,
so the tab icon and notifications kept showing the previous agent
(e.g. codex when claude is now running).

Extract the title->agent mapping into terminal-title-agent-type.ts and
let a live, explicit title override a stale launchAgent once the pane has
shown activity. Apply the same guard to notification dispatch so a stored
snapshot from a different agent is not reused for the rich notification.
Also add the missing MiMo Code title mapping.

Fixes #6355

* test(terminal-pane): align completion fixture with live agent identity

The working->idle completion test paired a stored codex snapshot with a
live '* Claude done' title. After the stale-identity guard, an explicit
Claude title correctly drops a mismatched codex snapshot, so the rich
notification no longer carried codex's prompt/tool fields.

Make the fixture self-consistent (snapshot agentType: 'claude') so the
test still verifies the rich completion snapshot flows through on
working->idle, matching the corrected dispatch behavior. The discard
path is covered separately in use-notification-dispatch.test.ts.

Refs #6355

* Harden agent notification snapshot identity guard

---------

Co-authored-by: brennanb2025 <brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
2026-06-29 01:40:33 -07:00
Jinwoo HongandOrca 9675335da3 refactor(settings): unify network-address picker across mobile & server-share (#6715)
* refactor(settings): unify network-address picker across mobile and server-share

The 'Share this Orca server' form (Settings → Runtime Environments) had the
same uneven dropdown + always-visible custom-text-field layout the mobile
pairing screen used to have.

Extract a generic AddressPicker + CustomAddressDialog (validator and copy
injected) and use it from both surfaces:
- Mobile keeps its IPv4 / Tailscale *.ts.net grammar.
- Server-share gets a plain dropdown (incl. 'This computer') plus an
  'Add custom address…' row opening a dialog that accepts host, host:port,
  or a ws(s):// URL (new parseServerShareAddress validator + tests).

Collapses the server form's separate selectedAddress/customAddress state
into one, dropping the side-by-side text field. Muted (non-red) validation
hint, all strings localized (en/es/ja/ko/zh).

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

* fix(settings): bound the server-share connection-address dropdown width

Removing the side-by-side custom field left the picker as flex-1, which
stretched the trigger across the whole card for a short value like
'This computer (127.0.0.1)'. Give it min-w-[240px] max-w-full so it sizes to
content and only a long custom URL grows it (then truncates within the card).

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 01:36:49 -07:00
Brennan Bensonandbrennanb2025 171576b3e4 Disable Kitty enhanced keyboard protocol for local Windows ConPTY terminals (#6541)
Fixes #2434. Orca unconditionally advertised xterm's Kitty enhanced
keyboard protocol (vtExtensions.kittyKeyboard) to every pane, including
local native Windows ConPTY terminals. Several local Windows CLIs (e.g.
the Antigravity agy CLI) read the advertisement but do not decode CSI-u,
so once enhanced reporting is on they ignore Enter/Up/Down and other
navigation keys.

Add a terminal-keyboard-protocol gate that withholds the advertisement
only for a genuine local Windows ConPTY pane (reusing the existing
isLocalNativeWindowsConpty predicate), while SSH and macOS/Linux panes
keep enhanced reporting.

Co-authored-by: brennanb2025 <brennanb2025@users.noreply.github.com>
2026-06-29 01:29:16 -07:00
Brennan BensonandNeil 5fd104a17b Fix runtime GitHub work item details (#6473)
* Fix runtime GitHub work item details

* Update web GitHub API parity test

* Address runtime GitHub action guards

* Fix runtime details cache invalidation

* test: add #6429 regression guard for runtime details routing

* Extract GitHub runtime source helpers

---------

Co-authored-by: Neil <neil@stably.ai>
2026-06-29 01:11:43 -07:00
Jinjing 95e12be31c Keep disabled delete worktree option for primary checkouts (#6716)
* Keep disabled delete worktree option for primary checkouts

Primary checkout rows cannot be removed via git-worktree. To maintain
visual parity with non-primary cards, this keeps a disabled "Delete
Worktree" option in the context menu alongside the active "Remove
Project" action.

* Wrap disabled delete worktree menu item in a Tooltip

Replace the native title attribute with a Tooltip component. This ensures
that the explanation for why the primary worktree cannot be deleted is
reliably displayed when hovering over the disabled menu item.
2026-06-29 01:06:59 -07:00
299bc421e2 feat(mobile-pairing): combobox with manual network address entry (#6501)
* docs(spec): manual network address entry for mobile pairing

* docs(plan): manual network address entry for mobile pairing

* docs(plan): fix two test-spec issues in Task 1

* feat(mobile-pairing): add parseManualNetworkAddress validator

* docs(plan): fix buildComboboxEntries filter rule

* feat(mobile-pairing): buildComboboxEntries for network interface combobox

* feat(mobile-pairing): combobox with manual address entry

* docs(spec): align buildComboboxEntries behavior with corrected plan

* fix(mobile-pairing): use text-destructive token for inline error

* fix(mobile-pairing): route Use row through translate() with i18next interpolation

* refactor(mobile-pairing): extract NetworkInterfaceCombobox shared by MobileHero and Settings section

The Popover + Command + manual-entry UX lived only in the Settings →
Mobile → Network Interface section. The mobile pairing screen
("Step 2 of 2 — Pair this computer") had its own copy of the same
Select-based dropdown that did not support manual address entry. A
user trying to pair with a Tailscale MagicDNS hostname from the
pairing screen could not enter it.

Extract the typeable combobox into a shared
`NetworkInterfaceCombobox` component. Both surfaces now render the
same Popover + Command with manual address entry, the `Use "..."`
row, the inline validation error, and the `(custom)` trigger label.
Settings keeps its own Generate QR button, Refresh + Tooltip, and
Tailnet accordion around the combobox. The pairing screen keeps its
existing layout (label + combobox + refresh icon).

Net deletion: ~160 lines. Net behavior gain: manual address entry is
now reachable from both surfaces, not only Settings.

* style(ui): give CommandInput a visible background so the search box is not lost

The Popover content above the CommandList renders the cmdk CommandInput
with only a thin bottom border. Against a white popover background it
visually disappears, especially in the mobile pairing screen where the
popover sits inside a dark phone mockup. Add a subtle `bg-muted/30` +
`py-1` so the input row is unambiguous, without changing the input's
shape or behavior.

* fix(mobile-pairing): drop cmdk CommandItem, use plain buttons inside Popover

In `pnpm dev` HMR cycle the cmdk CommandItem `onSelect` dispatch was
unreliable — clicking the item fired the synthetic event but the parent
React state never received it, so the trigger label never updated after
the user picked a manual address or a refreshed interface.

Replace the `Command` + `CommandItem` primitives inside
NetworkInterfaceCombobox with a native `<input>` + `<button>` list
wrapped by Radix `Popover`. The list now responds to the user's first
click without any intermediate effect that could be skipped in dev mode.
The combobox keeps the same props contract, the same placeholder, the
same inline validation, and the same `Use "<address>"` row at the
bottom of the list.

Update MobileNetworkInterfaceSection.test.tsx selectors from
`role=option` (cmdk's) to `role=button` so the integration test still
asserts the right element. All 479 feature tests still pass.

* debug(mobile-pairing): log handleSelect* invocations to confirm click path

* fix(mobile-pairing): commit on pointerdown to beat Radix Popover close race

In dev mode Radix Popover's close-on-pointerdown handler occasionally
fires before React's synthetic click dispatch reaches the option button,
so the parent's selectedAddress never updates after the user picks a
manual address. Bind the commit handler to pointerdown (synchronous,
before any pointer-up / click synthesis) and call event.preventDefault()
to avoid text-selection side effects. Keep onClick as a fallback so
keyboard / touch / programmatic-dispatch paths still work.

* fix(mobile-pairing): keep manually-typed addresses across network refresh

`selectRefreshedNetworkAddress` used to fall back to the first OS
interface whenever `currentAddress` wasn't in the OS-enumerated
list — so a user who typed a Tailscale MagicDNS name saw their
selection snap back to LAN every time `loadNetworkInterfaces`
returned. Treat manual entries as sticky by passing an
`isManual` flag from the caller; `selectRefreshedNetworkAddress`
now keeps the address when the caller says the user typed it.

`MobilePage` tracks `addressIsManual` alongside
`selectedAddress`: `handleAddressChange` flips it on when the
picked address is not in the OS list, and `loadNetworkInterfaces`
passes it through so refresh keeps the choice.

* chore(mobile-pairing): remove debug logs and sync new-combobox-listbox i18n key

* fix(mobile-pairing): address CodeRabbit review on manual-address lifecycle

Three real bugs from review, plus a regression test:

1. NetworkInterfaceCombobox in MobileHero was disabled when
   `networkInterfaces.length === 0`, which locked users out of the
   only path to type a manual address during a transient empty
   discovery. Pass `disabled={false}` and let the combobox's
   own empty-state copy explain the situation.

2. `selectRefreshedNetworkAddress` returned `undefined` whenever
   `interfaces.length === 0`, even if the user had a manual
   address and `currentAddressIsManual` was true. Keep the manual
   address so a recovering discovery doesn't clobber it.

3. `loadNetworkInterfaces` could rewrite `selectedAddress` (e.g.
   when a refresh swaps to a freshly-discovered tailnet) but never
   updated `addressIsManual`, so the next refresh could revert
   the user back to LAN. Re-derive `addressIsManual` from the
   new address after every refresh.

Adds a regression test in
`mobile-network-interface-selection.test.ts` exercising the
empty-refresh + manual path.

* style(mobile-pairing): trim disabled-false comment to two lines

* refactor(mobile-pairing): replace typeable combobox with select + custom-address dialog

The Settings/MobileHero network selector used a Popover+search-input hybrid
that looked uneven and hid its validation error behind the open popover.

Replace it with a plain Select of discovered interfaces plus an
'Add custom address…' footer row that opens a small dialog for entering a
Tailscale hostname or static IP. Drops the 'MagicDNS' jargon for plainer
copy, keeps the '(custom)' trigger label, and routes all strings through
translate() with real es/ja/ko/zh translations. Removes the now-dead
buildComboboxEntries helper. Also adds the missing scrollbar-sleek class
the old list omitted (was failing pnpm lint).

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

---------

Co-authored-by: ppw-stack <ppw-stack@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 00:32:36 -07:00
AmsZuidas bb4c0e2162 fix(i18n): correct zh "Explorer" mistranslation (探险家 → 资源管理器) (#6706) 2026-06-29 00:18:54 -07:00
a85218dd60 feat: add terminal pane title shortcuts (#6508)
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 00:13:29 -07:00
WolfieandWolfgang Schoenberger b7f7c30904 fix(terminal): bundle nerd font symbol fallback (#5712)
Co-authored-by: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com>
2026-06-29 00:07:09 -07:00
lvfenandJinjing b776506809 Support submodule diffs and upstream-base compares in Source Control (#6350)
* feat(source-control): show submodule diffs with lazy expansion

Dirty submodules now expand inline in Source Control to reveal their
inner changes, with file-level diffs that are read-only from the parent
worktree. Inner status is fetched lazily only when a submodule is
expanded, so status polling never recurses into (possibly nested)
submodules. Adds a submodule-status path across local and SSH runtimes
and git providers.

* feat(source-control): add compare-against-current-branch setting

Adds a global setting (default off) that defaults the Source Control
compare base to the current branch's upstream so the panel prioritizes
local changes instead of the full delta versus the repository default
branch. When the branch has no upstream, the compare view falls back to
working-tree-only. This affects only the compare/diff view; the Pull
Request and rebase merge target are unchanged.

* refactor(source-control): extract submodule status hook and entry-action gates

Moves the lazy submodule-expansion state into a useSourceControlSubmoduleStatus
hook and centralizes per-row stage/unstage/discard eligibility into
source-control-entry-actions, shrinking SourceControl.tsx and keeping the
read-only submodule rules consistent across the row UI, bulk actions, and tests.
The hook adds a generation guard so a slow submodule-status response from a
previous worktree (common over SSH) can't write stale status into the current
panel. On the relay side, configured submodule paths are read through a
short-TTL per-instance cache so a burst of diff clicks does not re-read
.gitmodules over the SSH link. Adds tests for the new modules.

* fix(source-control): address submodule/compare review feedback

- Degrade git.submoduleStatus to an actionable reconnect hint when an older
  SSH relay lacks the RPC, mirroring clone()/worktreeIsClean fallbacks.
- Keep the branch-compare summary while upstream status is still loading so
  it no longer flickers when switching worktrees with prefer-upstream on.
- Mark the compare-base switch as type="button" to avoid form submission.
- Add diff base / source control keywords to the Git settings search catalog.
- Assert the compare-base toggle's own switch state and updateSettings call.

* fix(source-control): address second-round submodule/compare review feedback

- Route submodule inner diffs through resolveSubmoduleWorktreePath so a
  crafted .gitmodules path can't escape the selected worktree
- Clear statusReadsInFlight alongside the diff dedupe on git mutations so a
  post-mutation getStatus() can't join a stale in-flight read
- Clear the SSH diff dedupe in getSubmoduleStatus to mirror getStatus
- Derive list-view selection from the submodule-injected rows so expanded
  submodule children are selectable
- Refresh commit history when the upstream compare base changes

* Support staged submodule expansion and refine default compare base

- Support expanding and diffing staged submodule changes (HEAD vs index) independently of unstaged changes (index vs worktree).
- Track submodule expansion states using a compound key of area and path to prevent conflicts between staged and unstaged listings.
- Update the compare-against-upstream setting to a segmented control for the "Default Compare Base" policy.
- Fall back to the repository default branch when comparing a branch with no upstream, preventing comparison views from unexpectedly disappearing.

* Fix submodule staging behavior, WSL caching, and double-click toggles

- Namespace submodule path cache per WSL distro to prevent cross-distro
  collisions.
- Preserve the staged area of child entries when expanding unstaged
  submodules so staged inner changes do not open empty diffs.
- Prefix oldPath with the submodule path for renamed inner entries.
- Ignore click events where detail > 1 to prevent double-clicks from
  instantly collapsing newly expanded submodules.

* Secure submodule path resolution and prevent stale status updates

* Extract and centralize submodule path validation into a new
  `resolveSubmoduleWorktreePath` helper to prevent path traversal
  exploits when resolving paths from untrusted `.gitmodules` files.
* Invalidate submodule expansion state and increment the query
  generation whenever the active runtime environment or connection
  route changes, preventing out-of-order responses from writing
  stale data.

* Set git identity via CLI config options in test commits

- Extract test email and name into constants.
- Use `-c` config flags to pass user identity to `git commit` dynamically.
- This ensures commits succeed in submodule checkouts or CI environments
  where a local or global identity is not configured.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-06-28 23:43:33 -07:00
Neilandbennewell35 09c8b34c34 fix(file-explorer): search folder workspace roots (#6567)
Resolve the File Explorer and Quick Open runtime file-list target through known worktrees so folder workspaces provide their root path, and make connectionId resolution reactive for SSH folder workspaces after store hydration.

Supersedes #6170.

Co-authored-by: bennewell35 <142949922+bennewell35@users.noreply.github.com>
2026-06-28 23:43:23 -07:00
NeilandOrca bcce8e37e2 fix: queue AI Vault resume command in the configured Windows shell (#6571)
Resume-in-tab/drag-resume queued a cmd.exe-syntax command into a freshly
spawned tab whose live shell is the configured Windows shell (default
PowerShell). PowerShell mis-parsed the cmd ""-doubled wrapper and reported
"is not recognized as an internal or external command, operable program or
batch file", so the session never resumed.

Resolve terminalWindowsShell to a startup-shell family and quote the queued
command per shell (PowerShell Set-Location/$env, POSIX cd for git-bash),
only emitting the cmd /d /s /c wrapper when the live shell is cmd. The
copy-to-clipboard command is unchanged and stays cmd-wrapped.

Fixes #6152

Co-authored-by: Orca <help@stably.ai>
2026-06-28 23:20:26 -07:00
Jingyue Wu 5807ba808d Add text replace functionality to rich markdown editor (#6703)
Adds replace, replace all, match case, whole word, and the editor replace shortcut to the rich markdown editor. Includes review hardening for live replace matching and Unicode-safe whole-word boundaries.
2026-06-28 23:15:09 -07:00
NeilandChaDongWun eb89255e8f fix: background-mount hidden automation worktrees before launch (#6568)
Headless automation launches (launchAgentBackgroundSession) created an
inactive tab via createTab(..., { activate: false }) without first telling
the renderer to background-mount that worktree's terminal surface. As a
result the hidden surface either never mounted (no entry in
mountedWorktreeIdsRef) or mounted with display:none (zero-size, can't be
measured/fit), so the eager PTY buffer never flushed on the first mount —
the run tab showed only the shell prompt until an unmount/remount gave the
off-screen xterm a real layout box.

Dispatch BACKGROUND_MOUNT_TERMINAL_WORKTREE_EVENT immediately before
createTab, mirroring the established renderer-backed Codex startup path in
useIpcEvents. The Terminal.tsx listener adds the worktree to
mountedWorktreeIdsRef and marks it measurable for a 3000ms window, so the
hidden surface renders with opacity-0/pointer-events-none (a measurable box)
instead of display:none, letting the first xterm fit flush the buffer.

Extract the inline measurable-mount block into
background-terminal-worktree-visibility.ts (behavior-identical, now
unit-tested) and add unit + E2E coverage.

Fixes #6244

Co-authored-by: ChaDongWun <66347959+lovewave02@users.noreply.github.com>
2026-06-28 23:13:03 -07:00
Jingyue Wu 2aeb026cba fix: Monaco find widget toggle button positioning (#6683)
The generic `.button` height override was collapsing the expand/collapse
chevron to 20px, pinning it to the top instead of centering it vertically.
Switch to targeting `.button.toggle` and restore `height: 100%` so Monaco's
own layout centers the chevron over the find row.
2026-06-28 23:08:28 -07:00
Jinwoo HongandOrca 41e5c3918b Add cancel flow for Claude account login (#6702)
Co-authored-by: Orca <help@stably.ai>
2026-06-28 22:33:07 -07:00
Dvitash 29df9a3ab5 Fix remote OMP terminal title thrash (#6689)
Fixes #6619: OMP-owned remote/mobile terminals no longer flicker their tab
label between "OMP" and "Pi".

OMP wraps Pi, so it emits Pi-identity OSC titles and status frames during
active work. On the host, in mirrored remote tabs, and in the title-derived
sidebar rows, those frames were stored verbatim — so an OMP-launched pane
alternated between "OMP" (launch identity) and "Pi" (live frame). The fix
introduces a shared owner-normalization helper (agent-title-owner.ts) that
rewrites Pi-compatible titles/status entries to the authoritative launch
owner, but only when the incoming and owner profiles share the same
titleIdentityGroup — so true Pi sessions, unrelated agents, and custom titles
are left untouched.

Maintainer hardening on top of the original change:
- Skip the new getForegroundProcess probe entirely when launchAgent is already
  known (it is only ever the owner fallback when launchAgent is unknown), and
  gate the onPtyData trigger on a real status transition rather than per-frame
  braille-spinner title churn — avoiding a relay round-trip per output frame on
  SSH/daemon-backed terminals.
- Make the foreground refresh fire-and-forget on the mobile listing hot path
  (listTerminals/getWorktreePs) so latency does not grow per session and a
  throwing snapshot listener cannot abort the liveness sweep.
- Added regression tests + a Why comment on the renderer owner precedence.

Verified: 829 tests pass; node/web/cli typechecks clean; oxlint clean;
reproduced the flicker against main as a negative control and confirmed the
live renderer build collapses interleaved OMP/Pi frames to a stable OMP label
with zero Pi leaks while leaving true-Pi/unrelated/custom titles unchanged.

Co-authored-by: Dvitash <dvitash3414@gmail.com>
2026-06-28 22:25:15 -07:00
NeilandOrca 9b275fa16f perf(editor): skip markdown preview TOC parse while the panel is closed (#6700)
Co-authored-by: Orca <help@stably.ai>
2026-06-28 21:59:32 -07:00
Jinjing 61bb2f9df1 Fix macOS IME input mode detection (#6699)
* Gate punctuation forwarding on input source feature

Ensure ASCII and CJK direct punctuation keys are only treated as keydown
candidates if `forwardAsciiPunctuation` is enabled in the current input
source features. This avoids forwarding punctuation when the feature is
disabled, satisfying requirements where certain input methods (such as
Vietnamese) should not have punctuation forwarding enabled.

* Fix macOS IME input mode detection
2026-06-28 21:32:13 -07:00
Jinjing 01cf5e3b7d Support Vietnamese IME and synthetic Unicode in terminal forwarder (#6682)
Previously, the terminal IME workaround only forwarded punctuation for
macOS CJK input methods. This generalizes the system into a native text
forwarder that also handles:

- Short text replacements for Vietnamese input methods (Telex, VNI).
- Synthesized Unicode inputs with unreliable physical key metadata.

This prevents xterm's kitty keyboard protocol from encoding and
canceling these keydowns before the native glyphs can be committed.
2026-06-28 21:26:45 -07:00
NeilandOrca ad87bb0123 perf(editor): skip markdown TOC parse while the panel is closed (#6695)
The rich markdown editor rebuilt the entire Table-of-Contents outline on
every content change by running a full-document remark parse
(buildMarkdownTableOfContents), then discarded the result whenever the TOC
panel was closed — which is the default state. The parse is driven by the
300ms-debounced serialize path, so it fired ~3x/sec during sustained typing,
with cost scaling linearly with document size.

Gate the memo on showTableOfContents so the parse only runs when the panel is
actually open. Including showTableOfContents in the deps rebuilds the outline
the moment the panel opens, so there is no stale TOC.

Benchmark (config/scripts/markdown-toc-parse-benchmark.mjs), per content
change while typing:
  175 KiB doc / 200 headings -> ~65 ms median  (13.2 s cumulative over a
                                                 ~1 min typing burst)
  351 KiB doc / 400 headings -> ~138 ms median (27.8 s cumulative)
With the fix this drops to ~0 ms while the panel is closed.

Adds a unit test proving the parse is skipped (and a stable empty-array
reference returned) while closed, and still runs when open.

Co-authored-by: Orca <help@stably.ai>
2026-06-28 21:19:10 -07:00
NeilandOrca aa79465df5 fix(web): detect half-open server socket with a liveness heartbeat (#6697)
Co-authored-by: Orca <help@stably.ai>
2026-06-28 21:16:32 -07:00
NeilandOrca 5db5b23257 perf(renderer): drop per-tick matchMedia alloc + purge hibernation epoch map (#6696)
Co-authored-by: Orca <help@stably.ai>
2026-06-28 21:16:02 -07:00
Jinwoo Hong 63c8a6697d Fix remote-host file open denied during SSH hydration (#6648)
Opening a file to edit on a remote SSH host could fail with "Access denied: path resolves outside allowed directories" and stay stuck. Right after a session restore the SSH repo has not hydrated, so the owner lookup returns undefined ("owner unknown"); the editor treated that like null ("local") and read the remote path off the local filesystem, then the retry gate excluded "access denied" so the error latched permanently.

Distinguish "owner not yet known" from "definitely local": when the worktree's backing repo has not hydrated, fail with a retryable owner-not-ready error instead of a local read, and retry it on a steady cadence bounded to ~2 min. If the host never connects, surface a truthful terminal message with a Retry button rather than retrying forever; a successful read at any point recovers immediately. Hydrated local repos resolve to null and are unaffected. The defect is platform-agnostic; Windows just surfaces the race most often.

Closes #6648
2026-06-28 21:03:48 -07:00
42b2ecc5c6 feat(emulator): Android emulation via scrcpy (cross-platform, iOS parity) (#6434)
* docs: add Android emulation design spec

Adds the design for first-class Android emulator support as a cross-platform
peer of the iOS simulator feature: an extracted EmulatorBackend interface
(iOS + Android), full AVD lifecycle management via the Android SDK, a live
scrcpy H.264 pane decoded in-renderer with WebCodecs, the full control surface
(tap/gesture/type/buttons/rotate), accessibility tree, app install/launch,
runtime permissions, logcat, and a dedicated orca-emulator-android skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(emulator): add EmulatorBackend interface + backend/codec session tags

First step of multi-backend emulator support: introduce the EmulatorBackend
type and tag each session with its backend kind + stream codec, defaulting to
ios/mjpeg so existing serve-sim behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(emulator): extract IosEmulatorBackend and make the bridge a router

Move the serve-sim/simctl device + helper + input mechanics out of
EmulatorBridge into IosEmulatorBackend (implementing EmulatorBackend). The
bridge now owns the session registry and lifecycle orchestration and routes
each command to the backend that owns the target device. iOS behavior is
unchanged; the existing bridge tests pass untouched and the backend gains its
own input-op coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): add pure Android leaf modules (sdk/adb/avd/scrcpy/input/ax)

Dependency-injected building blocks for the Android emulator backend, each unit-
tested in isolation: SDK + tool discovery, adb device/output parsing, AVD list +
boot arg building, scrcpy control-socket byte encoders, normalized<->pixel +
keycode mapping, and a uiautomator XML accessibility-tree parser. Not yet wired;
AndroidEmulatorBackend composes these in the next phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(emulator): gate availability on the iOS backend + lock it with tests

inspectEmulatorAvailability now decides iOS host support via the registered iOS
backend instead of a bare platform literal, routing the decision through the
multi-backend seam. Output shape and all messages are unchanged (the settings
pane still reads simctl/serveSim). Adds the previously-missing regression tests
covering the unsupported, ready, no-devices, and tool-failure paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): add Android app/permissions/logcat arg builders

Pure adb arg-builders + a logcat line parser for app install/launch, runtime
permission grant/revoke/reset, and logcat capture. Unit-tested in isolation;
wired into AndroidEmulatorBackend's capability verbs in a later phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): AndroidEmulatorBackend device management + unified device list

Adds the Android backend (registered alongside iOS in the bridge): SDK-gated
host support, device/AVD discovery and merge, AVD boot + boot-completion wait,
shutdown, and tap/swipe/type/button/rotate/exec via `adb shell input` so control
works without the scrcpy server (the live H.264 stream lands in the streaming
phase). Surfaces everything through a new cross-platform `orca emulator devices`
command (RPC emulator.listDevices -> bridge.listAllDevices) with a platform
column. Device inventory is split into its own module to keep files focused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): Android capability verbs (install/launch/permissions/ax/logcat)

Wires the Android capability operations into AndroidEmulatorBackend and exposes
them through a capability-gated bridge router (runCapability), RPC, and CLI:
- orca emulator install/launch/permissions/ax/logcat
Capabilities are advertised per backend; calling one on a backend that lacks it
(e.g. iOS) fails with emulator_unsupported instead of a silent no-op. Input ops
and capability ops are split into focused modules to keep files under the
line cap; the runtime shares one target-param type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(skill): add orca-emulator-android skill + cross-ref from iOS skill

Documents the cross-platform Android emulator control surface (devices, input,
hardware buttons, rotate, install/launch, permissions, ax, logcat) driveable via
the orca CLI today, and notes the live visual pane is in development. Points the
iOS skill's "when not to use" at the new Android skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): Android live-pane streaming scaffolding (scrcpy + WebCodecs)

Builds the H.264 video path as scaffolding: scrcpy frame/codec-meta parsing,
server-deploy arg builders, control-protocol encoders (committed earlier), the
stream session (server + sockets), a video pub/sub registry, the
emulator:videoStream* IPC channel, and a renderer WebCodecs->canvas hook. Pure
framing/deploy/registry are unit-tested; the socket/WebCodecs/jar integration is
clearly flagged UNVERIFIED and the remaining wiring (startSession, preload,
pane codec branch, packaging the jar) is documented in
docs/android-emulation-streaming.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fix streaming notes doc path in video-stream hook comment

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(emulator): add diagnostic probes for Android testing

Adds an emulator-probe logger (console + temp file at
os.tmpdir()/orca-android-emu-probe.log) and wires probes at four layers so
errors surface during manual testing: every emulator.* RPC call + error (RPC
dispatcher), every adb/emulator command + non-zero exit (command runner), and
the scrcpy session + video-stream IPC lifecycle. Temporary diagnostics; remove
or gate behind a flag once the Android pane is validated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): show Mobile Emulator settings cross-platform + aggregate Android availability

The Mobile Emulator settings section is no longer macOS-gated (Android works on
Windows/Linux), and inspectEmulatorAvailability now aggregates the iOS and
Android backends: Android devices/AVDs appear in the device list and a host
without iOS gets the Android setup message instead of "requires macOS".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): show Mobile Emulator sidebar nav entry on non-mac desktops

The settings sidebar nav registered the Mobile Emulator entry behind isMac, so
it stayed hidden on Windows/Linux even after the section content was ungated.
Widen it to showDesktopOnlySettings to match the section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): wire Android startSession to scrcpy + client-downloaded jar

AndroidEmulatorBackend.startSession now boots the device, ensures the scrcpy
server jar (downloaded by the client into the per-user cache on first use, not
bundled), starts a ScrcpyStreamSession, and feeds its H.264 frames to the video
registry; stopHelperForDevice tears it down. Sessions carry their backend kind
so worktree-active routing picks the right backend. Boot, host SDK discovery,
and the stream starter are split into focused modules to stay under the line cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): stop the iOS backend from claiming Android devices off-mac

iOS ownsDevice now returns false unless the host supports it, so on Windows an
Android serial routes to the Android backend instead of erroring with
"requires macOS". Backend-for-device fallback prefers a host-supported backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): scrcpy scid 31-bit + retry video socket until server delivers

Two fixes validated against a real emulator: scrcpy parses scid as a signed
32-bit hex int, so mask to 31 bits + pad to 8 digits (8-byte values overflowed
and the server exited). And adb accepts the forwarded TCP connection before the
server's abstract socket exists then resets it, so retry the video socket until
it actually delivers the dummy byte before connecting control. H.264 meta now
arrives (576x1280). Adds socket/server-exit diagnostics probes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): render the Android H.264 pane via WebCodecs

Wires the live Android pane end-to-end: preload exposes emulator video stream
APIs; the pane's device list uses the unified emulator.listDevices (Android +
iOS); and emulator-screen-stream-content renders a WebCodecs <canvas> for
scrcpy:// sessions (H.264, SPS/PPS prepended to the first keyframe) instead of
the MJPEG <img>. The video hook reports the stream size for the device frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): buffer the current GOP for late video subscribers

The renderer subscribes after attach already started the scrcpy stream, so the
registry now caches the current GOP (keyframe + following deltas) alongside the
codec meta and config, and replays it on subscribe. A pane opened mid-stream
decodes from the keyframe immediately instead of showing black until scrcpy's
next periodic keyframe (~10s). Refreshes the now-validated session doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): show New Mobile Emulator tab action off macOS

The tab create menu and its dropdown item gated the New Mobile Emulator action
on isMacOs, hiding it on Windows/Linux where Android emulation is now supported.
Gate on mobileEmulatorEnabled + onNewSimulatorTab (already cross-platform) so
the action appears wherever a mobile emulator backend is available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): open the Mobile Emulator tab off macOS

openMobileEmulatorTab and ensureSimulatorTab both returned null unless the host
was macOS, so the New Mobile Emulator action no-opped on Windows/Linux even
though the menu entry showed. Drop the isMacOsHost early-returns; the
mobileEmulatorEnabled setting and backend availability already gate the feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): resolve a default attach device across backends

emulatorAttach with no device fell back only to the iOS listSimulators picker
(empty on Windows/Linux), so the pane's no-device launch flow errored. Extract
resolveDefaultAttachDevice: iOS default first, else the first booted (else
first) device across host backends, so Android attaches without an explicit
device. Split into its own module to stay under the line cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): render the pane off macOS instead of an unavailable wall

EmulatorPane short-circuited to the "macOS only" EmulatorUnavailablePane on any
non-Mac host, blocking the now-working Android pane. Always render the pane
content; its device discovery and error surface handle a missing backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): let attach boot a shut-down AVD with a stale active session

getReusableActiveForWorktree called resolveDeviceId on the requested device,
which throws for a not-yet-booted Android AVD, aborting the attach. Guard it so
a resolve failure means "not the active device" and the attach falls through to
a fresh boot — so picking a shut-down AVD in the pane and hitting Connect boots
it via ensureBooted instead of erroring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): launch the AVD detached instead of via execFile

bootAndroidDevice started the emulator through the command runner (execFile with
a timeout + 1MB stdout maxBuffer), which kills the long-running, verbose emulator
process — so booting an AVD from the pane never actually came up. Spawn it
detached with no stdio and unref it so it outlives the call, mirroring how the
scrcpy server is launched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): dedupe concurrent attaches into one scrcpy stream

Extract AndroidStreamController to own the per-serial scrcpy lifecycle and
dedupe starts: concurrent attaches (e.g. the pane's auto-attach racing the tab
launch) now share one in-flight start and reuse the live stream instead of
spawning a second scrcpy server that fights for the port and kills the first.
Also initialize the registry GOP buffer in register() (latent type error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): boot the AVD headless without a console window

The detached spawn opened a Windows console (showing the emulator's verbose
qemu/netsim logs) and a redundant native emulator window. Pass windowsHide and
run the emulator with -no-window so it boots headless — the scrcpy pane is the
view, matching how iOS hides Simulator.app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): boot the AVD with a hidden console, not detached

detached: true sets DETACHED_PROCESS, which gives the console-subsystem emulator
no console — so it and its qemu/netsim children pop their own visible cmd window
that windowsHide can't suppress. Drop detached and rely on windowsHide
(CREATE_NO_WINDOW = hidden console) + unref; spawn already keeps it alive past
the launch call, and managed emulators are shut down on app quit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): keep Android emulators alive when switching devices

Attaching a different device shut down the active one (shutdownDevice: true),
which for Android meant killing the running emulator and cold-booting the target
(~60s) on every switch — and switching back. Add bridge.stopActiveForSwitch:
Android emulators stay running for instant switch-back, while iOS simulators are
still replaced. Switching to an already-running emulator is now immediate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(emulator): only resize the video canvas when dimensions change

The decoder output handler set canvas.width/height on every frame, which
reallocates the canvas backing store and forces an object-contain reflow each
frame — a needless per-frame cost. Resize only when the frame dimensions
actually change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): genericize copy + add Android Studio setup link

Replace iOS-only wording (Xcode/Simulator/iPhone) in the pane and settings with
backend-neutral copy so Android reads correctly on every platform. When no
emulator is available, the Mobile Emulator settings now show a "Download Android
Studio" link plus setup guidance (ANDROID_HOME / default install path). Removes
the now-unused, macOS-only EmulatorUnavailablePane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): show emulator SDK status in settings

The backend availability now reports the resolved Android SDK path, aggregated
into emulator.availability as an `android` block. The Mobile Emulator settings
render an "Emulator SDKs" card showing Android SDK (detected at <path> / not
found, with a Download Android Studio link) and, on macOS, iOS Simulator (Xcode)
status — mirroring the agent-control card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): locate a custom Android SDK folder from settings

Add an androidSdkPath setting and a "Locate SDK folder…" / Clear action in the
emulator SDK status card. The path is applied as the highest-priority discovery
candidate (falls back if invalid), and the backend's SDK is re-resolved on use
via a new AndroidSdkState — so locating or installing the SDK takes effect on
Refresh without restarting Orca. Guards the status card against older runtimes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): settle the scrcpy video socket once to stop retry storms

A failed TCP connect emits both 'error' and 'close', so retry ran twice and
scheduled openVideoSocket(attempt+1) twice — fanning out into an exponential
connection storm while waiting for the server to start listening. A runaway
chain could then hit attempt 100 and fail/close a stream that had already
connected. Replace the delivered flag with a single settled latch so each
socket retries (or delivers) exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): address CodeRabbit review findings

- avd-boot: handle spawn 'error' (an unhandled ChildProcess error crashed the
  main process); validate the target is a known AVD before launching.
- capability-ops: propagate adb non-zero exits for launch/permission/logcat and
  check the uiautomator dump before reading (avoids stale XML).
- scrcpy-video-registry: actually replay the buffered GOP on subscribe so late
  subscribers decode immediately.
- android-sdk-state: re-resolve host discovery every call so a changed SDK path
  takes effect live (no restart).
- android-sdk-discovery: require both adb and the emulator binary.
- emulator-bridge: fall back to the platform-primary backend (Android off-mac)
  so setup errors aren't iOS/CoreSimulator on Windows/Linux.
- scrcpy-server-download: dedupe concurrent first-use downloads + add a timeout.
- scrcpy-stream-session: idle-socket connect timeout; surface control-socket
  errors instead of swallowing them.
- android-exec: pass the whole command so the device shell parses quotes/pipes.
- avd-manager: match emulator log prefixes exactly (keep AVD names like
  PixelWARNINGTest).
- permissions: `pm reset-permissions` is global and takes no package argument.
- stream controller/starter: drop stale handles for dead streams; idempotent
  teardown. use-emulator-video-stream: stopVideoStream returns Promise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): populate the GOP buffer and reuse live scrcpy streams

Self-audit follow-ups in the same class as the CodeRabbit GOP-replay finding:

- scrcpy-video-registry: pushFrame never wrote to entry.gop, so the replay
  loop added for late subscribers iterated an empty array — a no-op. Build the
  GOP on ingest (start at each keyframe, append following deltas; don't buffer
  deltas before the first keyframe). Adds tests for population, reset, and the
  pre-keyframe guard.
- android backend: isSessionReusable was stubbed to always return false with a
  "no persistent stream yet" note, but scrcpy streams are persistent now — so
  every renderer remount tore down and respawned the server. Reuse a live
  stream (scrcpyVideoRegistry.has) so remounts reconnect, matching iOS. The
  device-mismatch check still runs first, so device switching is unaffected.
- Refresh stale comments that implied unfinished/unverified work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* WIP: Changes before auto-review fixes

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

* Refine mobile emulator availability settings

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

* Address emulator review follow-ups

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-28 21:01:09 -07:00
NeilandOrca 1dc63e9d64 perf: throttle hidden-pane agent inspection cadence to a 3s backstop (#6693)
A terminal pane that has ever run an agent keeps polling the OS process
table for completion detection even while HIDDEN. The cadence (750ms
active / 2000ms idle) did not depend on visibility, so a backgrounded
pane forked a `ps`-scan every ~750ms purely to keep the process-exit
backstop alive — wasting idle CPU, especially on shared SSH relays.

Follow-up to #6288 / PR #6667 (merged), which deduped scans WITHIN a
poll tick via a 500ms-TTL snapshot cache. This attacks the orthogonal
axis: the number of ticks a hidden pane runs.

Hidden panes now poll at a 3s cadence (HIDDEN_POLL_INTERVAL_MS). This is
safe because the process poll is only a BACKSTOP: the primary completion
signals — Orca-managed agent hooks (observeHookStatus, push via IPC) and
xterm title changes (observeTitle) — are event-driven and fire
regardless of poll cadence or visibility. Only a non-hooked agent with
no recognizable completion title relies solely on the poll; for that
case worst-case hidden completion-notification latency rises to ~2x3s
(~6s, repeat-idle-sample) from ~2x750ms.

Visible-pane cadence is unchanged (750ms). Becoming visible re-arms the
slow hidden timer at full cadence immediately, so switching back to a
throttled pane is not sluggish (scheduleNextPoll previously no-oped while
any timer was pending).

Measured inspection calls over 60s: hidden 78->20 (-74%); visible 78->78.

Co-authored-by: Orca <help@stably.ai>
2026-06-28 20:17:12 -07:00
Jinjing e59f5979aa Support SSH workspaces for AI Vault session resume and restrict runtime hosts (#6685)
* fixes https://github.com/stablyai/orca/issues/6270

* fix: support ai vault resume for ssh folders
2026-06-28 20:05:15 -07:00
Jinjing 49c0917dbe Prevent background terminal sessions from auto-activating (#6686)
* Support background and focused terminal presentation modes

* Add `presentation` field ('background' | 'focused') to terminal
  creation to control focus behavior.
* Prevent terminal creation from stealing UI focus by default.
* Return discoverability warnings when default terminal presentation
  fails, unless explicit background mode is selected.
* Update orchestration SKILL.md to clarify review-only worker
  completion rules and named owner handoffs.

* Prevent background terminal sessions from auto-activating

Ensure that terminal tabs created with 'background' presentation (such
as background agent sessions or locally backed renderer transports) do
not automatically activate or get selected as the active tab on remote
or mobile sessions.

- Add a selectIfNoActiveTab option to control auto-activation on mobile
- Set presentation to 'background' for remote runtime transports and
  agent background sessions
- Skip tab auto-selection when the presentation is background

* Keep background terminal create payloads focused
2026-06-28 19:52:04 -07:00
Neil 0976f9427c fix(linux): disable GPU sandbox to stop terminal input freeze on Wayland (#5319) 2026-06-28 19:43:18 -07:00
Jinjing 4a07a1dd86 fix(terminal): keep post-spawn reconcile alive through locks (#6684) 2026-06-28 19:24:37 -07:00
NeilandOrca 8555acbee4 fix: restore right-click "Mark Unread" in compact worktree cards (#6677)
Co-authored-by: Orca <help@stably.ai>
2026-06-28 19:19:35 -07:00
Jinjing 6e2a3a4b6a Keep custom and prefilled workspace names as user-authored (#6680)
Avoid automatically managing or overwriting manually entered or prefilled
workspace names in the composer state. This ensures user-authored names
are preserved and not overridden by linked-item or AI-generated naming,
unless the prefilled name matches the linked work item seed.
2026-06-28 19:00:47 -07:00
Jinjing 3c614b2e68 Fix terminal history, focus recovery, and stale PTY events (#6354)
* Wire terminal-initiated worktree navigation to back-and-forth stack

* Unify terminal-driven worktree activation and Cmd+J recency marking.
* Record worktree visits in the back/forward history stack unless navigating history.

* Resolve stale terminal focus and ignore late exits from old PTYs

- Repair the active terminal pane leaf selection when a pane's PTY exits, preventing focus from being stranded on a dead pane.
- Ensure stale, pruned, or unbound terminal leaves are not persisted as active in visual layouts or shutdown snapshots.
- Ignore late exit notifications and subscription end events from old PTY transports or remote stream handles after a reconnect or rebind.

* Prevent active terminal focus on dead panes and ignore stale PTY events

- Redirect active keyboard focus to a live, PTY-backed sibling pane
  if the focused pane dies, fails to spawn, or is hydrated without a
  live connection.
- Ignore stale data, replay messages, and subscription rejection errors
  from older PTY sessions after a transport has reconnected.
- Avoid persisting stale PTY bindings and dead pane focus inside
  terminal shutdown layout snapshots.

* Clear pending input when attaching a different remote terminal handle

When switching between different remote terminal handles, debounced input
meant for the previous terminal could get incorrectly flushed and sent
to the newly attached terminal.

Fix this by clearing the batcher's pending state (text and byte counts)
and invoking `inputBatcher.clear()` if the attached terminal handle
changes.

* Add window wake recovery hook for visible terminal panes

- Extract and enhance window focus and visibility change recovery logic
  into a dedicated `useTerminalWindowWakeRecovery` hook.
- Recover the xterm renderer, input surface, and WebGL texture atlases
  when macOS/display wakes or the window/tab regains focus.
- Schedule recovery steps with requestAnimationFrame to ensure the
  terminal layout settles correctly.

* Fix mutable variable capturing in remote transport test

Avoid mutating a local `let` variable from inside a Promise executor
closure by wrapping the rejection callback in a `const` object.

* Prevent duplicate terminal wake recovery on overlapping window events

When window focus and visibility events fire concurrently, they can trigger multiple redundant wake recovery passes.

- Return early if a wake recovery frame is already scheduled.
- Keep the scheduled animation frame instead of canceling it to ensure we get a settled recovery pass.
2026-06-28 18:11:15 -07:00