* Cut fseventsd load: fix macOS exclusion cap, drop wide always-on FSEvents streams
- @parcel/watcher maps plain-path ignores to FSEventStreamSetExclusionPaths,
but macOS caps exclusions at 8 per stream and fails closed. Orca passed 9,
silently disabling ALL daemon-side exclusions: fseventsd delivered every
node_modules/.git event to each per-worktree explorer stream (measured 16x
client CPU). Cap plain paths at 8 (extras demoted to userspace globs) via a
shared filesystem-watcher-ignore module, also unifying the runtime
file-watcher host list so identical roots share one native stream.
- worktree-base-directory-watcher subscribed recursive FSEvents streams over
every repo's ENTIRE workspace root (all worktrees) and whole common .git
(objects included) with glob-only ignores (zero daemon-side exclusion),
just to observe a few shallow paths. Replace with a 2s readdir/stat poller
(zero fseventsd clients, measured 70x less delivery CPU); SSH targets keep
provider.watch unchanged.
- serve-sim state watcher held an app-lifetime fs.watch on $TMPDIR (the
noisiest dir on the system); its existing 250ms existence poll makes the
parent watch redundant, so drop it.
Co-authored-by: Orca <help@stably.ai>
* Eliminate base-poller idle cost: mtime-gated ticks + narrow macOS worktree-metadata stream
- base targets: tick is now one stat of each gate dir (root + nested
containers); the readdir + marker fan-out runs only when a gate dir
changed, with a 30s ungated backstop and a bounded pending-marker
recheck list. Idle tick 600us -> 1.1us at 146 worktrees.
- git-common targets on macOS: replace the 2s entry sweep with one narrow
native stream on <common>/.git/worktrees (tiny, rare-churn tree): zero
idle syscalls and external worktree add/remove/HEAD detection back to
sub-second. Re-arms via existence poll across git worktree prune
deleting/recreating the dir. Win/Linux keep the listing poll (no
fseventsd there; a Windows dir handle could block prune).
- split git-common logic into worktree-git-common-watch.ts (max-lines).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: reordered default columns in kanban board
* Introduce dedicated flag to repair reversed default workspace statuses
- Add `_workspaceStatusesReorderedDefaultRepaired` to decouple the
one-shot repair from the initial status order migration.
- Ensure the repair runs for users who saved the reversed default
payload (with "Done" on the left) during a short-lived broken build.
- Support both "Completed" and "Done" labels when identifying default
status shapes to migrate or repair.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Add keyboard shortcut to toggle the Quick Commands menu
- New `tab.openQuickCommandsMenu` keybinding action (no default binding)
- TabBarQuickCommandsMenu listens for the binding and toggles open/closed
- Scoped to the active tab group naturally since the component only mounts when its group is focused
* Show keyboard shortcut in Quick Commands menu trigger tooltip
* Add tests
* expand tests
* Expand keyboard toggle to call handleOpenChange and skip repeated keys
- Replace `setMenuOpen` toggle with `handleOpenChange(!menuOpen)` so closing
via keyboard runs the same reset logic (query, focus frame, value override)
- Guard against key-repeat events to prevent rapid toggling on held key
- Wrap `handleOpenChange` in `useCallback` so it's stable enough to include
in the `useEffect` dependency array without causing spurious re-registrations
- Update tests to reflect that re-running the effect between presses is
required for the close path, and add a repeat-event test
* Add docstring to withShortcutHint func
* review: harden quick commands menu shortcut
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Keep the synthetic floating workspace local while a remote runtime is active, including terminal/browser creation, activation, close, and remote snapshot handling.
Maintainer follow-ups:
- require worktreeId for runtime-session terminal create payloads
- add renderer-backed terminal create reply sender regression coverage
- merge current main and keep the WSL readDir breadcrumb test aligned with main's Windows-only handler coverage
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules
Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.
Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):
error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse (19: copy-then-reverse -> toReversed)
warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type (aliasing footgun guard)
- typescript/no-unsafe-function-type (bans bare Function type)
- unicorn/prefer-array-flat-map (map().flat() -> flatMap())
- unicorn/prefer-regexp-test (.match() in bool ctx -> .test())
mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.
Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.
* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse
mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).
Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Fixes#5508.
Rejects mobile-scope phone-QR pairing in the full web client, keeps runtime browser access links on the instant startup path via advisory pairing scope metadata, and surfaces forbidden runtime scope errors instead of rendering empty workspaces or retry-looping setup checks.
Security note: pairing offer scope is UI metadata only. Runtime RPC authorization remains based on the server-side device token registry and mobile allowlist.
* Support respawn-pane in Claude Agent Teams tmux shim
Claude Code's pane backend spawns each teammate in two steps: it
`split-window`s a holding pane running `cat`, then `respawn-pane -k`s
that pane with the real teammate command. Orca's tmux dispatcher had no
`respawn-pane` case, so every teammate spawn failed with
`tmux: unsupported command: respawn-pane` and the real command was
dropped — agent teams were completely broken in Orca on current Claude
Code (2.1.x).
Honor respawn-pane by closing the placeholder terminal and re-splitting
from the pane's original origin/direction with the real command, keeping
the fake pane id stable so later send-keys/kill-pane/list-panes still
resolve. Panes now remember how they were split for faithful recreation.
Pure pane geometry/format helpers move to a sibling module to keep the
dispatcher within the 300-line cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* respawn-pane: create replacement before closing placeholder
Address review: closing the placeholder before the replacement split
succeeded could leave the fake pane id registered against a dead handle.
Split first; on cleanup failure, discard the new split and keep the
placeholder registered so send-keys/kill-pane/capture-pane still resolve.
Adds a regression test covering a failed respawn split.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 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>
* Keep mobile pairing data on stable userData path
Use the canonical userData path captured before app.setName() when starting the runtime WebSocket server so paired mobile device tokens and E2EE keys survive restarts/updates. Add regression coverage for app-name-driven userData path changes.
Co-Authored-By: Claude <noreply@anthropic.com>
* Migrate mobile pairing files from legacy path
Co-Authored-By: Claude <noreply@anthropic.com>
* Address mobile pairing migration review
Co-Authored-By: Claude <noreply@anthropic.com>
* Document mobile pairing path helpers
Co-Authored-By: Claude <noreply@anthropic.com>
* Explain paired mobile credential migration
Co-Authored-By: Claude <noreply@anthropic.com>
* Harden migrated mobile pairing credentials and tighten tests
Re-assert secure-file permissions after the migration copyFileSync, which
does not carry Windows ACLs, so the copied device tokens and E2EE keypair
keep the current-user-only restriction instead of relying on the runtime's
later lazy re-harden on read.
Import the pairing filename constants from the shared source of truth so a
rename can't silently pass the tests against stale names, and add no-op
regression coverage for the source==target (case-insensitive FS) and
fresh-install (no legacy files) paths.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Folder-workspace selectors can now resolve through the existing folder launch-scope path before falling back to normal worktree resolution. This fixes file explorer read/preview/search/list operations for local and SSH-backed folder workspaces without changing git target resolution.
Refs #6357.
Supersedes #6363.
Co-authored-by: bennewell35 <newelljben@gmail.com>
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>
* 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>
* 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
* 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.
Defect 1: the typed GitLab MR search query was dropped before reaching
the API. Thread query?: string end-to-end through the renderer effect,
the source-lookup, the preload/RPC args, and the desktop IPC handlers
(which previously passed a hardcoded undefined), and honor it on both the
glab REST path (&search=) and the cwd-inferred 'glab mr list' fallback.
Defect 2: when MR base resolution failed the renderer silently returned,
leaving baseBranch undefined so the worktree was created off the repo
default branch (origin/master) with no feedback. Surface the failure via
toast and clear stale base state, mirroring the GitHub PR path. Also make
resolveManagedMrBase resilient to an optional compare-base (target branch)
fetch failure: degrade gracefully by dropping compareBaseRef instead of
aborting, so a merged MR with a deleted target ref still resolves to its
valid source-branch base.
Fixes#6263
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): support Windows shell selection for SSH hosts
* fix(runtime): register SSH Windows capability preflight RPC
* fix(terminal): honor remote Git Bash and split preflight IPC seams
* fix(terminal): keep SSH Windows shell state tied to the selected host
* fix(terminal): preserve Windows SSH shell selection
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
* Preserve terminal scrollback when restoring a hidden alternate-screen pane
On Windows, an alternate-screen TUI pane (Claude Code) could lose scroll-up
after being hidden (tab/worktree switch or app backgrounded) and shown again.
Root cause: on hidden->visible restore, applyMainBufferSnapshot
unconditionally wrote a destructive clear (\x1b[2J\x1b[3J\x1b[H) before
replaying the snapshot, wiping xterm's scrollback. The attach/reattach path
already guards this same clear behind !isAlternateScreen (pty-transport.ts);
the snapshot path did not.
Thread an alternateScreen flag from the backend headless serialization
through the pty:getMainBufferSnapshot IPC + preload types into
applyMainBufferSnapshot, and skip the destructive clear when restoring an
alternate-screen buffer. Add a regression test asserting the clear is NOT
emitted for an alt-screen restore (and the existing normal-buffer test still
asserts it IS).
Fixes#5723
* test: account for alternateScreen flag in empty headless snapshot assertions
The alt-screen scrollback fix threads an alternateScreen flag through the
headless snapshot return type. Two sibling empty-buffer tests used exact
toEqual() assertions that did not include the new (legitimately false) key.
Update them to reflect the correct snapshot shape.
---------
Co-authored-by: brennanb2025 <brennanb2025@users.noreply.github.com>
* fix(automations): track exact run terminal targets
Record terminal pane keys and PTY ids on automation dispatches, persist them across run updates, and propagate them through runtime launch results.
Use the recorded pane and PTY for completion detection, session reuse, prompt submission, and View run navigation so split-pane tabs reopen the actual run terminal.
Keep legacy runs compatible by falling back to workspace resume when exact terminal identity is absent.
* chore: address automation targeting review cleanup
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Adopt pending terminal tab reveals
* Materialize pending remote terminal tabs
Wait for mobile terminal creates to produce a ready PTY handle before resolving. If the renderer only publishes a pending tab shell, materialize a headless PTY into the same tab/leaf identity so later renderer focus adopts it instead of duplicating the tab.
Add a pane-key spawn reservation shared by runtime materialization and renderer pty:spawn so focus racing fallback reuses the winning PTY, including when SSH env stripping removes ORCA_PANE_KEY. Cover the pending-surface fallback and both overlapping spawn race directions with regression tests.
* Settle pane spawn reservation on any post-spawn failure
The per-paneKey spawn reservation was only settled on spawn error, persist
error, or final resolve. A throw in the post-spawn region (seedHeadlessTerminal,
registerPty, rememberPaneKeyForPty, track) rejected the IPC promise but left the
reservation in paneSpawnReservationsByPaneKey forever, so every later spawn for
that pane awaited a promise that never settled and hung.
Wrap each post-reservation body (both spawn paths) in a catch-all that rejects
the reservation with the real error and rethrows; reject is a no-op once
resolved. Remove the now-redundant inner reject calls. Add regression tests for
both paths.
* Tear down runtime-owned headless tabs on mobile session close
The renderer-attached branch of closeMobileSessionTab closed the renderer's
pane but never pruned mobileSessionTabsByWorktree or dropped the persisted
binding. For runtime-owned headless terminals — serve/SSH materialized, or a
pending shell the renderer never adopted, all introduced by the pending-surface
materialize fallback — syncMobileSessionTabs then re-hydrates the closed tab
from the persisted session, resurrecting it with a live PTY. "Close others" on
a paired client showed closed remote tabs reappearing with their sessions still
running.
Add isRuntimeOwnedHeadlessMobileTab (serve/SSH-owned, or no live PTY and absent
from the renderer graph). When the whole parent of such a tab is closed, route
through closeHeadlessMobileTerminalTab to kill the PTY, remove the persisted
binding, and prune+emit, then best-effort notify the renderer so no adopted
pane is left dead. Genuinely renderer-owned tabs and exact split-leaf closes
keep the existing path. Cover the serve parent close, renderer-published vs
runtime-unadopted pending tabs, and the serve split-leaf case with regressions.
* Tear down only headless tabs the renderer never published on mobile close
The close-others teardown discriminator returned false for any resolved
non-serve/ssh PTY, so headless tabs carrying a daemon session id
(<worktreeId>@@<shortUuid>) that the host materialized but the renderer never
published leaked into the session snapshot and the client mirrored them
(confirmed live: host published 5 tabs for a worktree the renderer showed 1 for).
That id shape is ALSO minted for ordinary renderer-owned daemon-backed local
terminals, so it can't classify ownership by shape — the renderer graph
(this.tabs) does. Classify a tab as runtime-owned when it is serve/ssh (always,
they're preserved + re-hydrated) or when the renderer graph never published it
(a leaked/unadopted shell); delegate everything the renderer graph lists,
including ordinary @@ local terminals and pending tabs.
Regression tests: renderer-owned @@ tab is delegated (not de-persisted); leaked
@@ tab the renderer never published is torn down + de-persisted.
* feat(agents): native Xiaomi MiMo Code support
Add mimo-code TUI agent (detect mimo, --prompt, --session resume).
Inject MIMOCODE_HOME overlay and /hook/mimo-code status plugin on mimo
launch when agent status hooks are enabled; restore via shell-ready
wrappers. Reuse OpenCode-family hook normalization in the listener.
SSH remote MiMo hook overlays are not included (local/daemon first).
Closes#6220
* fix(mimo): remirror overlay config idempotently
rmSync overlay config before mirror so a second mimo launch does not
hit EEXIST in mirrorEntry and fall back to the user MIMOCODE_HOME.
* review: harden mimo code support
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Doan Bac Tam <24356000+doanbactam@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
- Decouple heartbeat and worker_done logic from the main coordinator
to allow re-use in the orchestration.check RPC method.
- Reconcile unread lifecycle messages during orchestration.check so that
manual coordinators can authoritatively process completions and
heartbeats even if they bypass the main coordinator loop.
- Add unit tests validating task completion, heartbeat recording, and
safety checks against stale or unauthorized worker_done payloads.
* Fix floating workspace terminal selector_not_found on a remote runtime
resolveWorktreeSelector threw selector_not_found for the floating-terminal
sentinel id (global-floating-terminal) because it is a repo-less synthetic
session with no entry in the worktree catalog. A remote client paired to this
serve sends that sentinel, so the normal id: lookup failed and the floating
pane rendered black. Resolve the sentinel (bare and id:-prefixed) to a virtual
ResolvedWorktree rooted at the serve user's home so the PTY spawns in a real
existing dir; unknown id selectors still throw selector_not_found. Adds two
regression tests.
Rebased onto current main (was 75 commits stale); net delta unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Scope floating terminal selector to terminal launches
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>