Add mobile emulator#4754
Conversation
There was a problem hiding this comment.
Important
A few correctness issues to address before merging — one causes silent state loss on pane closure for external simulators, and one triggers an infinite retry loop on attach failure.
Reviewed changes — Adds a mobile emulator backed by serve-sim, spanning CLI handlers, a main-process EmulatorBridge with lifecycle management, RPC methods, preload IPC channels, a renderer emulator pane with device-frame chrome, touch/scroll gesture streaming, workspace session persistence, and a PTY-based state watcher for external helper discovery.
- Add
EmulatorBridgewith session management — maps worktree→session, distinguishes Orca-managed from terminal-started (external) helpers, handles attach/reuse/replace/stop/kill/shutdown/destroy lifecycle, retries stale endpoint starts - Add
RuntimeEmulatorCommandswith 12 RPC methods — tap, gesture, type, button, rotate, exec, attach, list, kill, shutdown, listSimulators, unregisterActive; delegates to bridge - Add
ServeSimStateWatcher— watches$TMPDIR/serve-sim/state files and PTY output to detect externally-started serve-sim helpers without focus-steal - Add
serve-sim-executionmodule — resolves bundled/development serve-sim executables, handles Electron-as-Node mode, implements a macOSopenshim to suppress Simulator.app activation - Add
useEmulatorPaneSessionhook — manages device list, auto-attach, shutdown, and session state with per-worktree tab management, usesCustomEventbus for cross-instance coordination - Add
EmulatorDeviceFramewith gesture handling — renders iPhone/iPad-style chrome with dynamic scaling, live touch streaming via WebSocket, pointer/swipe/tap/wheel-to-scroll gesture resolution, and resize-observer-based responsive layout - Add
EmulatorPaneOverlayLayer— renders simulator tabs as CSS-anchored overlays on the owning tab group body, only active when the owning group is focused - Extend tab bar for simulator tabs — adds
simulatorcontent type to tab bar, right-split placement support, unified-tab persistence, and a "New Mobile Emulator" entry in the new-tab menu - Add CLI
emulatorcommands —list,attach,tap,swipe,type,button,rotate,exec,kill,shutdownwith device/worktree selectors - Integrate into runtime lifecycle — bridge singleton wired into
OrcaRuntimeService, watcher bound/unbound on PTY create/exit/worktree-remove,onAppQuitcleanup
ℹ️ CLI emulator handlers have no platform guard for non-macOS
On Linux/Windows the CLI emulator commands reach execFile('serve-sim', ...) which fails with ENOENT. The error message (Command failed: serve-sim ...) is cryptic compared to the clean emulator_not_macos error that ensureSimulatorBooted and shutdownSimulatorDevice produce. The renderer already shows EmulatorUnavailablePane; the CLI should provide an equally clear message.
Technical details
# CLI emulator handlers — no platform guard
## Affected sites
- `src/cli/handlers/emulator.ts:77-190` — all handler functions reach RPC without a platform check
## Required outcome
- A `platform() !== 'darwin'` check early in each handler (or a shared gateway) that throws a clear error like "emulator commands require macOS" before the RPC call
- Consistent with the `emulator_not_macos` error code already defined in `emulator-errors.ts`
## Suggested approach
Add a platform check in `getEmulatorCommandTarget` (or a new shared gateway) that throws `RuntimeClientError` with a user-friendly message when `platform() !== 'darwin'`. This avoids repeating the check in every handler.ℹ️ Zero log points in the emulator bridge and helper process management
There are no console.log/electron-log calls anywhere in the emulator bridge, serve-sim execution, helper process management, or endpoint readiness modules. Key lifecycle events — helper started, helper killed, session registered, session destroyed, watcher detected external helper, endpoint readiness check, reconnection attempts — produce no telemetry. Makes production debugging and support diagnosis blind for all emulator-related failures.
ℹ️ emulator.exec RPC allows arbitrary serve-sim subcommands
ExecParams uses z.string() without constraints, and the handler passes user input directly to serve-sim after stripping only Orca-specific targeting flags (--device/-d/--worktree). While execFile prevents shell injection, there is no whitelist of allowed subcommands. Destructive serve-sim features (e.g. --kill if the stripping logic misses a variant) could be reachable. The stripping logic in stripEmulatorTargetArgs covers the flag forms the CLI uses but serve-sim may have additional flags that should not be passed through. This is the documented "raw passthrough" design intent, but worth a comment on the trust boundary.
ℹ️ Nitpicks
useEmulatorTouchStreamreconnects at a fixed 750ms with no exponential backoff or max retry count — a permanently dead serve-sim WS endpoint causes unbounded reconnection attempts while the pane is open.EmulatorBridge.sessionsMap entries are never cleaned up whenunregisterActiveEmulatoris called (onlyactiveByWorktreeis deleted) — the session entry survives until app quit or explicit stop/kill/shutdown. Low impact (entries are tiny), but grows over many worktree lifecycle cycles.resolveSimulatorUdidinsimctl-simulator-devices.tsreturns the raw input string when bothsimctlandserve-sim --listfail to resolve a name to a UDID, instead of throwingemulator_device_not_found. The eventual error from serve-sim will be less clear than a dedicated error code.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
| return null | ||
| } | ||
| const session = this.sessions.get(key) | ||
| this.activeByWorktree.delete(worktreeId) |
There was a problem hiding this comment.
🚨 activeByWorktree.delete(worktreeId) executes before the managedOnly guard below. When managedOnly is true and the session is unmanaged (terminal-started), line 135 returns null — but the worktree→session binding was already deleted and is unrecoverable.
This path is hit when the last simulator pane unmounts (useEmulatorPaneSession calls emulator.shutdown with managedOnly: true). For an external (terminal-started) session, this correctly avoids killing the helper, but silently severs the worktree's ability to discover its active emulator. Subsequent CLI commands fail with emulator_no_active even though the serve-sim process still runs and the sessions Map still holds the entry.
Technical details
# activeByWorktree.delete before managedOnly guard
## Affected sites
- `src/main/emulator/emulator-bridge.ts:134` — `this.activeByWorktree.delete(worktreeId)` executes unconditionally
- `src/main/emulator/emulator-bridge.ts:135-136` — managed-only guard returns null *after* the irreversible delete
- `src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts:5848-5852` — unmount effect calls `emulator.shutdown({ managedOnly: true })`, which reaches `shutdownActiveManagedForWorktree` → `stopActiveManagedForWorktree({ managedOnly: true })` → `stopActiveForWorktreeInternal({ managedOnly: true })`
## Required outcome
- Move `this.activeByWorktree.delete(worktreeId)` to *after* the guard at line 136, before the stop action at line 138, so the entry is only removed when cleanup actually proceeds
- The sweep at lines 146-150 (deleting other worktrees referencing the same key) should also run after the guard
## Suggested approach
```typescript
const session = this.sessions.get(key)
if (!session || (options.managedOnly && !session.managed)) {
return null // entry NOT deleted yet — caller can retry
}
this.activeByWorktree.delete(worktreeId)
// ... stop, shutdown, sweep ...
```| if (loading) { | ||
| return | ||
| } | ||
| suppressAutoAttachRef.current = false |
There was a problem hiding this comment.
🚨 suppressAutoAttachRef.current = false at the top of attach() unconditionally resets the suppression flag. When attach() fails (e.g., no simulators available), the suppression flag is cleared, loading returns to false in finally, and the auto-attach effect re-fires — calling attach() again. This creates an unbounded retry loop.
Technical details
# Infinite auto-attach retry on failure
## Affected sites
- `src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts:115` — `suppressAutoAttachRef.current = false` at top of `attach()`
- `src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts:233-238` — auto-attach effect depends on `[attach, autoAttachOnMount, loading, session]` and gates on `suppressAutoAttachRef`
## Required outcome
- On attach failure, `suppressAutoAttachRef` must remain `true` (or be set to `true` in the catch block) so the effect does not re-invoke `attach()`
- Alternatively, remove line 115 entirely and only reset the flag on successful attach at line 157| return latest | ||
| } | ||
|
|
||
| private scanExistingStateFiles(): void { |
There was a problem hiding this comment.
latestBoundWorktree() returns the last worktree in ptyToWorktree insertion order — effectively the most recently bound PTY's worktree. The state-file watcher callback (line 211) and scanExistingStateFiles (line 239) both use this to assign detected serve-sim helpers to a worktree. If a user has two worktrees and starts serve-sim from worktree A's terminal, the detected event may be assigned to worktree B (if B's PTY was bound more recently), causing cross-worktree session leakage.
Technical details
# latestBoundWorktree is non-deterministic
## Affected sites
- `src/main/emulator/serve-sim-state-watcher.ts:230-236` — `latestBoundWorktree` returns insertion-order-last value
- `src/main/emulator/serve-sim-state-watcher.ts:211` — state-file watcher uses `latestBoundWorktree` for events from `watch()`
- `src/main/emulator/serve-sim-state-watcher.ts:239` — `scanExistingStateFiles` uses `latestBoundWorktree`
## Required outcome
- State files (`server-<UDID>.json`) don't inherently carry worktree info — the watcher needs a way to map serve-sim output to the correct worktree
- Options: (a) emit the event without a worktree and let the renderer resolve scoping, (b) enrich serve-sim state files with a worktree identifier (e.g., env var at `serve-sim --detach` time), or (c) track which worktree's PTY produced the JSON in `ingestPtyOutput`
## Open questions for the human
- Is the expectation that most users have only one worktree active at a time? If so, the heuristic might be good enough for now.| socket.destroy() | ||
| resolve(ready) | ||
| } | ||
| socket.setTimeout(CONNECT_TIMEOUT_MS, () => finish(false)) |
There was a problem hiding this comment.
socket.setTimeout(CONNECT_TIMEOUT_MS, ...) in Node.js fires on idle timeout (time since last I/O), not on connection timeout. During connect(), the socket is actively establishing the TCP handshake and is not "idle" — so this timer may not fire at all during connection attempts. If serve-sim's port is not open, the OS-level TCP connect timeout (often 30–75s on macOS) applies instead of the intended 500ms, delaying the readiness retry loop in waitForServeSimEndpointReady.
Technical details
# socket.setTimeout is idle-timeout, not connect-timeout
## Affected sites
- `src/main/emulator/serve-sim-endpoint-readiness.ts:41` — `socket.setTimeout(CONNECT_TIMEOUT_MS, () => finish(false))`
## Required outcome
- Use a separate `setTimeout` for the connection-establishment deadline that fires unconditionally after 500ms
- Alternatively, listen for `connect` first and then set the idle timeout, or use `socket.once('timeout', ...)` after `connect` fires for read/write timeout on established connections
## Suggested approach
```typescript
const deadline = setTimeout(() => finish(false), CONNECT_TIMEOUT_MS)
socket.once('connect', () => {
clearTimeout(deadline)
finish(true)
})
socket.once('error', () => {
clearTimeout(deadline)
finish(false)
})
```| usesElectronAsNode: boolean | ||
| } | ||
|
|
||
| function ensureMacOpenShim(): string | null { |
There was a problem hiding this comment.
ensureMacOpenShim writes a shell script to a world-predictable /tmp/orca-serve-sim-open-shim/open path and prepends that directory to PATH for serve-sim. On multi-user macOS, another user could pre-create this path with malicious content. The write is also non-atomic (writeFileSync directly to the final path, not write-then-rename). Since the shim directory shadows /usr/bin/open, serve-sim processes inherit a potentially attacker-controlled open binary.
Technical details
# TOCTOU race in ensureMacOpenShim
## Affected sites
- `src/main/emulator/serve-sim-execution.ts:37-41` — `mkdirSync` + `writeFileSync` to `/tmp/orca-serve-sim-open-shim/open`
- `src/main/emulator/serve-sim-execution.ts:56` — PATH prepend shadows `/usr/bin/open`
## Required outcome
- Use a non-shared directory (e.g., `app.getPath('sessionData')` or a `mkdtemp`-generated path) instead of the static `/tmp` path
- Write atomically: write to a temp file in the same directory, then `renameSync` into placeCo-authored-by: Orca <[email protected]>
9cd8355 to
5d7159c
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds serve-sim packaging and CLI surface; implements a main-process EmulatorBridge with serve-sim lifecycle and helper management; registers emulator RPCs and preload IPC; introduces renderer simulator tabs/pane, touch/gesture streaming and mapping; and updates workspace session persistence/hydration for simulator tabs. ChangesEmulator CLI and simulator tabs
|
There was a problem hiding this comment.
Important
Prior critical issues from the first review remain unaddressed — destroyAllSessions scope was fixed, but the other five anchored threads (including two criticals) and six body-level findings are unchanged. This re-review found no new issues.
Reviewed changes — Incremental review of force-pushed commit 5d7159c against prior review at 9cd8355. One prior thread resolved; the rest of the code is substantively unchanged with one minor catch-block guard added.
- Scoped
destroyAllSessionsto managed-only — addedsession.managedcheck before shutdown, preventing non-Orca simulators from being shut down on app quit - Added stale-attach error guard in
use-emulator-pane-session.ts— catch block returns early whenrequestedTargetmatchesliveTargetRef, suppressing spurious error UI during reconnection races
<!--
Pullfrog review metadata
- Mode: IncrementalReview (delta against prior pullfrog review)
- Files reviewed: 98
- Commits reviewed: 1
- Base: main (2b29acb)
- Head: Jinwoo-H/mobile-emulator (5d7159c)
- Reviewed commits:
- 5d7159c — Add mobile emulator
- Prior pullfrog review: 9cd8355 (#4754 (review))
- Submitted at: 2026-06-07T00:00:00Z
-->
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/src/store/slices/tabs.ts (1)
1644-1655:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle layout-key deletion when pruning removes all surviving groups.
On Line 1644,
layoutByWorktreeis only updated whennextLayoutis truthy. If pruning collapses to no layout (nextLayoutisundefined) while a previous layout exists, stale layout state is retained and later logic can treat the worktree as still group-owned.Suggested fix
- ...(nextLayout && layoutChanged - ? { - layoutByWorktree: { - ...current.layoutByWorktree, - // Why: a restored live runtime terminal needs a concrete leaf - // in the split-group model before activation runs again. - // Without this, the worktree still looks render-empty and the - // activation fallback spawns a duplicate "Terminal 2". - [worktreeId]: nextLayout! - } - } - : {}), + ...(layoutChanged + ? { + layoutByWorktree: (() => { + const nextLayoutByWorktree = { ...current.layoutByWorktree } + if (nextLayout) { + // Why: a restored live runtime terminal needs a concrete leaf + // in the split-group model before activation runs again. + // Without this, the worktree still looks render-empty and the + // activation fallback spawns a duplicate "Terminal 2". + nextLayoutByWorktree[worktreeId] = nextLayout + } else { + delete nextLayoutByWorktree[worktreeId] + } + return nextLayoutByWorktree + })() + } + : {}),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/store/slices/tabs.ts` around lines 1644 - 1655, The current update only writes layoutByWorktree when nextLayout is truthy, leaving stale entries when pruning removes all groups; modify the update logic around layoutByWorktree (the block using nextLayout, layoutChanged, worktreeId, current) so that if layoutChanged is true and nextLayout is undefined you remove the worktree's key from layoutByWorktree (i.e. produce a new layoutByWorktree object that omits current.layoutByWorktree[worktreeId]) instead of leaving the old entry, ensuring consumers won't treat the worktree as still group-owned.
♻️ Duplicate comments (5)
src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts (1)
115-115:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winInfinite retry loop on attach failure (still present).
This is the same issue from the previous review:
suppressAutoAttachRef.current = falseat line 115 unconditionally resets the suppression flag. Whenattach()fails (lines 159-171), the catch block does not setsuppressAutoAttachRef.current = true, so whenloadingreturns tofalsein the finally block (line 173), the auto-attach effect (lines 233-238) re-fires and callsattach()again, creating an infinite retry loop.Fix by moving line 115 to line 156 (after successful attach) or setting
suppressAutoAttachRef.current = truein the catch block.🔒 Proposed fix
Option 1: Only reset flag on success
async (deviceTarget?: string) => { if (loading) { return } - suppressAutoAttachRef.current = false setLoading(true) setError(null) if (tabId) { useAppStore.getState().setTabLabel(tabId, 'Starting…') } let requestedTarget: string | undefined try { // ... attach logic ... applySession(res?.info, attached, nextList) if (attached) { + suppressAutoAttachRef.current = false void refreshDevices(bootedTarget) }Option 2: Suppress retries on failure
} } catch (e: unknown) { + suppressAutoAttachRef.current = true if (requestedTarget && liveTargetRef.current === requestedTarget) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts` at line 115, The suppressAutoAttachRef.current flag is being unconditionally reset which causes infinite retry when attach() fails; update the attach flow in use-emulator-pane-session so suppression is only cleared after a successful attach (move the suppressAutoAttachRef.current = false to execute after attach() succeeds) or alternatively set suppressAutoAttachRef.current = true inside the attach() catch block before rethrowing/returning, and ensure the loading finally block does not unconditionally trigger the auto-attach effect; adjust references to suppressAutoAttachRef.current, attach(), and the attach() catch/finally sections accordingly.src/main/emulator/serve-sim-execution.ts (1)
32-47:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winTOCTOU race and world-writable path in
ensureMacOpenShim.This is a previously flagged security issue that remains unresolved.
ensureMacOpenShimwrites a shell script to a world-predictable/tmp/orca-serve-sim-open-shim/openpath and prepends that directory toPATHfor serve-sim. On multi-user macOS, another user could pre-create this path with malicious content. The write is also non-atomic (writeFileSyncdirectly to the final path, not write-then-rename). Since the shim directory shadows/usr/bin/open, serve-sim processes inherit a potentially attacker-controlledopenbinary.🔒 Recommended fix
Use a non-shared directory (e.g.,
app.getPath('sessionData')orapp.getPath('userData')) instead of the static/tmppath, and write atomically:-const MAC_OPEN_SHIM_DIR = join(tmpdir(), 'orca-serve-sim-open-shim') +const MAC_OPEN_SHIM_DIR = join(app.getPath('userData'), 'serve-sim-open-shim') const MAC_OPEN_SHIM_PATH = join(MAC_OPEN_SHIM_DIR, 'open') +const MAC_OPEN_SHIM_TEMP = join(MAC_OPEN_SHIM_DIR, 'open.tmp') function ensureMacOpenShim(): string | null { if (platform() !== 'darwin') { return null } try { mkdirSync(MAC_OPEN_SHIM_DIR, { recursive: true }) const current = existsSync(MAC_OPEN_SHIM_PATH) ? readFileSync(MAC_OPEN_SHIM_PATH, 'utf8') : '' if (current !== MAC_OPEN_SHIM) { - writeFileSync(MAC_OPEN_SHIM_PATH, MAC_OPEN_SHIM, { mode: 0o755 }) + writeFileSync(MAC_OPEN_SHIM_TEMP, MAC_OPEN_SHIM, { mode: 0o755 }) + renameSync(MAC_OPEN_SHIM_TEMP, MAC_OPEN_SHIM_PATH) } chmodSync(MAC_OPEN_SHIM_PATH, 0o755) return MAC_OPEN_SHIM_DIR🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/emulator/serve-sim-execution.ts` around lines 32 - 47, The ensureMacOpenShim function currently writes MAC_OPEN_SHIM to a predictable, shared MAC_OPEN_SHIM_DIR/MAC_OPEN_SHIM_PATH with non-atomic write and world-writable risk; change it to create the shim inside a per-user, non-shared directory (use Electron app.getPath('sessionData') or app.getPath('userData') instead of the static MAC_OPEN_SHIM_DIR), ensure the directory has restrictive permissions (owner-only), write the file atomically (write to a temp file in that same directory then rename to MAC_OPEN_SHIM_PATH), set explicit owner/mode (chmodSync to 0o755 after rename), and keep the function name ensureMacOpenShim and constants MAC_OPEN_SHIM_PATH/MAC_OPEN_SHIM for locating the change; also preserve the darwin guard and catch errors as before.src/main/emulator/serve-sim-endpoint-readiness.ts (1)
41-41:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
socket.setTimeoutis idle timeout, not connect timeout.This is a previously flagged issue that remains unresolved.
socket.setTimeout(CONNECT_TIMEOUT_MS, ...)in Node.js fires on idle timeout (time since last I/O), not on connection timeout. Duringconnect(), the socket is actively establishing the TCP handshake and is not "idle" — so this timer may not fire at all during connection attempts. If serve-sim's port is not open, the OS-level TCP connect timeout (often 30–75s on macOS) applies instead of the intended 500ms, delaying the readiness retry loop inwaitForServeSimEndpointReady.🐛 Proposed fix
Use a separate
setTimeoutfor the connection-establishment deadline:function canConnectToEndpoint(endpoint: TcpEndpoint): Promise<boolean> { return new Promise((resolve) => { const socket = connect({ host: endpoint.host, port: endpoint.port }) socket.unref() let settled = false const finish = (ready: boolean) => { if (settled) { return } settled = true + clearTimeout(deadline) socket.removeAllListeners() socket.destroy() resolve(ready) } - socket.setTimeout(CONNECT_TIMEOUT_MS, () => finish(false)) + const deadline = setTimeout(() => finish(false), CONNECT_TIMEOUT_MS) socket.once('connect', () => finish(true)) socket.once('error', () => finish(false)) }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/emulator/serve-sim-endpoint-readiness.ts` at line 41, The current use of socket.setTimeout(CONNECT_TIMEOUT_MS, ...) is wrong because it’s an idle timer; replace it with a separate connection-establishment deadline: create a plain setTimeout (e.g. connectTimer) that fires after CONNECT_TIMEOUT_MS to destroy the socket and call finish(false), and clear that timer on 'connect', 'error', and when finish runs to avoid leaks; update the code inside waitForServeSimEndpointReady/serve-sim-endpoint-readiness where socket and finish() are referenced to remove the socket.setTimeout call and wire the new connectTimer lifecycle to the existing socket event handlers and finish() cleanup.src/main/emulator/emulator-bridge.ts (2)
134-134:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winState corruption:
activeByWorktree.deletebefore managed-only guard.This is a previously flagged issue that remains unresolved. When
managedOnlyis true and the session is unmanaged (terminal-started), line 135 returnsnull— but line 134 has already deleted the worktree→session binding, which is unrecoverable. Subsequent CLI commands fail withemulator_no_activeeven though the serve-sim process still runs and thesessionsMap still holds the entry.🐛 Proposed fix
Move the delete to after the guard:
private async stopActiveForWorktreeInternal( worktreeId: string, options: { shutdownDevice?: boolean; managedOnly?: boolean } = {} ): Promise<string | null> { const key = this.activeByWorktree.get(worktreeId) if (!key) { return null } const session = this.sessions.get(key) - this.activeByWorktree.delete(worktreeId) if (!session || (options.managedOnly && !session.managed)) { return null } + this.activeByWorktree.delete(worktreeId) await this.stopServeSimForDevice(session.deviceUdid, {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/emulator/emulator-bridge.ts` at line 134, The deletion of the worktree→session mapping (this.activeByWorktree.delete(worktreeId)) is happening before the managed-only guard and can corrupt state when managedOnly is true; move the delete so it occurs after the check that returns null for unmanaged sessions (the guard that reads managedOnly and session.managed) so the mapping is only removed when you actually decide the session should be discarded; update the logic in the method that uses activeByWorktree, sessions, managedOnly and session.managed to perform the guard first and only call this.activeByWorktree.delete(worktreeId) after confirming the session is allowed to be removed.
334-348:⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoffUnconditional device shutdown on app quit may surprise users.
This is a previously flagged issue that remains unaddressed.
destroyAllSessions()unconditionally callsshutdownSimulatorDevice()for every managed session on app quit, which powers off the iOS Simulator itself — not just the streaming helper. A user with a booted simulator (started outside Orca or for other purposes) may find it shut down after closing the app.Consider separating "kill streaming helper" from "shut down simulator device" in the quit path, or making device shutdown opt-in via user preference.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/emulator/emulator-bridge.ts` around lines 334 - 348, The current destroyAllSessions() unconditionally calls shutdownSimulatorDevice() for each managed session; change it to only stop the streaming helper and make simulator shutdown opt-in: remove the unconditional shutdownSimulatorDevice(...) call from destroyAllSessions() so it only calls stopServeSimForDevice(session.deviceUdid, { helperPid: session.pid }) and handles errors, then either (a) add a new boolean parameter to destroyAllSessions(shutdownDevices = false) that triggers shutdownSimulatorDevice(...) when true, or (b) add a separate method (e.g., shutdownAllDevicesForManagedSessions()) that iterates sessions and calls shutdownSimulatorDevice(...) so callers can invoke it based on a user preference or startup-tracking flag (e.g., session.startedByOrca). Ensure sessions.clear() and activeByWorktree.clear() behavior remains unchanged and that stopServeSimForDevice and shutdownSimulatorDevice error handling is preserved.
🧹 Nitpick comments (6)
src/renderer/src/components/emulator-pane/use-emulator-pane-controls.ts (1)
29-37: 💤 Low valueRotation only cycles between two orientations.
sendRotatetoggles between'landscape_left'and'portrait', omitting'landscape_right'and'portrait_upside_down'. If full rotation support is needed, consider cycling through all four orientations.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/components/emulator-pane/use-emulator-pane-controls.ts` around lines 29 - 37, sendRotate currently toggles only between 'landscape_left' and 'portrait', skipping 'landscape_right' and 'portrait_upside_down'; update sendRotate (and the nextRotateOrientationRef initialization if present) to cycle through a full ordered list of orientations (e.g., ['portrait','landscape_left','portrait_upside_down','landscape_right']) instead of a binary toggle, pick the next index modulo length, and pass that orientation into callRuntimeRpc so all four orientations are supported.src/cli/handlers/emulator.ts (1)
34-75: 💤 Low valueClarify the dual input format for
--points.Lines 41-44 accept both
{ "points": [...] }and[...]as valid input. For a CLI flag, accepting two different JSON formats can be confusing. Consider supporting only the direct array format[...], or add a comment explaining why both are needed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/cli/handlers/emulator.ts` around lines 34 - 75, The parser parseEmulatorGesturePoints currently accepts two input shapes for --points (either a raw array or an object with a points property), which is confusing; pick one consistent format and enforce it (preferred: require the direct array form) by changing the parsing logic to expect an array at top-level and throw a RuntimeClientError if the parsed value is not an array, remove the branch that looks for parsed.points, and update the error message to reference --points array format; update any related doc/comment near parseEmulatorGesturePoints to explain the single accepted JSON shape.src/main/runtime/rpc/methods/emulator.ts (1)
5-5: ⚡ Quick winRemove redundant
.optional()when using.partial().In Zod 4, calling
.partial()on an object already makes all keys optional. The additional.optional()on the field is redundant:z.object({ worktree: z.string().optional() }).partial()produces{ worktree?: string | undefined }, where the.optional()adds no value.♻️ Simplify the schema definitions
-const WorktreeParam = z.object({ worktree: z.string().optional() }).partial() +const WorktreeParam = z.object({ worktree: z.string() }).partial()And at line 134:
- params: z.object({ worktree: z.string().optional() }).partial(), + params: z.object({ worktree: z.string() }).partial(),Also applies to: 134-134
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/runtime/rpc/methods/emulator.ts` at line 5, The WorktreeParam schema uses z.object({ worktree: z.string().optional() }).partial(), which redundantly marks the field optional twice; remove the inner .optional() so it becomes z.object({ worktree: z.string() }).partial() and do the same for any other schema in this file that wraps a z.string().optional() inside a .partial() (the similar definition referenced in the review), i.e., locate the WorktreeParam constant and the other schema and drop the inner .optional() calls.src/main/emulator/emulator-bridge.ts (2)
162-162: 💤 Low valueRemove pointless
voidstatement.Line 162 uses
void _opts?.worktreeIdto suppress an unused-parameter warning, butworktreeIdis actively used at lines 169-173. The void statement has no effect.♻️ Proposed fix
- void _opts?.worktreeId if (_opts?.device) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/emulator/emulator-bridge.ts` at line 162, Remove the pointless `void _opts?.worktreeId` statement: delete that line and leave the `_opts` parameter and later uses of `_opts.worktreeId` (lines where `worktreeId` is referenced) intact so the actual value is used; if you intended to silence an unused-parameter warning instead, rename the parameter or adjust linter settings rather than using the `void` expression.
1-354: ⚖️ Poor tradeoffConsider splitting this file to address max-lines.
Static analysis reports the file exceeds the line limit. As per coding guidelines, avoid adding a
max-linesdisable comment. Instead, consider extracting focused modules (e.g., session management, command routing, lifecycle/cleanup helpers) into separate files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/emulator/emulator-bridge.ts` around lines 1 - 354, File exceeds max-lines; split responsibilities out of EmulatorBridge by extracting session management (move sessions map and methods registerActiveEmulator, unregisterActiveEmulator, getActiveForWorktree, getReusableActiveForWorktree, stopActiveForWorktreeInternal, stopActiveForWorktree, stopActiveManagedForWorktree, shutdownActiveManagedForWorktree, destroyAllSessions, onAppQuit, and uses of activeByWorktree) into a SessionManager module/class, extract command routing/target resolution (getTargetOrThrow, ensureUdid, ensureDeviceBooted, tap, gesture, type, button, rotate, exec, execServeSim and parse/strip helpers) into a CommandRouter module that the bridge delegates to, and move lifecycle/cleanup helpers (stopServeSimForDevice, hasServeSimHelperForDevice, startHelperForDevice, kill, shutdown and helper-process calls like killServeSimHelperProcessesForDevice/listServeSimHelperProcessesForDevice) into a LifecycleManager module; keep EmulatorBridge as a thin coordinator that composes these modules in its constructor (inject ServeSimExecutable and waitForEndpointReady) and preserves the existing public method signatures so callers are unchanged.Source: Coding guidelines
src/renderer/src/components/tab-bar/group-tab-order.ts (1)
85-90: 💤 Low valueConsider adding a comment explaining the simulator id pattern.
The simulator branch uses
tab.idfor both the presence check and the resultid, differing from terminal/browser (which usetab.entityIdforid). While this is correct because simulators have a 1:1 relationship between tabs and entities (soentityId === id), a brief comment would clarify why this branch follows a different pattern.📝 Suggested clarifying comment
seenBrowsers.add(tab.entityId) result.push({ type: 'browser', id: tab.entityId, tabId: tab.id }) + // Simulator tabs have a 1:1 relationship with their entity, so we key by tab id. } else if (tab.contentType === 'simulator') { if (!simulatorTabIds.has(tab.id) || seenSimulators.has(tab.id)) { continue🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/components/tab-bar/group-tab-order.ts` around lines 85 - 90, Add a short inline comment in the simulator branch (around the checks for simulatorTabIds, seenSimulators and the result.push call) explaining that simulators use tab.id as the entity id (i.e., entityId === id) and therefore we intentionally use tab.id here instead of tab.entityId; reference the symbols simulatorTabIds, seenSimulators, and the result.push({ type: 'simulator', id: tab.id, tabId: tab.id }) so future readers understand the 1:1 relationship and why this branch differs from terminal/browser handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/packaged-runtime-node-modules.cjs`:
- Around line 106-112: The try/catch around readPackage currently swallows
errors for every dependency; change it so only errors for packageName ===
'serve-sim' are ignored: inside the catch block, if packageName === 'serve-sim'
return (skip), otherwise re-throw the caught error so corrupted/missing packages
surface; refer to the existing variables/functions packageName and readPackage
to locate the block and implement the conditional rethrow.
In `@src/cli/specs/emulator.ts`:
- Line 29: The allowedFlags array in the emulator spec contains a duplicated
'text' entry; edit the allowedFlags definition (the array assigned to
allowedFlags) and remove the redundant 'text' so each flag appears only once
(e.g., change allowedFlags: [...GLOBAL_FLAGS, 'text', 'device', 'emulator',
'worktree']).
In `@src/main/runtime/orca-runtime.ts`:
- Around line 12103-12104: The new serveSimStateWatcher.bindPty(ptyId,
worktreeId) calls are leaving watcher state when a PTY is pruned via
dropDisconnectedPtyRecord (the non-onPtyExit path); update the prune path in
dropDisconnectedPtyRecord to call the corresponding unbind method on
serveSimStateWatcher (e.g., serveSimStateWatcher.unbindPty or equivalent) with
the same ptyId/worktreeId, and add the same unbind call alongside any other
prune/refresh paths (the other bind location around the second occurrence) so
both bindPty sites have matching cleanup when PTYs are removed.
In `@src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts`:
- Around line 574-585: The newSimulatorTab command is reading the global
activeWorktreeId (useAppStore.getState().activeWorktreeId) which can target the
wrong worktree; change it to use the hook-scoped worktreeId available in this
model instead: in newSimulatorTab replace the wtId assignment with the local
worktreeId, keep the null/undefined guard and then call
ensureSimulatorTab(worktreeId, { placement: 'rightSplit', targetGroupId:
groupId, surfacePane: true }) so the simulator opens beside the correct panel;
reference symbols: newSimulatorTab, worktreeId, ensureSimulatorTab, groupId.
In `@src/shared/keybindings.ts`:
- Around line 423-430: The default shortcut for the action with id
'tab.newSimulator' is being registered cross-platform; change its
defaultBindings so the 'Mod+Shift+E' binding is only applied on macOS. Locate
the entry for id 'tab.newSimulator' in keybindings and modify the call to
platformBindings (or wrap the binding logic) so that it only returns
['Mod+Shift+E'] when the runtime platform is macOS (darwin); keep other
platforms returning no default binding. Ensure only the 'tab.newSimulator' entry
is changed and use the existing platformBindings helper or a small platform
check to enforce macOS-only assignment.
---
Outside diff comments:
In `@src/renderer/src/store/slices/tabs.ts`:
- Around line 1644-1655: The current update only writes layoutByWorktree when
nextLayout is truthy, leaving stale entries when pruning removes all groups;
modify the update logic around layoutByWorktree (the block using nextLayout,
layoutChanged, worktreeId, current) so that if layoutChanged is true and
nextLayout is undefined you remove the worktree's key from layoutByWorktree
(i.e. produce a new layoutByWorktree object that omits
current.layoutByWorktree[worktreeId]) instead of leaving the old entry, ensuring
consumers won't treat the worktree as still group-owned.
---
Duplicate comments:
In `@src/main/emulator/emulator-bridge.ts`:
- Line 134: The deletion of the worktree→session mapping
(this.activeByWorktree.delete(worktreeId)) is happening before the managed-only
guard and can corrupt state when managedOnly is true; move the delete so it
occurs after the check that returns null for unmanaged sessions (the guard that
reads managedOnly and session.managed) so the mapping is only removed when you
actually decide the session should be discarded; update the logic in the method
that uses activeByWorktree, sessions, managedOnly and session.managed to perform
the guard first and only call this.activeByWorktree.delete(worktreeId) after
confirming the session is allowed to be removed.
- Around line 334-348: The current destroyAllSessions() unconditionally calls
shutdownSimulatorDevice() for each managed session; change it to only stop the
streaming helper and make simulator shutdown opt-in: remove the unconditional
shutdownSimulatorDevice(...) call from destroyAllSessions() so it only calls
stopServeSimForDevice(session.deviceUdid, { helperPid: session.pid }) and
handles errors, then either (a) add a new boolean parameter to
destroyAllSessions(shutdownDevices = false) that triggers
shutdownSimulatorDevice(...) when true, or (b) add a separate method (e.g.,
shutdownAllDevicesForManagedSessions()) that iterates sessions and calls
shutdownSimulatorDevice(...) so callers can invoke it based on a user preference
or startup-tracking flag (e.g., session.startedByOrca). Ensure sessions.clear()
and activeByWorktree.clear() behavior remains unchanged and that
stopServeSimForDevice and shutdownSimulatorDevice error handling is preserved.
In `@src/main/emulator/serve-sim-endpoint-readiness.ts`:
- Line 41: The current use of socket.setTimeout(CONNECT_TIMEOUT_MS, ...) is
wrong because it’s an idle timer; replace it with a separate
connection-establishment deadline: create a plain setTimeout (e.g. connectTimer)
that fires after CONNECT_TIMEOUT_MS to destroy the socket and call
finish(false), and clear that timer on 'connect', 'error', and when finish runs
to avoid leaks; update the code inside
waitForServeSimEndpointReady/serve-sim-endpoint-readiness where socket and
finish() are referenced to remove the socket.setTimeout call and wire the new
connectTimer lifecycle to the existing socket event handlers and finish()
cleanup.
In `@src/main/emulator/serve-sim-execution.ts`:
- Around line 32-47: The ensureMacOpenShim function currently writes
MAC_OPEN_SHIM to a predictable, shared MAC_OPEN_SHIM_DIR/MAC_OPEN_SHIM_PATH with
non-atomic write and world-writable risk; change it to create the shim inside a
per-user, non-shared directory (use Electron app.getPath('sessionData') or
app.getPath('userData') instead of the static MAC_OPEN_SHIM_DIR), ensure the
directory has restrictive permissions (owner-only), write the file atomically
(write to a temp file in that same directory then rename to MAC_OPEN_SHIM_PATH),
set explicit owner/mode (chmodSync to 0o755 after rename), and keep the function
name ensureMacOpenShim and constants MAC_OPEN_SHIM_PATH/MAC_OPEN_SHIM for
locating the change; also preserve the darwin guard and catch errors as before.
In `@src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts`:
- Line 115: The suppressAutoAttachRef.current flag is being unconditionally
reset which causes infinite retry when attach() fails; update the attach flow in
use-emulator-pane-session so suppression is only cleared after a successful
attach (move the suppressAutoAttachRef.current = false to execute after attach()
succeeds) or alternatively set suppressAutoAttachRef.current = true inside the
attach() catch block before rethrowing/returning, and ensure the loading finally
block does not unconditionally trigger the auto-attach effect; adjust references
to suppressAutoAttachRef.current, attach(), and the attach() catch/finally
sections accordingly.
---
Nitpick comments:
In `@src/cli/handlers/emulator.ts`:
- Around line 34-75: The parser parseEmulatorGesturePoints currently accepts two
input shapes for --points (either a raw array or an object with a points
property), which is confusing; pick one consistent format and enforce it
(preferred: require the direct array form) by changing the parsing logic to
expect an array at top-level and throw a RuntimeClientError if the parsed value
is not an array, remove the branch that looks for parsed.points, and update the
error message to reference --points array format; update any related doc/comment
near parseEmulatorGesturePoints to explain the single accepted JSON shape.
In `@src/main/emulator/emulator-bridge.ts`:
- Line 162: Remove the pointless `void _opts?.worktreeId` statement: delete that
line and leave the `_opts` parameter and later uses of `_opts.worktreeId` (lines
where `worktreeId` is referenced) intact so the actual value is used; if you
intended to silence an unused-parameter warning instead, rename the parameter or
adjust linter settings rather than using the `void` expression.
- Around line 1-354: File exceeds max-lines; split responsibilities out of
EmulatorBridge by extracting session management (move sessions map and methods
registerActiveEmulator, unregisterActiveEmulator, getActiveForWorktree,
getReusableActiveForWorktree, stopActiveForWorktreeInternal,
stopActiveForWorktree, stopActiveManagedForWorktree,
shutdownActiveManagedForWorktree, destroyAllSessions, onAppQuit, and uses of
activeByWorktree) into a SessionManager module/class, extract command
routing/target resolution (getTargetOrThrow, ensureUdid, ensureDeviceBooted,
tap, gesture, type, button, rotate, exec, execServeSim and parse/strip helpers)
into a CommandRouter module that the bridge delegates to, and move
lifecycle/cleanup helpers (stopServeSimForDevice, hasServeSimHelperForDevice,
startHelperForDevice, kill, shutdown and helper-process calls like
killServeSimHelperProcessesForDevice/listServeSimHelperProcessesForDevice) into
a LifecycleManager module; keep EmulatorBridge as a thin coordinator that
composes these modules in its constructor (inject ServeSimExecutable and
waitForEndpointReady) and preserves the existing public method signatures so
callers are unchanged.
In `@src/main/runtime/rpc/methods/emulator.ts`:
- Line 5: The WorktreeParam schema uses z.object({ worktree:
z.string().optional() }).partial(), which redundantly marks the field optional
twice; remove the inner .optional() so it becomes z.object({ worktree:
z.string() }).partial() and do the same for any other schema in this file that
wraps a z.string().optional() inside a .partial() (the similar definition
referenced in the review), i.e., locate the WorktreeParam constant and the other
schema and drop the inner .optional() calls.
In `@src/renderer/src/components/emulator-pane/use-emulator-pane-controls.ts`:
- Around line 29-37: sendRotate currently toggles only between 'landscape_left'
and 'portrait', skipping 'landscape_right' and 'portrait_upside_down'; update
sendRotate (and the nextRotateOrientationRef initialization if present) to cycle
through a full ordered list of orientations (e.g.,
['portrait','landscape_left','portrait_upside_down','landscape_right']) instead
of a binary toggle, pick the next index modulo length, and pass that orientation
into callRuntimeRpc so all four orientations are supported.
In `@src/renderer/src/components/tab-bar/group-tab-order.ts`:
- Around line 85-90: Add a short inline comment in the simulator branch (around
the checks for simulatorTabIds, seenSimulators and the result.push call)
explaining that simulators use tab.id as the entity id (i.e., entityId === id)
and therefore we intentionally use tab.id here instead of tab.entityId;
reference the symbols simulatorTabIds, seenSimulators, and the result.push({
type: 'simulator', id: tab.id, tabId: tab.id }) so future readers understand the
1:1 relationship and why this branch differs from terminal/browser handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5f7fcdf0-02d5-4c4e-a23b-ddb460d3bcda
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (97)
.npmrcconfig/electron-builder.config.cjsconfig/packaged-runtime-node-modules.cjspackage.jsonskills/orca-cli/SKILL.mdskills/orca-emulator/SKILL.mdsrc/cli/args.tssrc/cli/dispatch.tssrc/cli/handlers/emulator.tssrc/cli/help.tssrc/cli/index.test.tssrc/cli/selectors.tssrc/cli/specs/emulator.tssrc/cli/specs/index.tssrc/main/browser/browser-guest-ui.tssrc/main/emulator/emulator-bridge-types.tssrc/main/emulator/emulator-bridge.test.tssrc/main/emulator/emulator-bridge.tssrc/main/emulator/emulator-errors.tssrc/main/emulator/emulator-gesture-sender.test.tssrc/main/emulator/emulator-gesture-sender.tssrc/main/emulator/emulator-types.tssrc/main/emulator/serve-sim-detached-session.tssrc/main/emulator/serve-sim-endpoint-readiness.tssrc/main/emulator/serve-sim-execution.tssrc/main/emulator/serve-sim-helper-processes.test.tssrc/main/emulator/serve-sim-helper-processes.tssrc/main/emulator/serve-sim-state-watcher.test.tssrc/main/emulator/serve-sim-state-watcher.tssrc/main/emulator/simctl-simulator-devices.tssrc/main/index.tssrc/main/runtime/orca-runtime-emulator.tssrc/main/runtime/orca-runtime.tssrc/main/runtime/rpc/dispatcher.tssrc/main/runtime/rpc/errors.tssrc/main/runtime/rpc/methods/emulator.tssrc/main/runtime/rpc/methods/index.tssrc/preload/api-types.tssrc/preload/index.tssrc/renderer/src/components/Terminal.tsxsrc/renderer/src/components/WorktreeJumpPalette.tsxsrc/renderer/src/components/emulator-pane/EmulatorPane.tsxsrc/renderer/src/components/emulator-pane/EmulatorPaneOverlayLayer.tsxsrc/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.tssrc/renderer/src/components/emulator-pane/emulator-device-frame-layout.tssrc/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsxsrc/renderer/src/components/emulator-pane/emulator-device-frame.tsxsrc/renderer/src/components/emulator-pane/emulator-device-state.test.tssrc/renderer/src/components/emulator-pane/emulator-device-state.tssrc/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsxsrc/renderer/src/components/emulator-pane/emulator-pane-types.tssrc/renderer/src/components/emulator-pane/emulator-phone-hardware-buttons.tsxsrc/renderer/src/components/emulator-pane/emulator-screen-gesture.test.tssrc/renderer/src/components/emulator-pane/emulator-screen-gesture.tssrc/renderer/src/components/emulator-pane/emulator-screen-stream-content.tsxsrc/renderer/src/components/emulator-pane/emulator-unavailable-pane.tsxsrc/renderer/src/components/emulator-pane/use-emulator-pane-controls.tssrc/renderer/src/components/emulator-pane/use-emulator-pane-session.tssrc/renderer/src/components/emulator-pane/use-emulator-touch-stream.tssrc/renderer/src/components/tab-bar/TabBar.context-menu.test.tssrc/renderer/src/components/tab-bar/TabBar.tsxsrc/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.tssrc/renderer/src/components/tab-bar/group-tab-order.test.tssrc/renderer/src/components/tab-bar/group-tab-order.tssrc/renderer/src/components/tab-bar/reconcile-order.tssrc/renderer/src/components/tab-group/TabGroupPanel.tsxsrc/renderer/src/components/tab-group/useTabDragSplit.tssrc/renderer/src/components/tab-group/useTabGroupWorkspaceModel.tssrc/renderer/src/components/terminal/tab-type-cycle.tssrc/renderer/src/hooks/ipc-tab-switch.test.tssrc/renderer/src/hooks/ipc-tab-switch.tssrc/renderer/src/hooks/resolve-zoom-target.tssrc/renderer/src/hooks/useIpcEvents.tssrc/renderer/src/lib/ensure-simulator-tab-behavior.test.tssrc/renderer/src/lib/ensure-simulator-tab.test.tssrc/renderer/src/lib/ensure-simulator-tab.tssrc/renderer/src/lib/file-type-icons.tssrc/renderer/src/lib/simulator-tab-shutdown.tssrc/renderer/src/lib/tab-number-shortcuts.tssrc/renderer/src/lib/workspace-session-patch.test.tssrc/renderer/src/lib/workspace-session-patch.tssrc/renderer/src/lib/workspace-session-unified-tabs.tssrc/renderer/src/lib/workspace-session.test.tssrc/renderer/src/lib/workspace-session.tssrc/renderer/src/store/slices/store-cascades.test.tssrc/renderer/src/store/slices/tabs-hydration.test.tssrc/renderer/src/store/slices/tabs-hydration.tssrc/renderer/src/store/slices/tabs.test.tssrc/renderer/src/store/slices/tabs.tssrc/renderer/src/store/slices/worktrees.tssrc/renderer/src/web/web-preload-api.tssrc/shared/emulator-touch-frame.test.tssrc/shared/emulator-touch-frame.tssrc/shared/keybindings.tssrc/shared/runtime-types.tssrc/shared/types.tssrc/shared/workspace-session-schema.ts
Co-authored-by: Orca <[email protected]>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Extracts EmulatorSessionRegistry from the bridge, pulls emulator RPC commands into RuntimeEmulatorCommands, and applies minor fixes. Prior findings from the initial review remain open.
- Extracted
EmulatorSessionRegistry— session and worktree maps moved into a dedicated class inemulator-session-registry.ts, withtoSessionInfofor Info↔State conversion - Extracted
RuntimeEmulatorCommands— emulator RPC methods now live in a separate command class inorca-runtime-emulator.ts, mirroring the browser command pattern - Plugged
serveSimStateWatcherPTY leak —dropDisconnectedPtyRecordinorca-runtime.tsnow callsserveSimStateWatcher.unbindPty(ptyId)when a PTY is pruned without normal exit - Fixed
newSimulatorTabworktree scoping — action handler inorca-runtime.tsuses the hook-scopedworktreeIdinstead ofuseAppStore.getState().activeWorktreeId, preventing wrong-worktree tab creation during multi-worktree use - Disabled emulator shortcut on Linux/Windows —
newSimulatorTabkeybinding now uses platform-specific defaults:darwin: ['Mod+Shift+E'],linux: [],win32: [] - Narrowed
inspect-webkitomission in packaging —isKnownOmittedServeSimDependencyverifies the caller is serve-sim's ownnode_modulesbefore swallowing a missing closure dependency - Removed duplicate
textflag fromemulator typeCLI specallowedFlags
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/emulator/emulator-session-registry.ts`:
- Around line 8-24: registerActive currently keys the sessions Map by
info.deviceUdid which allows multiple worktrees to clobber the same session
entry; change the registry to enforce per-worktree ownership to avoid
cross-worktree overwrites by either (a) using a composite key (e.g., worktreeId
+ deviceUdid) or (b) storing a per-worktree map value (e.g., sessions:
Map<deviceUdid, Map<worktreeId, SessionInfo>>) so registerActive only sets the
session for the given worktree and stopActiveManagedForWorktree /
stopActiveForWorktreeInternal / clearSessionAndWorktrees operate on the
per-worktree entry; ensure registerActive checks for an existing session for
that worktree before mutating shared fields like managed and pid and update
clearSessionAndWorktrees to only remove mappings for the specific worktree (or
remove the device entry only when no worktrees remain).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 63c51198-8ff7-45a2-85e4-de57cd0b1823
📒 Files selected for processing (7)
config/packaged-runtime-node-modules.cjssrc/cli/specs/emulator.tssrc/main/emulator/emulator-bridge.tssrc/main/emulator/emulator-session-registry.tssrc/main/runtime/orca-runtime.tssrc/renderer/src/components/tab-group/useTabGroupWorkspaceModel.tssrc/shared/keybindings.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/shared/keybindings.ts
- src/cli/specs/emulator.ts
- src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
- config/packaged-runtime-node-modules.cjs
- src/main/emulator/emulator-bridge.ts
- src/main/runtime/orca-runtime.ts
3a1ad78 to
b68909d
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Adds settings support to the mobile emulator: enable/disable toggle, default device selection, an emulator.availability RPC method with a platform-aware health check, and guards across the shortcut system so the feature can be turned off.
- Added
emulator-availabilitymodule —inspectEmulatorAvailability()with platform guard andpickDefaultSimulatorDevice()using a priority chain (booted iPhone → booted any → available iPhone → available any → any → null) - Added
MobileEmulatorSettingsPane— enable/disable toggle, availability status badge with refresh, and default device Select with an "Automatic" sentinel value - Added
emulator.availabilityRPC method — bridges the availability check to the renderer settings pane viacallRuntimeRpc - Made
emulator.attachdevice optional — falls back tomobileEmulatorDefaultDeviceUdidfrom settings, then auto-selects an available iPhone viapickDefaultSimulatorDevice, then throwsemulator_device_not_found - Added
emulator_disablederror —emulatorAttachthrows this whenmobileEmulatorEnabled === falsein settings - Guarded browser shortcut forwarding with
mobileEmulatorEnabled—Cmd+Shift+Eis only forwarded to the renderer when the feature is enabled - Guarded tab bar and workspace model with
mobileEmulatorEnabled— hides "New Mobile Emulator" menu item and makesnewSimulatorTabactionundefinedwhen disabled - Guarded
ensureSimulatorTabwithmobileEmulatorEnabled— returnsnullwhen feature is off, preventing tab creation via keyboard or IPC - Extracted
useEmulatorPaneSessionEventshook — moves auto-attach and shutdownwindow.addEventListenerlogic out ofuseEmulatorPaneSessioninto a dedicated hook - Added default settings —
mobileEmulatorEnabled: trueandmobileEmulatorDefaultDeviceUdid: nullingetDefaultSettings
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/src/components/Terminal.tsx (1)
1322-1330:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd an explicit macOS runtime guard to the simulator shortcut branch.
Line 1323 handles
tab.newSimulatorwithout checking platform. That can enable simulator-tab creation on non-macOS via keybinding overrides, while the main-process forwarding path is explicitly darwin-gated.Suggested patch
- if (!e.repeat && mobileEmulatorEnabled && matchShortcut('tab.newSimulator')) { + if (!e.repeat && isMac && mobileEmulatorEnabled && matchShortcut('tab.newSimulator')) { e.preventDefault() notifyTerminalCapture('tab.newSimulator') if (!floatingWorkspaceFocused) { handleNewSimulatorTab() } return }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/components/Terminal.tsx` around lines 1322 - 1330, The simulator shortcut branch lacks a macOS runtime guard; update the condition that checks !e.repeat && mobileEmulatorEnabled && matchShortcut('tab.newSimulator') to also require a darwin platform check (e.g. process.platform === 'darwin' or an existing isDarwin helper) so simulator tabs can only be created on macOS. Modify the same block that calls notifyTerminalCapture('tab.newSimulator') and handleNewSimulatorTab() (and respects floatingWorkspaceFocused) to only run when the runtime is macOS, matching the main-process forwarding guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/renderer/src/components/Terminal.tsx`:
- Around line 1322-1330: The simulator shortcut branch lacks a macOS runtime
guard; update the condition that checks !e.repeat && mobileEmulatorEnabled &&
matchShortcut('tab.newSimulator') to also require a darwin platform check (e.g.
process.platform === 'darwin' or an existing isDarwin helper) so simulator tabs
can only be created on macOS. Modify the same block that calls
notifyTerminalCapture('tab.newSimulator') and handleNewSimulatorTab() (and
respects floatingWorkspaceFocused) to only run when the runtime is macOS,
matching the main-process forwarding guard.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1c6f324f-7f66-4d5d-aa9b-57aef19c2e0a
📒 Files selected for processing (25)
src/cli/handlers/emulator.tssrc/cli/specs/emulator.tssrc/main/browser/browser-guest-ui.tssrc/main/browser/browser-manager.tssrc/main/emulator/emulator-availability.tssrc/main/emulator/emulator-bridge.test.tssrc/main/emulator/emulator-bridge.tssrc/main/emulator/emulator-errors.tssrc/main/runtime/orca-runtime-emulator.tssrc/main/runtime/orca-runtime.tssrc/main/runtime/rpc/methods/emulator.tssrc/renderer/src/components/Terminal.tsxsrc/renderer/src/components/emulator-pane/use-emulator-pane-session-events.tssrc/renderer/src/components/emulator-pane/use-emulator-pane-session.tssrc/renderer/src/components/settings/MobileEmulatorSettingsPane.tsxsrc/renderer/src/components/settings/Settings.tsxsrc/renderer/src/components/settings/mobile-emulator-search.tssrc/renderer/src/components/tab-bar/TabBar.tsxsrc/renderer/src/components/tab-group/useTabGroupWorkspaceModel.tssrc/renderer/src/hooks/useSettingsNavigationMetadata.tssrc/renderer/src/lib/ensure-simulator-tab-behavior.test.tssrc/renderer/src/lib/ensure-simulator-tab.tssrc/renderer/src/lib/settings-navigation-types.tssrc/shared/constants.tssrc/shared/types.ts
✅ Files skipped from review due to trivial changes (1)
- src/shared/constants.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- src/cli/specs/emulator.ts
- src/main/emulator/emulator-errors.ts
- src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts
- src/main/runtime/rpc/methods/emulator.ts
- src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
- src/renderer/src/lib/ensure-simulator-tab.ts
- src/main/runtime/orca-runtime-emulator.ts
- src/cli/handlers/emulator.ts
- src/main/emulator/emulator-bridge.ts
- src/main/runtime/orca-runtime.ts
- src/renderer/src/components/tab-bar/TabBar.tsx
d9cb302 to
2b9c295
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Incremental review of the settings commit (d9cb302d) against the prior review at afce1b24f. Adds an enable/disable toggle, availability diagnostics, and a default device selector for the mobile emulator, gated behind macOS-only UI and runtime guards.
- Added
emulator-availabilitymodule —inspectEmulatorAvailability()with platform guard, simctl/serve-sim health checks, andpickDefaultSimulatorDevice()priority chain (booted iPhone → booted any → available iPhone → available any → any) - Added
MobileEmulatorSettingsPane— enable/disable toggle, availability status badge with refresh, and default deviceSelectwith an "Automatic" sentinel value - Added
emulator.availabilityRPC method — bridges the availability check to the renderer settings pane viacallRuntimeRpc - Added
emulator_disablederror —emulatorAttachthrows this whenmobileEmulatorEnabled === falsein settings - Added
mapEmulatorErrorto RPC dispatcher — forwards structuredEmulatorError.codestrings (parallel tomapBrowserError) - Guarded browser shortcut forwarding —
Cmd+Shift+Einbrowser-guest-ui.tsnow checksprocess.platform === 'darwin'and readsmobileEmulatorEnabledfrom settings before forwarding to the renderer - Guarded tab bar and workspace model — "New Mobile Emulator" menu item hidden and
newSimulatorTabactionundefinedwhenmobileEmulatorEnabledisfalse - Guarded
ensureSimulatorTab— returnsnullwhenmobileEmulatorEnabled === falseor platform is not macOS - Extracted
useEmulatorPaneSessionEventshook — moves auto-attach and shutdownwindow.addEventListenerlogic out ofuseEmulatorPaneSession - Extended settings schema —
mobileEmulatorEnabled(defaulttrue) andmobileEmulatorDefaultDeviceUdid(defaultnull) inGlobalSettingsandgetDefaultSettings - Extended tab type system —
'simulator'added toTabContentType,WorkspaceVisibleTabType, and workspace session zod schema - Wired
serveSimStateWatcherlifecycle hooks —bindPty/unbindPty/forgetWorktree/ingestPtyOutputwoven intoOrcaRuntimeServicealongside the existingadvertisedUrlWatcherhooks - Added
checkServeSimAvailabletoEmulatorBridge— runsserve-sim --helpwith a 10s timeout
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
Co-authored-by: Orca <[email protected]>
2b9c295 to
2b696b4
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Incremental review of the settings commit (2b696b483) against prior review at d9cb302d. Adds an enable/disable toggle, availability diagnostics, a default device selector and settings-schema wiring for the mobile emulator, gated behind platform-aware and settings-aware guards across the UI and shortcut system.
- Added
emulator-availabilitymodule —inspectEmulatorAvailability()with platform guard, simctl/serve-sim health checks, andpickDefaultSimulatorDevice()priority chain (booted iPhone → booted any → available iPhone → available any → any → null) - Added
MobileEmulatorSettingsPane— enable/disable toggle, availability status badge with refresh, and a default deviceSelectwith an "Auto-select device" sentinel value - Added
emulator.availabilityRPC method — bridges the availability check to the renderer settings pane - Added
emulator_disablederror —emulatorAttachthrows this whenmobileEmulatorEnabled === false - Made
emulator.attachdevice optional in RPC params and CLI spec, with fallback chain: explicit device → settings default →pickDefaultSimulatorDevice→emulator_device_not_founderror - Guarded emulator shortcuts and UI —
Cmd+Shift+Eforwarding in browser guest readsmobileEmulatorEnabled; Terminal shortcut handler, TabBar "New Mobile Emulator" menu item, workspace modelnewSimulatorTabaction, andensureSimulatorTaball return early or hide when feature is disabled - Extracted
useEmulatorPaneSessionEvents— auto-attach and shutdownCustomEventlisteners into a dedicated hook called fromuseEmulatorPaneSession - Wired
configuredDefaultUdidinto session hook —selectedUdidinitializes from and syncs to the settings default, and theattachfallback chain includes the configured value
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/src/components/Terminal.tsx (1)
1323-1328:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winOnly consume the shortcut when a simulator tab can actually open.
This currently calls
preventDefault()before the floating-workspace check, soCmd/Ctrl+Shift+Ebecomes a no-op there. It also leaves the macOS-only restriction implicit even though the menu path already guards it explicitly.Suggested change
- if (!e.repeat && mobileEmulatorEnabled && matchShortcut('tab.newSimulator')) { + if ( + !e.repeat && + isMac && + mobileEmulatorEnabled && + !floatingWorkspaceFocused && + matchShortcut('tab.newSimulator') + ) { e.preventDefault() notifyTerminalCapture('tab.newSimulator') - if (!floatingWorkspaceFocused) { - handleNewSimulatorTab() - } + handleNewSimulatorTab() return }As per coding guidelines, "Keep all platform-dependent behavior behind runtime checks."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/components/Terminal.tsx` around lines 1323 - 1328, Only call e.preventDefault(), notifyTerminalCapture('tab.newSimulator') and handleNewSimulatorTab() only when a simulator tab can actually open: wrap those calls in the same conditional that checks mobileEmulatorEnabled and !floatingWorkspaceFocused (and an explicit macOS runtime guard if the feature is macOS-only, e.g. isMac() or process.platform==='darwin'). Move the e.preventDefault() out of the top-level shortcut handler and into the block that also calls notifyTerminalCapture and handleNewSimulatorTab so the shortcut isn’t consumed when floatingWorkspaceFocused is true or the platform/feature flag disallows opening the simulator; keep matchShortcut('tab.newSimulator') as the outer detector.Source: Coding guidelines
🧹 Nitpick comments (1)
src/renderer/src/components/Terminal.tsx (1)
213-213: ⚡ Quick winDocument the opt-out setting semantics.
!== falseis carrying a non-obvious contract here: older persisted settings default to enabled. A shortWhy:comment would make this much harder to "simplify" into a truthy check later.Suggested change
- const mobileEmulatorEnabled = useAppStore((s) => s.settings?.mobileEmulatorEnabled !== false) + // Why: this setting is opt-out so older sessions without a stored value stay enabled. + const mobileEmulatorEnabled = useAppStore((s) => s.settings?.mobileEmulatorEnabled !== false)As per coding guidelines, "When writing or modifying code driven by a design doc or non-obvious constraint, add a comment explaining why the code behaves the way it does."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/components/Terminal.tsx` at line 213, The expression computing mobileEmulatorEnabled in Terminal.tsx uses useAppStore((s) => s.settings?.mobileEmulatorEnabled !== false) and relies on an opt-out semantics (undefined/old persisted values should default to enabled), so add a short inline comment above that line explaining this non-obvious contract and why !== false is used instead of a simple truthy check — reference mobileEmulatorEnabled, useAppStore and settings?.mobileEmulatorEnabled in the comment so future maintainers don't simplify it incorrectly.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/renderer/src/components/Terminal.tsx`:
- Around line 1323-1328: Only call e.preventDefault(),
notifyTerminalCapture('tab.newSimulator') and handleNewSimulatorTab() only when
a simulator tab can actually open: wrap those calls in the same conditional that
checks mobileEmulatorEnabled and !floatingWorkspaceFocused (and an explicit
macOS runtime guard if the feature is macOS-only, e.g. isMac() or
process.platform==='darwin'). Move the e.preventDefault() out of the top-level
shortcut handler and into the block that also calls notifyTerminalCapture and
handleNewSimulatorTab so the shortcut isn’t consumed when
floatingWorkspaceFocused is true or the platform/feature flag disallows opening
the simulator; keep matchShortcut('tab.newSimulator') as the outer detector.
---
Nitpick comments:
In `@src/renderer/src/components/Terminal.tsx`:
- Line 213: The expression computing mobileEmulatorEnabled in Terminal.tsx uses
useAppStore((s) => s.settings?.mobileEmulatorEnabled !== false) and relies on an
opt-out semantics (undefined/old persisted values should default to enabled), so
add a short inline comment above that line explaining this non-obvious contract
and why !== false is used instead of a simple truthy check — reference
mobileEmulatorEnabled, useAppStore and settings?.mobileEmulatorEnabled in the
comment so future maintainers don't simplify it incorrectly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c7306e62-76a1-4914-97e7-9ee1b4965d80
📒 Files selected for processing (25)
src/cli/handlers/emulator.tssrc/cli/specs/emulator.tssrc/main/browser/browser-guest-ui.tssrc/main/browser/browser-manager.tssrc/main/emulator/emulator-availability.tssrc/main/emulator/emulator-bridge.test.tssrc/main/emulator/emulator-bridge.tssrc/main/emulator/emulator-errors.tssrc/main/runtime/orca-runtime-emulator.tssrc/main/runtime/orca-runtime.tssrc/main/runtime/rpc/methods/emulator.tssrc/renderer/src/components/Terminal.tsxsrc/renderer/src/components/emulator-pane/use-emulator-pane-session-events.tssrc/renderer/src/components/emulator-pane/use-emulator-pane-session.tssrc/renderer/src/components/settings/MobileEmulatorSettingsPane.tsxsrc/renderer/src/components/settings/Settings.tsxsrc/renderer/src/components/settings/mobile-emulator-search.tssrc/renderer/src/components/tab-bar/TabBar.tsxsrc/renderer/src/components/tab-group/useTabGroupWorkspaceModel.tssrc/renderer/src/hooks/useSettingsNavigationMetadata.tssrc/renderer/src/lib/ensure-simulator-tab-behavior.test.tssrc/renderer/src/lib/ensure-simulator-tab.tssrc/renderer/src/lib/settings-navigation-types.tssrc/shared/constants.tssrc/shared/types.ts
✅ Files skipped from review due to trivial changes (3)
- src/shared/constants.ts
- src/renderer/src/lib/settings-navigation-types.ts
- src/cli/specs/emulator.ts
🚧 Files skipped from review as they are similar to previous changes (21)
- src/renderer/src/components/settings/Settings.tsx
- src/main/emulator/emulator-errors.ts
- src/renderer/src/components/settings/mobile-emulator-search.ts
- src/renderer/src/hooks/useSettingsNavigationMetadata.ts
- src/main/browser/browser-guest-ui.ts
- src/main/emulator/emulator-availability.ts
- src/cli/handlers/emulator.ts
- src/shared/types.ts
- src/main/browser/browser-manager.ts
- src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts
- src/main/emulator/emulator-bridge.test.ts
- src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
- src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx
- src/main/runtime/rpc/methods/emulator.ts
- src/renderer/src/lib/ensure-simulator-tab.ts
- src/renderer/src/components/emulator-pane/use-emulator-pane-session-events.ts
- src/main/emulator/emulator-bridge.ts
- src/main/runtime/orca-runtime-emulator.ts
- src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts
- src/renderer/src/components/tab-bar/TabBar.tsx
- src/main/runtime/orca-runtime.ts
Co-authored-by: Orca <[email protected]>
…emulator # Conflicts: # src/main/runtime/rpc/dispatcher.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Incremental review of 3d73d5b against prior review at 2b696b4. Stabilizes the mobile emulator lifecycle with main-process-owned MJPEG streaming, atomic split-tab creation, prelaunched session bridging, and graceful shutdown scheduling.
- Moved MJPEG streaming to the main process — new
MjpegFrameStreamclass withextractJpegFramesparser owns the raw HTTP connection to serve-sim; the renderer receives individual JPEG frames via IPC (emulatorFrameStreamStart/Frame/Error/Stop), avoiding Chromium NetworkService restarts under long-lived MJPEG loads - Added
createUnifiedTabInSplitstore method — creates a simulator tab directly in a new right-split group in a single atomic operation, replacing the old create-then-drop pattern that could persist midpoint state during HMR/reload - Added
openMobileEmulatorTabwith "surface first, attach second" flow — surfaces the tab synchronously before the async emulator attach completes, usessimulator-launch-coordination(prelaunched session + manual-launch-pending flags) to bridge the gap for auto-attach and manual-launch event consumers - Refactored
useEmulatorPaneSession— extracteduseEmulatorPaneLifecycle(mount/unmount shutdown scheduling),useEmulatorPaneManualLaunchEvents(CustomEvent listeners for launch-started/launch-failed),emulator-pane-session-view(computed view layer),emulator-attach-target(priority-ordered target resolution), andemulator-prelaunched-session(session state from prelaunched info) - Added graceful shutdown scheduler —
scheduleSimulatorPaneManagedShutdownwith a 1.5s grace period checks that no simulator tabs remain before shutting down, cancels on remount (cancelPendingSimulatorPaneShutdown), and separates the check from the action via injectable deps - Added
hideNativeSimulatorApp— osascript call suppresses the native Simulator.app window after attach, so Orca's embedded stream is the sole view - Broadened macOS open shim —
ensureMacOpenShimnow intercepts anySimulator.app/com.apple.iphonesimulatorvariant, falling through to real/usr/bin/openfor non-simulator targets - Updated
serve-sim-detached-session— derives MJPEG endpoint from older serve-simurlfield viastreamUrlFromServeSimUrlwhenstreamUrlis absent
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
Co-authored-by: Orca <[email protected]>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Prevents duplicate mobile emulator tab creation by adding a guard in openMobileEmulatorTab and disabling the "New Mobile Emulator" context menu item when a workspace already has one.
- Added
getSimulatorTabForWorktreehelper — extracted fromensure-simulator-tab.ts; queries the store for the firstsimulator-typed unified tab across all groups for the worktree - Added early return in
openMobileEmulatorTab— short-circuits before state mutation or RPC calls when the workspace already owns an emulator tab, returning the existing tab ID - Disabled duplicate-launch context menu item — TabBar's "New Mobile Emulator"
DropdownMenuItemrenders asdisabledwith aTooltip("An emulator already exists in this workspace.") whenworkspaceHasSimulatorTabis true - Extended
TabItemunion andreconcileTabOrder—simulatortab type flows through the reconciled tab order and adjacent editor-file tabs carryactiveTabType === 'simulator'active-state wiring - Propagated
onNewSimulatorTabandactiveSimulatorTabIdthroughTabGroupPanel— connects the workspace model'snewSimulatorTabaction andactiveTabstate to the TabBar
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
Co-authored-by: Orca <[email protected]>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Converts the disabled "New Mobile Emulator" menu item into a functional "Go to Mobile Emulator" action that surfaces the existing tab when a workspace already owns one.
- Replace disabled menu item with go-to action —
TabBar.tsxnow renders a clickable "Go to Mobile Emulator"DropdownMenuItemwhenworkspaceHasSimulatorTabis true, tooltip updated to "Open the existing emulator tab." - Route
newSimulatorTabto existing tab —useTabGroupWorkspaceModel.tschecksgetSimulatorTabForWorktreebefore creating a new tab; when one exists, callsensureSimulatorTabwithsurfacePane: true.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
Co-authored-by: Orca <[email protected]>
…/mobile-emulator # Conflicts: # src/renderer/src/lib/workspace-session-patch.ts # src/renderer/src/lib/workspace-session.ts
Co-authored-by: Orca <[email protected]>
Co-authored-by: Orca <[email protected]>
On macOS, tab.newSimulator (New mobile emulator tab) and sidebar.explorer.toggle (Show Explorer) both defaulted to Mod+Shift+E. Show Explorer had the chord first (stablyai#2581); stablyai#4754 reused it for the emulator. When focus is in a terminal, the terminal-scope keydown handler fires first, calls preventDefault(), and opens the emulator, so the global Show Explorer toggle never runs. The conflict detector missed this because the two actions live in different scopes (tabs vs global) and tab.newSimulator never declared a conflictGroup, unlike the comparable tab.openQuickCommandsMenu action. - Move tab.newSimulator's macOS default to Mod+Shift+S (S = Simulator), leaving Cmd+Shift+E to Show Explorer (matches VS Code). - Add conflictGroup: 'global' so findKeybindingConflicts and the Settings UI flag any future overlap with a global chord. - Add a regression test covering the default chord and the guard. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
On macOS, tab.newSimulator (New mobile emulator tab) and sidebar.explorer.toggle (Show Explorer) both defaulted to Mod+Shift+E. Show Explorer had the chord first (stablyai#2581); stablyai#4754 reused it for the emulator. When focus is in a terminal, the terminal-scope keydown handler opens the emulator, so the chord no longer reliably shows the Explorer. The conflict detector missed this because the two actions live in different scopes (tabs vs global) and tab.newSimulator never declared a conflictGroup, unlike the comparable tab.openQuickCommandsMenu action. - Move tab.newSimulator's macOS default to Mod+Shift+S (S = Simulator), leaving Cmd+Shift+E to Show Explorer (matches VS Code). - Add conflictGroup: 'global' so findKeybindingConflicts and the Settings UI flag any future overlap with a global chord. - Add a regression test covering the default chord and the guard.

Summary
Validation
pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/store/slices/store-cascades.test.ts src/cli/index.test.ts src/shared/emulator-touch-frame.test.ts src/renderer/src/lib/ensure-simulator-tab.test.ts src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts src/main/emulator/emulator-bridge.test.ts src/main/emulator/emulator-gesture-sender.test.ts src/main/emulator/serve-sim-helper-processes.test.ts src/main/emulator/serve-sim-state-watcher.test.ts src/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.ts src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx src/renderer/src/components/emulator-pane/emulator-device-state.test.ts src/renderer/src/components/emulator-pane/emulator-screen-gesture.test.ts(14 files, 239 tests)pnpm typecheckpnpm run build:cli(passes; this machine still prints the expected/usr/local/bin/orca-devsymlink permission warning)Manual dogfood
/mobileapp on an iPhone 17 Pro simulator via Expo.Made with Orca 🐋
Summary by CodeRabbit
New Features
Documentation
Chores
Tests