Skip to content

Add mobile emulator#4754

Merged
Jinwoo-H merged 10 commits into
mainfrom
Jinwoo-H/mobile-emulator
Jun 8, 2026
Merged

Add mobile emulator#4754
Jinwoo-H merged 10 commits into
mainfrom
Jinwoo-H/mobile-emulator

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add the Orca mobile emulator runtime bridge backed by serve-sim, including simulator discovery, stream attach, lifecycle cleanup, and gesture forwarding.
  • Add the mobile emulator CLI surface plus runtime RPC/preload plumbing for agent-driven attach, listing, taps, swipes, text input, and shutdown.
  • Add the renderer emulator tab/pane with right-split placement, dynamic iPhone-style frame scaling, touch/scroll streaming, quieter status text, and persisted unified-tab session support.

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 typecheck
  • pnpm run build:cli (passes; this machine still prints the expected /usr/local/bin/orca-dev symlink permission warning)

Manual dogfood

  • Built/installed/launched the /mobile app on an iPhone 17 Pro simulator via Expo.
  • Verified Orca emulator taps, text input, and CLI-driven gesture scroll.
  • Checked Home, Settings, Terminal settings, Notifications, Troubleshooting diagnostics, About, Pair scanner, and manual pairing input screens.

Made with Orca 🐋

Summary by CodeRabbit

  • New Features

    • Full iOS Simulator support: CLI emulator commands, workspace "Mobile Emulator" tab with live MJPEG preview, interactive touch/gesture input, device boot/shutdown/selection, auto-attach, session lifecycle and runtime integration, and settings for enable/default device.
  • Documentation

    • Detailed CLI and emulator skill docs and usage guidance added.
  • Chores

    • Added serve-sim dependency and updated macOS packaging to include simulator resources and executable helpers.
  • Tests

    • Expanded coverage for emulator commands, gesture streaming, state detection, helper processes, and UI integration.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 EmulatorBridge with 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 RuntimeEmulatorCommands with 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-execution module — resolves bundled/development serve-sim executables, handles Electron-as-Node mode, implements a macOS open shim to suppress Simulator.app activation
  • Add useEmulatorPaneSession hook — manages device list, auto-attach, shutdown, and session state with per-worktree tab management, uses CustomEvent bus for cross-instance coordination
  • Add EmulatorDeviceFrame with 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 simulator content type to tab bar, right-split placement support, unified-tab persistence, and a "New Mobile Emulator" entry in the new-tab menu
  • Add CLI emulator commandslist, attach, tap, swipe, type, button, rotate, exec, kill, shutdown with device/worktree selectors
  • Integrate into runtime lifecycle — bridge singleton wired into OrcaRuntimeService, watcher bound/unbound on PTY create/exit/worktree-remove, onAppQuit cleanup

ℹ️ 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

  • useEmulatorTouchStream reconnects 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.sessions Map entries are never cleaned up when unregisterActiveEmulator is called (only activeByWorktree is 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.
  • resolveSimulatorUdid in simctl-simulator-devices.ts returns the raw input string when both simctl and serve-sim --list fail to resolve a name to a UDID, instead of throwing emulator_device_not_found. The eventual error from serve-sim will be less clear than a dedicated error code.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/emulator/emulator-bridge.ts Outdated
return null
}
const session = this.sessions.get(key)
this.activeByWorktree.delete(worktreeId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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 place

Comment thread src/main/emulator/emulator-bridge.ts
@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/mobile-emulator branch from 9cd8355 to 5d7159c Compare June 7, 2026 18:18
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds 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.

Changes

Emulator CLI and simulator tabs

Layer / File(s) Summary
All emulator changes (packaging → runtime → renderer)
config/*, package.json, skills/*, src/main/emulator/*, src/main/*, src/main/runtime/*, src/preload/*, src/cli/*, src/renderer/src/components/emulator-pane/*, src/renderer/src/*, src/shared/*
Adds serve-sim dependency and packaging rules; implements serve-sim execution, readiness, helper process discovery/kill; adds EmulatorBridge, session registry, serve-sim state watcher, simctl device control, and typed emulator errors; registers emulator RPCs/Zod schemas and maps emulator errors; exposes preload IPC and CLI commands/handlers/specs; implements renderer simulator pane, overlay, device frame, touch/gesture streaming via WebSocket, layout/gesture mapping, simulator tab integration, workspace-session persistence/hydration updates, and many tests.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 destroyAllSessions to managed-only — added session.managed check 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 when requestedTarget matches liveTargetRef, 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:
  • Prior pullfrog review: 9cd8355 (#4754 (review))
  • Submitted at: 2026-06-07T00:00:00Z
    -->

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Handle layout-key deletion when pruning removes all surviving groups.

On Line 1644, layoutByWorktree is only updated when nextLayout is truthy. If pruning collapses to no layout (nextLayout is undefined) 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 win

Infinite retry loop on attach failure (still present).

This is the same issue from the previous review: suppressAutoAttachRef.current = false at line 115 unconditionally resets the suppression flag. When attach() fails (lines 159-171), the catch block does not set suppressAutoAttachRef.current = true, so when loading returns to false in the finally block (line 173), the auto-attach effect (lines 233-238) re-fires and calls attach() again, creating an infinite retry loop.

Fix by moving line 115 to line 156 (after successful attach) or setting suppressAutoAttachRef.current = true in 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 win

TOCTOU race and world-writable path in ensureMacOpenShim.

This is a previously flagged security issue that remains unresolved. 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.

🔒 Recommended fix

Use a non-shared directory (e.g., app.getPath('sessionData') or app.getPath('userData')) instead of the static /tmp path, 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.setTimeout is 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. 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.

🐛 Proposed fix

Use a separate setTimeout for 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 win

State corruption: activeByWorktree.delete before managed-only guard.

This is a previously flagged issue that remains unresolved. When managedOnly is true and the session is unmanaged (terminal-started), line 135 returns null — but line 134 has already deleted the worktree→session binding, which is unrecoverable. Subsequent CLI commands fail with emulator_no_active even though the serve-sim process still runs and the sessions Map 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 tradeoff

Unconditional device shutdown on app quit may surprise users.

This is a previously flagged issue that remains unaddressed. destroyAllSessions() unconditionally calls shutdownSimulatorDevice() 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 value

Rotation only cycles between two orientations.

sendRotate toggles 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 value

Clarify 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 win

Remove 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 value

Remove pointless void statement.

Line 162 uses void _opts?.worktreeId to suppress an unused-parameter warning, but worktreeId is 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 tradeoff

Consider splitting this file to address max-lines.

Static analysis reports the file exceeds the line limit. As per coding guidelines, avoid adding a max-lines disable 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 value

Consider adding a comment explaining the simulator id pattern.

The simulator branch uses tab.id for both the presence check and the result id, differing from terminal/browser (which use tab.entityId for id). While this is correct because simulators have a 1:1 relationship between tabs and entities (so entityId === 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

📥 Commits

Reviewing files that changed from the base of the PR and between de7e32c and 5d7159c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (97)
  • .npmrc
  • config/electron-builder.config.cjs
  • config/packaged-runtime-node-modules.cjs
  • package.json
  • skills/orca-cli/SKILL.md
  • skills/orca-emulator/SKILL.md
  • src/cli/args.ts
  • src/cli/dispatch.ts
  • src/cli/handlers/emulator.ts
  • src/cli/help.ts
  • src/cli/index.test.ts
  • src/cli/selectors.ts
  • src/cli/specs/emulator.ts
  • src/cli/specs/index.ts
  • src/main/browser/browser-guest-ui.ts
  • src/main/emulator/emulator-bridge-types.ts
  • src/main/emulator/emulator-bridge.test.ts
  • src/main/emulator/emulator-bridge.ts
  • src/main/emulator/emulator-errors.ts
  • src/main/emulator/emulator-gesture-sender.test.ts
  • src/main/emulator/emulator-gesture-sender.ts
  • src/main/emulator/emulator-types.ts
  • src/main/emulator/serve-sim-detached-session.ts
  • src/main/emulator/serve-sim-endpoint-readiness.ts
  • src/main/emulator/serve-sim-execution.ts
  • src/main/emulator/serve-sim-helper-processes.test.ts
  • src/main/emulator/serve-sim-helper-processes.ts
  • src/main/emulator/serve-sim-state-watcher.test.ts
  • src/main/emulator/serve-sim-state-watcher.ts
  • src/main/emulator/simctl-simulator-devices.ts
  • src/main/index.ts
  • src/main/runtime/orca-runtime-emulator.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/dispatcher.ts
  • src/main/runtime/rpc/errors.ts
  • src/main/runtime/rpc/methods/emulator.ts
  • src/main/runtime/rpc/methods/index.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/Terminal.tsx
  • src/renderer/src/components/WorktreeJumpPalette.tsx
  • src/renderer/src/components/emulator-pane/EmulatorPane.tsx
  • src/renderer/src/components/emulator-pane/EmulatorPaneOverlayLayer.tsx
  • src/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.ts
  • src/renderer/src/components/emulator-pane/emulator-device-frame-layout.ts
  • src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx
  • src/renderer/src/components/emulator-pane/emulator-device-frame.tsx
  • src/renderer/src/components/emulator-pane/emulator-device-state.test.ts
  • src/renderer/src/components/emulator-pane/emulator-device-state.ts
  • src/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsx
  • src/renderer/src/components/emulator-pane/emulator-pane-types.ts
  • src/renderer/src/components/emulator-pane/emulator-phone-hardware-buttons.tsx
  • src/renderer/src/components/emulator-pane/emulator-screen-gesture.test.ts
  • src/renderer/src/components/emulator-pane/emulator-screen-gesture.ts
  • src/renderer/src/components/emulator-pane/emulator-screen-stream-content.tsx
  • src/renderer/src/components/emulator-pane/emulator-unavailable-pane.tsx
  • src/renderer/src/components/emulator-pane/use-emulator-pane-controls.ts
  • src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts
  • src/renderer/src/components/emulator-pane/use-emulator-touch-stream.ts
  • src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts
  • src/renderer/src/components/tab-bar/TabBar.tsx
  • src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts
  • src/renderer/src/components/tab-bar/group-tab-order.test.ts
  • src/renderer/src/components/tab-bar/group-tab-order.ts
  • src/renderer/src/components/tab-bar/reconcile-order.ts
  • src/renderer/src/components/tab-group/TabGroupPanel.tsx
  • src/renderer/src/components/tab-group/useTabDragSplit.ts
  • src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
  • src/renderer/src/components/terminal/tab-type-cycle.ts
  • src/renderer/src/hooks/ipc-tab-switch.test.ts
  • src/renderer/src/hooks/ipc-tab-switch.ts
  • src/renderer/src/hooks/resolve-zoom-target.ts
  • src/renderer/src/hooks/useIpcEvents.ts
  • src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts
  • src/renderer/src/lib/ensure-simulator-tab.test.ts
  • src/renderer/src/lib/ensure-simulator-tab.ts
  • src/renderer/src/lib/file-type-icons.ts
  • src/renderer/src/lib/simulator-tab-shutdown.ts
  • src/renderer/src/lib/tab-number-shortcuts.ts
  • src/renderer/src/lib/workspace-session-patch.test.ts
  • src/renderer/src/lib/workspace-session-patch.ts
  • src/renderer/src/lib/workspace-session-unified-tabs.ts
  • src/renderer/src/lib/workspace-session.test.ts
  • src/renderer/src/lib/workspace-session.ts
  • src/renderer/src/store/slices/store-cascades.test.ts
  • src/renderer/src/store/slices/tabs-hydration.test.ts
  • src/renderer/src/store/slices/tabs-hydration.ts
  • src/renderer/src/store/slices/tabs.test.ts
  • src/renderer/src/store/slices/tabs.ts
  • src/renderer/src/store/slices/worktrees.ts
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/emulator-touch-frame.test.ts
  • src/shared/emulator-touch-frame.ts
  • src/shared/keybindings.ts
  • src/shared/runtime-types.ts
  • src/shared/types.ts
  • src/shared/workspace-session-schema.ts

Comment thread config/packaged-runtime-node-modules.cjs
Comment thread src/cli/specs/emulator.ts Outdated
Comment thread src/main/runtime/orca-runtime.ts
Comment thread src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts Outdated
Comment thread src/shared/keybindings.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 in emulator-session-registry.ts, with toSessionInfo for Info↔State conversion
  • Extracted RuntimeEmulatorCommands — emulator RPC methods now live in a separate command class in orca-runtime-emulator.ts, mirroring the browser command pattern
  • Plugged serveSimStateWatcher PTY leakdropDisconnectedPtyRecord in orca-runtime.ts now calls serveSimStateWatcher.unbindPty(ptyId) when a PTY is pruned without normal exit
  • Fixed newSimulatorTab worktree scoping — action handler in orca-runtime.ts uses the hook-scoped worktreeId instead of useAppStore.getState().activeWorktreeId, preventing wrong-worktree tab creation during multi-worktree use
  • Disabled emulator shortcut on Linux/WindowsnewSimulatorTab keybinding now uses platform-specific defaults: darwin: ['Mod+Shift+E'], linux: [], win32: []
  • Narrowed inspect-webkit omission in packagingisKnownOmittedServeSimDependency verifies the caller is serve-sim's own node_modules before swallowing a missing closure dependency
  • Removed duplicate text flag from emulator type CLI spec allowedFlags

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7159c and afce1b2.

📒 Files selected for processing (7)
  • config/packaged-runtime-node-modules.cjs
  • src/cli/specs/emulator.ts
  • src/main/emulator/emulator-bridge.ts
  • src/main/emulator/emulator-session-registry.ts
  • src/main/runtime/orca-runtime.ts
  • src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
  • src/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

Comment thread src/main/emulator/emulator-session-registry.ts
@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/mobile-emulator branch from 3a1ad78 to b68909d Compare June 7, 2026 21:07

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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-availability moduleinspectEmulatorAvailability() with platform guard and pickDefaultSimulatorDevice() 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.availability RPC method — bridges the availability check to the renderer settings pane via callRuntimeRpc
  • Made emulator.attach device optional — falls back to mobileEmulatorDefaultDeviceUdid from settings, then auto-selects an available iPhone via pickDefaultSimulatorDevice, then throws emulator_device_not_found
  • Added emulator_disabled erroremulatorAttach throws this when mobileEmulatorEnabled === false in settings
  • Guarded browser shortcut forwarding with mobileEmulatorEnabledCmd+Shift+E is 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 makes newSimulatorTab action undefined when disabled
  • Guarded ensureSimulatorTab with mobileEmulatorEnabled — returns null when feature is off, preventing tab creation via keyboard or IPC
  • Extracted useEmulatorPaneSessionEvents hook — moves auto-attach and shutdown window.addEventListener logic out of useEmulatorPaneSession into a dedicated hook
  • Added default settingsmobileEmulatorEnabled: true and mobileEmulatorDefaultDeviceUdid: null in getDefaultSettings

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add an explicit macOS runtime guard to the simulator shortcut branch.

Line 1323 handles tab.newSimulator without 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

📥 Commits

Reviewing files that changed from the base of the PR and between afce1b2 and 3a1ad78.

📒 Files selected for processing (25)
  • src/cli/handlers/emulator.ts
  • src/cli/specs/emulator.ts
  • src/main/browser/browser-guest-ui.ts
  • src/main/browser/browser-manager.ts
  • src/main/emulator/emulator-availability.ts
  • src/main/emulator/emulator-bridge.test.ts
  • src/main/emulator/emulator-bridge.ts
  • src/main/emulator/emulator-errors.ts
  • src/main/runtime/orca-runtime-emulator.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/emulator.ts
  • src/renderer/src/components/Terminal.tsx
  • src/renderer/src/components/emulator-pane/use-emulator-pane-session-events.ts
  • src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts
  • src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx
  • src/renderer/src/components/settings/Settings.tsx
  • src/renderer/src/components/settings/mobile-emulator-search.ts
  • src/renderer/src/components/tab-bar/TabBar.tsx
  • src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
  • src/renderer/src/hooks/useSettingsNavigationMetadata.ts
  • src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts
  • src/renderer/src/lib/ensure-simulator-tab.ts
  • src/renderer/src/lib/settings-navigation-types.ts
  • src/shared/constants.ts
  • src/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

@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/mobile-emulator branch 2 times, most recently from d9cb302 to 2b9c295 Compare June 7, 2026 21:22

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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-availability moduleinspectEmulatorAvailability() with platform guard, simctl/serve-sim health checks, and pickDefaultSimulatorDevice() priority chain (booted iPhone → booted any → available iPhone → available any → any)
  • Added MobileEmulatorSettingsPane — enable/disable toggle, availability status badge with refresh, and default device Select with an "Automatic" sentinel value
  • Added emulator.availability RPC method — bridges the availability check to the renderer settings pane via callRuntimeRpc
  • Added emulator_disabled erroremulatorAttach throws this when mobileEmulatorEnabled === false in settings
  • Added mapEmulatorError to RPC dispatcher — forwards structured EmulatorError.code strings (parallel to mapBrowserError)
  • Guarded browser shortcut forwardingCmd+Shift+E in browser-guest-ui.ts now checks process.platform === 'darwin' and reads mobileEmulatorEnabled from settings before forwarding to the renderer
  • Guarded tab bar and workspace model — "New Mobile Emulator" menu item hidden and newSimulatorTab action undefined when mobileEmulatorEnabled is false
  • Guarded ensureSimulatorTab — returns null when mobileEmulatorEnabled === false or platform is not macOS
  • Extracted useEmulatorPaneSessionEvents hook — moves auto-attach and shutdown window.addEventListener logic out of useEmulatorPaneSession
  • Extended settings schemamobileEmulatorEnabled (default true) and mobileEmulatorDefaultDeviceUdid (default null) in GlobalSettings and getDefaultSettings
  • Extended tab type system'simulator' added to TabContentType, WorkspaceVisibleTabType, and workspace session zod schema
  • Wired serveSimStateWatcher lifecycle hooksbindPty/unbindPty/forgetWorktree/ingestPtyOutput woven into OrcaRuntimeService alongside the existing advertisedUrlWatcher hooks
  • Added checkServeSimAvailable to EmulatorBridge — runs serve-sim --help with a 10s timeout

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/mobile-emulator branch from 2b9c295 to 2b696b4 Compare June 7, 2026 21:26

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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-availability moduleinspectEmulatorAvailability() with platform guard, simctl/serve-sim health checks, and pickDefaultSimulatorDevice() 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 device Select with an "Auto-select device" sentinel value
  • Added emulator.availability RPC method — bridges the availability check to the renderer settings pane
  • Added emulator_disabled erroremulatorAttach throws this when mobileEmulatorEnabled === false
  • Made emulator.attach device optional in RPC params and CLI spec, with fallback chain: explicit device → settings default → pickDefaultSimulatorDeviceemulator_device_not_found error
  • Guarded emulator shortcuts and UICmd+Shift+E forwarding in browser guest reads mobileEmulatorEnabled; Terminal shortcut handler, TabBar "New Mobile Emulator" menu item, workspace model newSimulatorTab action, and ensureSimulatorTab all return early or hide when feature is disabled
  • Extracted useEmulatorPaneSessionEvents — auto-attach and shutdown CustomEvent listeners into a dedicated hook called from useEmulatorPaneSession
  • Wired configuredDefaultUdid into session hookselectedUdid initializes from and syncs to the settings default, and the attach fallback chain includes the configured value

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Only consume the shortcut when a simulator tab can actually open.

This currently calls preventDefault() before the floating-workspace check, so Cmd/Ctrl+Shift+E becomes 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 win

Document the opt-out setting semantics.

!== false is carrying a non-obvious contract here: older persisted settings default to enabled. A short Why: 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

📥 Commits

Reviewing files that changed from the base of the PR and between b68909d and 2b696b4.

📒 Files selected for processing (25)
  • src/cli/handlers/emulator.ts
  • src/cli/specs/emulator.ts
  • src/main/browser/browser-guest-ui.ts
  • src/main/browser/browser-manager.ts
  • src/main/emulator/emulator-availability.ts
  • src/main/emulator/emulator-bridge.test.ts
  • src/main/emulator/emulator-bridge.ts
  • src/main/emulator/emulator-errors.ts
  • src/main/runtime/orca-runtime-emulator.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/emulator.ts
  • src/renderer/src/components/Terminal.tsx
  • src/renderer/src/components/emulator-pane/use-emulator-pane-session-events.ts
  • src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts
  • src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx
  • src/renderer/src/components/settings/Settings.tsx
  • src/renderer/src/components/settings/mobile-emulator-search.ts
  • src/renderer/src/components/tab-bar/TabBar.tsx
  • src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
  • src/renderer/src/hooks/useSettingsNavigationMetadata.ts
  • src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts
  • src/renderer/src/lib/ensure-simulator-tab.ts
  • src/renderer/src/lib/settings-navigation-types.ts
  • src/shared/constants.ts
  • src/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

Jinwoo-H and others added 2 commits June 7, 2026 18:46

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 MjpegFrameStream class with extractJpegFrames parser 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 createUnifiedTabInSplit store 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 openMobileEmulatorTab with "surface first, attach second" flow — surfaces the tab synchronously before the async emulator attach completes, uses simulator-launch-coordination (prelaunched session + manual-launch-pending flags) to bridge the gap for auto-attach and manual-launch event consumers
  • Refactored useEmulatorPaneSession — extracted useEmulatorPaneLifecycle (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), and emulator-prelaunched-session (session state from prelaunched info)
  • Added graceful shutdown schedulerscheduleSimulatorPaneManagedShutdown with 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 shimensureMacOpenShim now intercepts any Simulator.app / com.apple.iphonesimulator variant, falling through to real /usr/bin/open for non-simulator targets
  • Updated serve-sim-detached-session — derives MJPEG endpoint from older serve-sim url field via streamUrlFromServeSimUrl when streamUrl is absent

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 getSimulatorTabForWorktree helper — extracted from ensure-simulator-tab.ts; queries the store for the first simulator-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" DropdownMenuItem renders as disabled with a Tooltip ("An emulator already exists in this workspace.") when workspaceHasSimulatorTab is true
  • Extended TabItem union and reconcileTabOrdersimulator tab type flows through the reconciled tab order and adjacent editor-file tabs carry activeTabType === 'simulator' active-state wiring
  • Propagated onNewSimulatorTab and activeSimulatorTabId through TabGroupPanel — connects the workspace model's newSimulatorTab action and activeTab state to the TabBar

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 actionTabBar.tsx now renders a clickable "Go to Mobile Emulator" DropdownMenuItem when workspaceHasSimulatorTab is true, tooltip updated to "Open the existing emulator tab."
  • Route newSimulatorTab to existing tabuseTabGroupWorkspaceModel.ts checks getSimulatorTabForWorktree before creating a new tab; when one exists, calls ensureSimulatorTab with surfacePane: true.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Jinwoo-H and others added 3 commits June 7, 2026 21:23
…/mobile-emulator

# Conflicts:
#	src/renderer/src/lib/workspace-session-patch.ts
#	src/renderer/src/lib/workspace-session.ts
@Jinwoo-H
Jinwoo-H merged commit 1b363b4 into main Jun 8, 2026
10 of 11 checks passed
thomaszdxsn pushed a commit to thomaszdxsn/orca that referenced this pull request Jun 15, 2026
yejinh added a commit to yejinh/orca that referenced this pull request Jul 13, 2026
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]>
yejinh added a commit to yejinh/orca that referenced this pull request Jul 13, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant