Skip to content

Refine simulator panel and inspect mode#261

Merged
zvadaadam merged 8 commits into
mainfrom
zvadaadam/mobile-inspect
May 5, 2026
Merged

Refine simulator panel and inspect mode#261
zvadaadam merged 8 commits into
mainfrom
zvadaadam/mobile-inspect

Conversation

@zvadaadam

@zvadaadam zvadaadam commented May 5, 2026

Copy link
Copy Markdown
Owner

Refactors the simulator panel into focused header/device/empty-state components and simplifies the idle/running UI with a cleaner picker, device header controls, and simplified frame rendering. Adds simulator switching/ownership safeguards, stream lifecycle cleanup, safer event parsing, packaged device-use manifest/resource resolution, and universal native helper preparation. Extends device-use inspector/simbridge support including redacted secure text snapshots and packaged siminspector handling, while removing the old device-chrome generation path. Validated with bun run typecheck, bun run typecheck:backend, bun run --cwd packages/device-use typecheck, and bunx vitest run --config test/vitest.config.ts test/unit/simulator/machine.test.ts.

Summary by CodeRabbit

  • New Features

    • Inspector snapshots and live "inspect" mode for UI hierarchies; frontend + backend start/inspect APIs
    • Simulator switching while streaming; new simulator panels, headers, controls, Apple logo icon, and device frame UI
  • Bug Fixes & Improvements

    • Robust manifest/path resolution for dev vs packaged installs
    • Improved simulator session, UDID and port management; stronger WebSocket payload validation
  • Removals

    • Device chrome asset generation pipeline and its scripts/tools removed
  • Chores

    • Bundled native inspector artifact and packaging/build updates

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68d4befa-70f0-4baa-971e-7cc3485067b0

📥 Commits

Reviewing files that changed from the base of the PR and between b8c16f8 and 3932265.

📒 Files selected for processing (1)
  • .claude
💤 Files with no reviewable changes (1)
  • .claude

📝 Walkthrough

Walkthrough

Adds a native siminspector (dylib + Objective‑C socket server), backend inspector APIs and UDID/port coordination, frontend inspector UI and types, device-frame refactor removing device‑chrome, dual installed-app manifest resolution, new simulator commands, and packaging/build updates across backend, web, and device‑use packages.


Changes

Inspector & Inspection UI

Layer / File(s) Summary
Data Types
apps/web/src/features/simulator/types.ts, apps/backend/src/services/simulator-context.ts
Adds InspectorRect, InspectorNode, InspectorSnapshot interfaces used by backend and frontend.
Native Server & Build
packages/device-use/native/Sources/SimInspector/Inspector.m, packages/device-use/native/Sources/SimInspector/build.sh
Adds Objective‑C AF_UNIX JSON‑line server producing view/layer snapshots and a build script producing universal siminspector.dylib.
Native Packaging Integration
packages/device-use/scripts/build-native.ts, packages/device-use/scripts/compile-bin.ts, packages/device-use/package.json, electron-builder.yml
Detects/validates architectures, builds or requires siminspector.dylib, copies it into bin/ and includes it in packaged Electron resources and package files.
Engine API
packages/device-use/src/engine/simbridge.ts, packages/device-use/src/engine/index.ts
Adds findInspectorPath() and re-exports it for runtime resolution of siminspector.dylib.
Backend Simulator Context
apps/backend/src/services/simulator-context.ts
Adds inspector socket helpers, startInspector and inspectorSnapshot, UDID "starting" claim tracking, in-memory reserved port scanning, binary resolution for inspector, accessibility fallback, and cleans up tracking in destroyAll.
Backend Command Handlers
apps/backend/src/services/agent/commands.ts, shared/events.ts
Adds sim:inspectStart and sim:inspectSnapshot to command dispatch and COMMAND_NAMES.
Frontend Service & Parsing
apps/web/src/features/simulator/api/simulator.service.ts
Adds type-safe parsers and new methods startInspect and inspectSnapshot; validates WS payload shapes and normalizes errors.
Frontend Viewer & Panel
apps/web/src/features/simulator/ui/SimulatorStreamViewer.tsx, apps/web/src/features/simulator/ui/SimulatorPanel.tsx, apps/web/src/features/simulator/ui/SimulatorDeviceHeader.tsx
Implements inspect mode: snapshot polling, flattening/selection/hovering of nodes, overlay highlight, InspectorDetailsPanel, toolbar buttons and wiring for starting/stopping inspection.
Tests / Wiring
apps/backend/test/unit/shared/events.test.ts, test/unit/simulator/machine.test.ts
Adds command constants tests and machine transition tests for device switching where relevant.

Device Frame, Device‑Chrome Removal & Installed‑App Resolution

Layer / File(s) Summary
Device‑Chrome Removal
apps/web/src/features/simulator/device-chrome.ts, scripts/prepare-device-chrome.mjs, package.json
Removes the device‑chrome manifest loader, its prepare script, and the prepare:device-chrome npm script.
DeviceFrame Refactor
apps/web/src/features/simulator/ui/DeviceFrame.tsx
Replaces manifest-driven bezel rendering with built-in geometry (KNOWN_SCREEN_SIZES), adds screenSize and header props, and computes aspect/radius responsively.
UI Components & Layout
apps/web/src/features/simulator/ui/AppleLogoIcon.tsx, SimulatorContentHeader.tsx, SimulatorLaunchPreview.tsx, SimulatorEmptySurface.tsx, simulatorDisplay.ts
Adds small UI components, header, launch preview, empty surface, and formatSimulatorRuntime.
State/Machine Adjustments
apps/web/src/features/simulator/machine.ts, apps/web/src/features/simulator/store.ts
Adds SWITCH_DEVICE event and machine transition handling; store doc updated.
Installed App Manifests
apps/backend/src/config/installed-apps.ts
Replaces prior single hardcoded manifest path with uniqueExisting([resolvePackagedManifest(), resolveDevManifest()]) to dedupe and use existing packaged or dev manifest paths.
Prepare Script Update
scripts/prepare-device-use.mjs
Reworks native artifact detection/build copy to handle both simbridge and siminspector.dylib, adds architecture checks and conditional native build invocation.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend (React)
    participant Backend as Backend (Node)
    participant Simulator as iOS Simulator
    participant Inspector as siminspector Socket

    User->>Frontend: Toggle Inspect mode
    Frontend->>Backend: sim:inspectStart(workspaceId, bundleId?)
    Backend->>Simulator: xcrun simctl launch with DYLD_INSERT_LIBRARIES=siminspector
    Simulator->>Inspector: Load dylib → open AF_UNIX socket
    Backend->>Inspector: Connect and send {"command":"snapshot"}
    Inspector->>Simulator: Traverse UIView/CALayer tree on main thread
    Inspector-->>Backend: JSON InspectorSnapshot\n
    Backend-->>Frontend: sim:inspectSnapshot payload
    Frontend->>Frontend: Parse snapshot, flatten nodes, compute bounds
    User->>Frontend: Alt+click on element
    Frontend->>Frontend: Pick node, show InspectorDetailsPanel, "Add to Chat"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs


Poem

🐰 I stitched a socket in the simulator’s den,

dylib whispers, windows open then —
Nodes spread like carrots in a tree,
Alt‑click plucks one straight to me.
A rabbit posts it to the chat pen.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refine simulator panel and inspect mode' accurately describes the main objective—refactoring the simulator panel UI and adding inspect mode functionality. It is concise, clear, and directly reflects the core changes evident throughout the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zvadaadam/mobile-inspect

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
apps/web/src/features/simulator/ui/SimulatorDeviceHeader.tsx (1)

130-145: ⚡ Quick win

Add exhaustiveness check to getStatusLabel.

The switch statement lacks a default case or exhaustiveness guard. If a new phase is added to SimPhase, this function would silently return undefined. Consider adding a default with a never assertion or using ts-pattern with .exhaustive().

Proposed fix
 function getStatusLabel(state: SimPhase) {
   switch (state.phase) {
     case "idle":
       return "Simulator idle";
     case "booting":
       return "Simulator booting";
     case "streaming":
       return "Simulator streaming";
     case "building":
       return "Building app";
     case "running":
       return "App running";
     case "error":
       return "Simulator error";
+    default: {
+      const _exhaustive: never = state;
+      return _exhaustive;
+    }
   }
 }
🤖 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 `@apps/web/src/features/simulator/ui/SimulatorDeviceHeader.tsx` around lines
130 - 145, getStatusLabel lacks an exhaustiveness check so adding new variants
to SimPhase can return undefined; update getStatusLabel to include a default
branch that asserts the unreachable value (use a never-typed helper, e.g.
function assertUnreachable(x: never): never) and call it in the default case, or
replace the switch with a pattern-matching approach (ts-pattern/.exhaustive())
to force compile-time exhaustiveness for the SimPhase union.
apps/web/src/features/simulator/machine.ts (1)

62-146: 💤 Low value

Consider migrating to ts-pattern for exhaustiveness checking.

The state machine uses a native switch with a manual never exhaustiveness check. Per coding guidelines, ts-pattern with .exhaustive() is preferred for discriminated unions. This is existing code structure, so migration can be deferred.

🤖 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 `@apps/web/src/features/simulator/machine.ts` around lines 62 - 146, The
current transition function uses a switch on event with a manual never-check
(_exhaustive) for exhaustiveness; replace it with ts-pattern's match to get
.exhaustive() checking: install/import match from 'ts-pattern', convert the
switch in transition(current: SimPhase, event: SimEvent) into a
match(event).with({ type: "BOOT" }, e => ... ) chain covering all event.type
cases (BOOT, SWITCH_DEVICE, STREAM_READY, BUILD_START, BUILD_SUCCESS,
APP_UNINSTALLED, STOP, ERROR, CLEAR) and return the same SimPhase results (use
current.udid/current.stream where needed) and finish with .exhaustive(),
removing the manual _exhaustive variable; ensure return types remain SimPhase |
null and preserve all existing guards (e.g., current.phase checks) inside each
handler.
apps/web/src/features/simulator/ui/SimulatorPanel.tsx (2)

783-882: ⚖️ Poor tradeoff

Consider extracting InspectorDetailsPanel to a separate file.

As a nested function component, it's recreated on every SimulatorPanel render. While the current implementation works, extracting it would:

  • Improve code organization (this file is now ~986 lines)
  • Allow React.memo optimization if needed later
  • Follow the pattern of other extracted components (SimulatorDeviceHeader, SimulatorStreamViewer, etc.)
🤖 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 `@apps/web/src/features/simulator/ui/SimulatorPanel.tsx` around lines 783 -
882, InspectorDetailsPanel is defined as a nested function component inside
SimulatorPanel which causes it to be recreated on every render; extract it into
its own file by creating a new component file (e.g., InspectorDetailsPanel.tsx)
that exports the InspectorDetailsPanel component (accepting props: node:
InspectorNode, prompt: string, onPromptChange: (value: string) => void, onClose:
() => void, onSendToChat: () => void), move the JSX and prop typings there,
import any types it needs (InspectorNode) from the same module as
SimulatorPanel, replace the nested function in SimulatorPanel with a simple
import/use of InspectorDetailsPanel, and optionally wrap the exported component
in React.memo to allow future memoization and match the project's component
extraction pattern.

601-607: 💤 Low value

Consider adding a stale-closure guard for the polling interval.

The interval callback captures refreshInspectorSnapshot which depends on workspaceId. If the workspace changes while inspect mode is active, the interval could fetch data for the wrong workspace before the cleanup runs.

A generation counter pattern (like workspaceGenerationRef already used elsewhere in this file) would prevent stale updates:

🛡️ Suggested guard pattern
 useEffect(() => {
   if (!inspectMode) return;
+  const gen = workspaceGenerationRef.current;
   const timer = setInterval(() => {
-    refreshInspectorSnapshot().catch((err) => setInspectError(getErrorMessage(err)));
+    refreshInspectorSnapshot()
+      .then((snapshot) => {
+        if (workspaceGenerationRef.current !== gen) return;
+        // snapshot already set inside refreshInspectorSnapshot
+      })
+      .catch((err) => {
+        if (workspaceGenerationRef.current !== gen) return;
+        setInspectError(getErrorMessage(err));
+      });
   }, 1000);
   return () => clearInterval(timer);
 }, [inspectMode, refreshInspectorSnapshot]);
🤖 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 `@apps/web/src/features/simulator/ui/SimulatorPanel.tsx` around lines 601 -
607, The polling effect for inspect mode can call refreshInspectorSnapshot for a
stale workspace; capture the current workspace generation from the existing
workspaceGenerationRef when installing the interval and skip ticks whose
generation no longer matches so the callback never invokes
refreshInspectorSnapshot for an out-of-date workspace. Concretely: inside the
useEffect that depends on inspectMode and refreshInspectorSnapshot, read const
generationAtStart = workspaceGenerationRef.current when creating the timer and
in the interval callback first check if (generationAtStart !==
workspaceGenerationRef.current) return; before calling
refreshInspectorSnapshot().catch(...); this uses the existing
workspaceGenerationRef to guard refreshInspectorSnapshot and prevent
stale-closure polling when workspace changes.
apps/web/src/features/simulator/api/simulator.service.ts (1)

55-61: ⚡ Quick win

parseInspectorSnapshot validates structure presence but not shape.

The function checks that snapshot exists and is an object, but then casts directly to InspectorSnapshot without validating that the object actually conforms to that interface (e.g., has roots, bundleId, pid, timestamp). If the backend returns an object with missing or wrong-typed fields, consumers will encounter runtime errors when accessing properties like snapshot.roots.

Consider either:

  1. Adding minimal shape validation before the cast
  2. Using a schema validator (e.g., Zod) for the response
Example with minimal validation
 function parseInspectorSnapshot(result: unknown): InspectorSnapshot {
   const snapshot = asRecord(result).snapshot;
   if (!snapshot || typeof snapshot !== "object") {
     throw new Error("Malformed inspector snapshot response");
   }
-  return snapshot as InspectorSnapshot;
+  const s = snapshot as Record<string, unknown>;
+  if (!Array.isArray(s.roots) || typeof s.bundleId !== "string") {
+    throw new Error("Malformed inspector snapshot response");
+  }
+  return snapshot as InspectorSnapshot;
 }
🤖 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 `@apps/web/src/features/simulator/api/simulator.service.ts` around lines 55 -
61, The parseInspectorSnapshot function only checks snapshot existence but then
force-casts to InspectorSnapshot; add minimal runtime shape validation in
parseInspectorSnapshot to verify required fields (e.g., roots is an array,
bundleId is a string, pid is a number, timestamp is a number) before returning,
and throw a descriptive error if any field is missing or wrong-typed;
alternatively replace the manual checks by validating the result with a schema
validator (e.g., a Zod schema for InspectorSnapshot) and use that to
parse/return the typed value.
apps/web/src/features/simulator/ui/SimulatorStreamViewer.tsx (1)

387-407: 💤 Low value

Inspect mode hover fires even during drag.

When inspectMode is true and the user is dragging (buttons === 1) without Alt, the handler first fires onInspectorHover then continues to send touch events. This causes unnecessary hover callbacks during normal drag operations. Consider guarding the hover call:

Suggested fix
   const handleMouseMove = useCallback(
     (e: React.MouseEvent) => {
-      if (inspectMode) {
+      if (inspectMode && e.buttons === 0) {
         onInspectorHover?.(getInspectorNode(e));
       }
       if (inspectMode && e.altKey) return;
       if (!isLive || e.buttons !== 1) 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 `@apps/web/src/features/simulator/ui/SimulatorStreamViewer.tsx` around lines
387 - 407, The handler handleMouseMove currently calls
onInspectorHover(getInspectorNode(e)) whenever inspectMode is true even if the
user is actively dragging (e.buttons === 1) and not holding Alt; change the
logic so that when inspectMode is true you first check the drag/Alt guard (i.e.,
if inspectMode && e.altKey is false and e.buttons === 1 then treat as a drag and
do not call onInspectorHover), and only call
onInspectorHover(getInspectorNode(e)) when inspectMode is true and the event is
not a drag (or Alt is pressed). Update the conditional ordering inside
handleMouseMove (using getInspectorNode, onInspectorHover, inspectMode,
e.buttons, e.altKey) so hover callbacks are skipped during normal drag
operations.
apps/backend/src/services/simulator-context.ts (1)

573-668: 💤 Low value

Port reservation released even when stream reuses existing UDID stream.

At line 607, releasePortReservation(reservedPort) is called after spawnStream succeeds, and reservedPort is set to null. However, if spawnStream returns an existing stream (lines 365-372), the reserved port was never actually used by the new stream—it just returns the existing port. The reservation is still released correctly, which is fine, but the port was reserved unnecessarily.

This is a minor inefficiency; the port scan work is wasted when reusing an existing stream.

Consider checking for existing stream before reserving
+  // Check if this UDID already has a stream before reserving a port
+  const existing = activeStreams.get(udid);
+  if (existing) {
+    try {
+      process.kill(existing.pid, 0);
+      // Reuse existing stream, skip port reservation
+      // ... handle reuse case
+    } catch {
+      activeStreams.delete(udid);
+    }
+  }
+
   reservedPort = await reservePort(3100 + sessions.size + startingUdids.size);
🤖 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 `@apps/backend/src/services/simulator-context.ts` around lines 573 - 668,
Before reserving a port, check whether an active stream already exists for the
UDID and only call reservePort/reservePortReservation when you truly need to
spawn a new stream; specifically, change the sequence around
spawnStream/reservePort so that you either (a) call a new helper like
getActiveStreamPort(udid) (or extend spawnStream to report reuse) and, if it
returns a port, skip reservePort and use that port, or (b) call spawnStream with
an explicit optionalPort parameter and only call reservePort when spawnStream
indicates it will create a new stream; update the code that currently sets
reservedPort, calls reservePort(…), then spawnStream(udid, reservedPort) to
first detect existing stream, and only set/release reservedPort when you
actually created a new stream (ensure releasePortReservation(reservedPort) and
reservedPort = null remain correct when creation succeeds).
🤖 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 `@apps/web/src/features/simulator/ui/SimulatorContentHeader.tsx`:
- Around line 55-57: In the SimulatorContentHeader component update the
simulator picker trigger element (the JSX element with aria-label="Select
simulator device") to use correct menu semantics by changing
aria-haspopup="listbox" to aria-haspopup="menu" (or removing the attribute to
let default semantics apply); ensure this change is applied on the dropdown
trigger JSX so it matches the DropdownMenu usage and assistive tech sees a menu
popup.

In `@apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx`:
- Around line 13-16: The error branch in SimulatorLaunchPreview renders an alert
even when errorMessage is undefined, causing a blank destructive state; update
the render logic in SimulatorLaunchPreview so the error alert is only rendered
when errorMessage is truthy (and only show the retry action when both canRetry
and onRetry are provided), i.e., gate the alert on errorMessage and gate the
retry button on canRetry && typeof onRetry === 'function' while preserving
existing props (errorMessage, canRetry, onRetry, onStart).

In `@packages/device-use/native/Sources/SimInspector/Inspector.m`:
- Around line 228-229: The current call to write(clientFd, output.bytes,
output.length) may perform a short write and truncate large json_line(response)
payloads; replace it with a loop that repeatedly writes the remaining bytes from
json_line(response) until total bytes == output.length, handling partial writes
by advancing the buffer pointer and reducing the remaining count, and on error
handle EINTR by retrying and treat other errors as fatal. Locate the write site
in Inspector.m (the json_line(response) / write(clientFd, ...) usage) and
implement this retry/partial-write logic (optionally using send(clientFd, ptr,
remaining, MSG_NOSIGNAL) if preferred).

In `@packages/device-use/scripts/build-native.ts`:
- Around line 57-64: The check for siminspector currently only uses
existsSync(inspectorBinary) (inspectorReady) which permits stale single-arch
dylibs to skip rebuilding; update the readiness check to mirror the simbridge
validation by verifying the inspector binary has both architectures (e.g., run
the same arch validation logic used for releaseBinary or a lipo/file check) and
treat inspectorReady as false if the universal/expected archs are not present so
the build path (build.sh) will rebuild a universal binary for both
electron-builder.yml targets; update the conditional that uses bridgeReady and
inspectorReady and the variables inspectorBinary/releaseBinary accordingly so
packaging never skips when inspectorBinary is single-arch or missing.

---

Nitpick comments:
In `@apps/backend/src/services/simulator-context.ts`:
- Around line 573-668: Before reserving a port, check whether an active stream
already exists for the UDID and only call reservePort/reservePortReservation
when you truly need to spawn a new stream; specifically, change the sequence
around spawnStream/reservePort so that you either (a) call a new helper like
getActiveStreamPort(udid) (or extend spawnStream to report reuse) and, if it
returns a port, skip reservePort and use that port, or (b) call spawnStream with
an explicit optionalPort parameter and only call reservePort when spawnStream
indicates it will create a new stream; update the code that currently sets
reservedPort, calls reservePort(…), then spawnStream(udid, reservedPort) to
first detect existing stream, and only set/release reservedPort when you
actually created a new stream (ensure releasePortReservation(reservedPort) and
reservedPort = null remain correct when creation succeeds).

In `@apps/web/src/features/simulator/api/simulator.service.ts`:
- Around line 55-61: The parseInspectorSnapshot function only checks snapshot
existence but then force-casts to InspectorSnapshot; add minimal runtime shape
validation in parseInspectorSnapshot to verify required fields (e.g., roots is
an array, bundleId is a string, pid is a number, timestamp is a number) before
returning, and throw a descriptive error if any field is missing or wrong-typed;
alternatively replace the manual checks by validating the result with a schema
validator (e.g., a Zod schema for InspectorSnapshot) and use that to
parse/return the typed value.

In `@apps/web/src/features/simulator/machine.ts`:
- Around line 62-146: The current transition function uses a switch on event
with a manual never-check (_exhaustive) for exhaustiveness; replace it with
ts-pattern's match to get .exhaustive() checking: install/import match from
'ts-pattern', convert the switch in transition(current: SimPhase, event:
SimEvent) into a match(event).with({ type: "BOOT" }, e => ... ) chain covering
all event.type cases (BOOT, SWITCH_DEVICE, STREAM_READY, BUILD_START,
BUILD_SUCCESS, APP_UNINSTALLED, STOP, ERROR, CLEAR) and return the same SimPhase
results (use current.udid/current.stream where needed) and finish with
.exhaustive(), removing the manual _exhaustive variable; ensure return types
remain SimPhase | null and preserve all existing guards (e.g., current.phase
checks) inside each handler.

In `@apps/web/src/features/simulator/ui/SimulatorDeviceHeader.tsx`:
- Around line 130-145: getStatusLabel lacks an exhaustiveness check so adding
new variants to SimPhase can return undefined; update getStatusLabel to include
a default branch that asserts the unreachable value (use a never-typed helper,
e.g. function assertUnreachable(x: never): never) and call it in the default
case, or replace the switch with a pattern-matching approach
(ts-pattern/.exhaustive()) to force compile-time exhaustiveness for the SimPhase
union.

In `@apps/web/src/features/simulator/ui/SimulatorPanel.tsx`:
- Around line 783-882: InspectorDetailsPanel is defined as a nested function
component inside SimulatorPanel which causes it to be recreated on every render;
extract it into its own file by creating a new component file (e.g.,
InspectorDetailsPanel.tsx) that exports the InspectorDetailsPanel component
(accepting props: node: InspectorNode, prompt: string, onPromptChange: (value:
string) => void, onClose: () => void, onSendToChat: () => void), move the JSX
and prop typings there, import any types it needs (InspectorNode) from the same
module as SimulatorPanel, replace the nested function in SimulatorPanel with a
simple import/use of InspectorDetailsPanel, and optionally wrap the exported
component in React.memo to allow future memoization and match the project's
component extraction pattern.
- Around line 601-607: The polling effect for inspect mode can call
refreshInspectorSnapshot for a stale workspace; capture the current workspace
generation from the existing workspaceGenerationRef when installing the interval
and skip ticks whose generation no longer matches so the callback never invokes
refreshInspectorSnapshot for an out-of-date workspace. Concretely: inside the
useEffect that depends on inspectMode and refreshInspectorSnapshot, read const
generationAtStart = workspaceGenerationRef.current when creating the timer and
in the interval callback first check if (generationAtStart !==
workspaceGenerationRef.current) return; before calling
refreshInspectorSnapshot().catch(...); this uses the existing
workspaceGenerationRef to guard refreshInspectorSnapshot and prevent
stale-closure polling when workspace changes.

In `@apps/web/src/features/simulator/ui/SimulatorStreamViewer.tsx`:
- Around line 387-407: The handler handleMouseMove currently calls
onInspectorHover(getInspectorNode(e)) whenever inspectMode is true even if the
user is actively dragging (e.buttons === 1) and not holding Alt; change the
logic so that when inspectMode is true you first check the drag/Alt guard (i.e.,
if inspectMode && e.altKey is false and e.buttons === 1 then treat as a drag and
do not call onInspectorHover), and only call
onInspectorHover(getInspectorNode(e)) when inspectMode is true and the event is
not a drag (or Alt is pressed). Update the conditional ordering inside
handleMouseMove (using getInspectorNode, onInspectorHover, inspectMode,
e.buttons, e.altKey) so hover callbacks are skipped during normal drag
operations.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d32e77aa-3c56-4b1d-b6af-71dc9590b27a

📥 Commits

Reviewing files that changed from the base of the PR and between 38a6693 and 6c554dc.

📒 Files selected for processing (32)
  • apps/backend/src/config/installed-apps.ts
  • apps/backend/src/services/agent/commands.ts
  • apps/backend/src/services/simulator-context.ts
  • apps/backend/test/unit/shared/events.test.ts
  • apps/web/src/features/simulator/api/simulator.service.ts
  • apps/web/src/features/simulator/device-chrome.ts
  • apps/web/src/features/simulator/machine.ts
  • apps/web/src/features/simulator/store.ts
  • apps/web/src/features/simulator/types.ts
  • apps/web/src/features/simulator/ui/AppleLogoIcon.tsx
  • apps/web/src/features/simulator/ui/DeviceFrame.tsx
  • apps/web/src/features/simulator/ui/SimulatorContentHeader.tsx
  • apps/web/src/features/simulator/ui/SimulatorDeviceHeader.tsx
  • apps/web/src/features/simulator/ui/SimulatorEmptySurface.tsx
  • apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx
  • apps/web/src/features/simulator/ui/SimulatorPanel.tsx
  • apps/web/src/features/simulator/ui/SimulatorStreamViewer.tsx
  • apps/web/src/features/simulator/ui/simulatorDisplay.ts
  • electron-builder.yml
  • package.json
  • packages/device-use/native/Sources/SimInspector/Inspector.m
  • packages/device-use/native/Sources/SimInspector/build.sh
  • packages/device-use/package.json
  • packages/device-use/scripts/build-native.ts
  • packages/device-use/scripts/compile-bin.ts
  • packages/device-use/src/cli/commands/serve.ts
  • packages/device-use/src/engine/index.ts
  • packages/device-use/src/engine/simbridge.ts
  • scripts/prepare-device-chrome.mjs
  • scripts/prepare-device-use.mjs
  • shared/events.ts
  • test/unit/simulator/machine.test.ts
💤 Files with no reviewable changes (3)
  • apps/web/src/features/simulator/device-chrome.ts
  • package.json
  • scripts/prepare-device-chrome.mjs

Comment thread apps/web/src/features/simulator/ui/SimulatorContentHeader.tsx
Comment thread apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx
Comment thread packages/device-use/native/Sources/SimInspector/Inspector.m Outdated
Comment thread packages/device-use/scripts/build-native.ts Outdated
@zvadaadam

Copy link
Copy Markdown
Owner Author

Greenlight — Round 1

Fixed (4): simulator picker ARIA menu semantics, launch error fallback copy, complete inspector socket writes, and universal siminspector architecture validation.

Local validation passed: bun run typecheck, bun run --cwd packages/device-use typecheck, bunx vitest run --config test/vitest.config.ts test/unit/simulator/machine.test.ts, and bun run --cwd packages/device-use build:native.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx (1)

74-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard retry button on a valid callback, not just canRetry.

If canRetry is true while onRetry is missing, the button renders but does nothing.

✅ Minimal fix
-              {canRetry && (
+              {canRetry && typeof onRetry === "function" && (
                 <Button
                   variant="outline"
                   onClick={onRetry}
                   className="min-h-10 min-w-[136px] gap-2 rounded-xl"
                 >
🤖 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 `@apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx` around lines
74 - 83, The retry button currently renders when canRetry is true even if
onRetry is undefined; update the render guard in SimulatorLaunchPreview (the JSX
that includes Button, RotateCcw and "Try Again") to require both canRetry and a
valid callback (e.g., check onRetry is a function or truthy) before rendering,
or alternatively render the Button disabled when onRetry is absent; ensure you
reference the existing onClick={onRetry} handler so the button is only
interactive when onRetry is provided.
🧹 Nitpick comments (1)
apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx (1)

50-85: ⚡ Quick win

Use ts-pattern + .exhaustive() for phase rendering.

This phase union is currently rendered with ad-hoc conditional branches; switching to ts-pattern gives enforced exhaustiveness and prevents silent misses when a new phase is added.

♻️ Suggested refactor
+import { match } from "ts-pattern";
...
-          {phase === "idle" && (
-            <Button
-              onClick={onStart}
-              disabled={!selectedUdid}
-              className="min-h-11 min-w-[180px] gap-2 rounded-xl transition-[background-color,border-color,color,box-shadow] duration-150"
-            >
-              <Play className="h-4 w-4" />
-              Start Simulator
-            </Button>
-          )}
-
-          {phase === "booting" && (
-            <div className="flex flex-col items-center gap-3" aria-live="polite">
-              <Loader2 className="text-primary h-6 w-6 animate-spin" />
-              <p className="text-text-secondary text-sm font-medium">Starting simulator</p>
-            </div>
-          )}
-
-          {phase === "error" && (
-            <div className="flex max-w-[240px] flex-col items-center gap-3" aria-live="polite">
-              <AlertCircle className="text-destructive h-5 w-5" />
-              <p className="text-destructive text-sm leading-5">
-                {errorMessage ?? "Something went wrong starting the simulator."}
-              </p>
-              {canRetry && (
-                <Button
-                  variant="outline"
-                  onClick={onRetry}
-                  className="min-h-10 min-w-[136px] gap-2 rounded-xl"
-                >
-                  <RotateCcw className="h-4 w-4" />
-                  Try Again
-                </Button>
-              )}
-            </div>
-          )}
+          {match(phase)
+            .with("idle", () => (
+              <Button
+                onClick={onStart}
+                disabled={!selectedUdid || typeof onStart !== "function"}
+                className="min-h-11 min-w-[180px] gap-2 rounded-xl transition-[background-color,border-color,color,box-shadow] duration-150"
+              >
+                <Play className="h-4 w-4" />
+                Start Simulator
+              </Button>
+            ))
+            .with("booting", () => (
+              <div className="flex flex-col items-center gap-3" aria-live="polite">
+                <Loader2 className="text-primary h-6 w-6 animate-spin" />
+                <p className="text-text-secondary text-sm font-medium">Starting simulator</p>
+              </div>
+            ))
+            .with("error", () => (
+              <div className="flex max-w-[240px] flex-col items-center gap-3" aria-live="polite">
+                <AlertCircle className="text-destructive h-5 w-5" />
+                <p className="text-destructive text-sm leading-5">
+                  {errorMessage ?? "Something went wrong starting the simulator."}
+                </p>
+                {canRetry && typeof onRetry === "function" && (
+                  <Button
+                    variant="outline"
+                    onClick={onRetry}
+                    className="min-h-10 min-w-[136px] gap-2 rounded-xl"
+                  >
+                    <RotateCcw className="h-4 w-4" />
+                    Try Again
+                  </Button>
+                )}
+              </div>
+            ))
+            .exhaustive()}

As per coding guidelines, **/*.{ts,tsx}: Prefer ts-pattern for discriminated unions. Use .exhaustive() for exhaustiveness checking.

🤖 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 `@apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx` around lines
50 - 85, Replace the ad-hoc conditional JSX branches for the local "phase" union
in SimulatorLaunchPreview with a ts-pattern match to enforce exhaustiveness:
import { match } from 'ts-pattern', then wrap the JSX return for the three cases
by calling match(phase).with('idle', () => /* return the Start Button JSX using
selectedUdid and onStart */).with('booting', () => /* return Loader JSX
*/).with('error', () => /* return Error JSX using errorMessage, canRetry and
onRetry */).exhaustive(); ensure all current props/variables (selectedUdid,
onStart, errorMessage, canRetry, onRetry) are used inside the corresponding arms
and replace the existing conditional blocks with this single match expression.
🤖 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 `@apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx`:
- Around line 51-54: The Start button currently binds onStart directly even
though onStart is optional, allowing a clickable button with no effect; update
the Button in SimulatorLaunchPreview (the element using props onStart and
selectedUdid) to guard against undefined onStart by disabling the button when
onStart is not provided or by wiring a safe caller (e.g., onClick={() =>
onStart?.()}) so clicks become no-ops only when appropriate; ensure the disabled
prop reflects both !selectedUdid and !onStart so the button is not interactable
when there is no handler.

In `@packages/device-use/native/Sources/SimInspector/Inspector.m`:
- Around line 208-221: Check that the request[@"command"] is an NSString before
calling isEqualToString: to avoid unrecognized selector crashes; replace the
current direct use of NSString *command = request[@"command"] and subsequent if
([command isEqualToString:@"..."]) checks with a guarded variant that verifies
[command isKindOfClass:[NSString class]] and returns @{@"ok": `@NO`, @"error":
@"invalid command type"} (or similar) when it's not a string, keeping the
existing handling for @"ping" and @"snapshot" (which calls snapshot_payload())
intact.
- Around line 258-273: The code currently unlinks addr.sun_path unconditionally
which can remove a live inspector socket and cause ownership races; update the
logic in Inspector.m around socket_path(), unlink, bind/listen so that before
unlinking you probe the existing path by creating a temporary AF_UNIX
SOCK_STREAM client socket and attempting to connect to addr.sun_path: if connect
succeeds (i.e., another live owner) do not unlink and abort/return, if connect
fails with ECONNREFUSED or ENOENT proceed to unlink and continue with bind, and
ensure you close the temporary probe socket and check errno for the correct
failure cases (use the same sockaddr_un structure and path.UTF8String for the
probe) so you only remove stale sockets and avoid clobbering live ones.

---

Duplicate comments:
In `@apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx`:
- Around line 74-83: The retry button currently renders when canRetry is true
even if onRetry is undefined; update the render guard in SimulatorLaunchPreview
(the JSX that includes Button, RotateCcw and "Try Again") to require both
canRetry and a valid callback (e.g., check onRetry is a function or truthy)
before rendering, or alternatively render the Button disabled when onRetry is
absent; ensure you reference the existing onClick={onRetry} handler so the
button is only interactive when onRetry is provided.

---

Nitpick comments:
In `@apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx`:
- Around line 50-85: Replace the ad-hoc conditional JSX branches for the local
"phase" union in SimulatorLaunchPreview with a ts-pattern match to enforce
exhaustiveness: import { match } from 'ts-pattern', then wrap the JSX return for
the three cases by calling match(phase).with('idle', () => /* return the Start
Button JSX using selectedUdid and onStart */).with('booting', () => /* return
Loader JSX */).with('error', () => /* return Error JSX using errorMessage,
canRetry and onRetry */).exhaustive(); ensure all current props/variables
(selectedUdid, onStart, errorMessage, canRetry, onRetry) are used inside the
corresponding arms and replace the existing conditional blocks with this single
match expression.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b267b05-5949-431b-a936-7498a359bd41

📥 Commits

Reviewing files that changed from the base of the PR and between 6c554dc and 30479a5.

📒 Files selected for processing (4)
  • apps/web/src/features/simulator/ui/SimulatorContentHeader.tsx
  • apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx
  • packages/device-use/native/Sources/SimInspector/Inspector.m
  • packages/device-use/scripts/build-native.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/features/simulator/ui/SimulatorContentHeader.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/device-use/scripts/build-native.ts

Comment thread apps/web/src/features/simulator/ui/SimulatorLaunchPreview.tsx
Comment thread packages/device-use/native/Sources/SimInspector/Inspector.m Outdated
Comment thread packages/device-use/native/Sources/SimInspector/Inspector.m
@zvadaadam

Copy link
Copy Markdown
Owner Author

Greenlight — Round 2

Fixed (3): disabled no-op Start button states, validated inspector command type, and avoided unlinking live inspector sockets.

Local validation passed: bun run --cwd packages/device-use build:native, bun run typecheck, bun run --cwd packages/device-use typecheck, and bunx vitest run --config test/vitest.config.ts test/unit/simulator/machine.test.ts.

@zvadaadam zvadaadam merged commit 6104d05 into main May 5, 2026
4 checks passed
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