| title | Dev Client picker + tutorial modal — Coverage Status |
|---|---|
| description | What dev-client-picker.ts currently handles, what it doesn't, and which gaps are tracked for future work. |
Last updated: 2026-05-29 by PR #<PR_NUM> (filled in at PR-open time)
The standalone harness suites at test-app/harness/suites/dev-client-picker.mjs and test-app/harness/suites/expo-tutorial-modal.mjs (workspace repo) reproduce the Expo Dev Client server-picker and first-launch-tutorial states and exercise the handling code paths in scripts/cdp-bridge/src/tools/dev-client-picker.ts. This page enumerates what currently works, what's known-broken, what got fixed in this PR, and what's deferred to future PRs.
Verify pass (DC-Task 8), 2026-05-29, UDID 78F7D2A1 / iOS 26.4, Node v24.13.0:
| Suite | Metro | Result | Key datum |
|---|---|---|---|
dev-client-picker.mjs |
DOWN | pass:true (exit 0) |
cdp_status returned in 10 ms with "Metro not found" — Metro-down path does NOT hang |
expo-tutorial-modal.mjs |
UP | pass:true (exit 0) |
reproduces tutorial-eligible state (bundle loaded, 1 JS target); tutorial text CDP-invisible |
| #136 sub-2 live repro | UP + picker up | hang reproduced | cdp_status took 33,481 ms (~33.5 s) and then returned a misleading ok:true |
- Picker detection (when a device session exists).
handleDevClientPicker()andisDevClientPickerShowing()(dev-client-picker.ts:144,:225) detect the picker bydevice_find-matching the literal textDevelopment servers/DEVELOPMENT SERVERS(PICKER_INDICATORS,dev-client-picker.ts:45).parseFirstServerEntry()(:120) then selects a server row to tap via a three-tier matcher: whole-line literal IP →host:portport-pattern (:76) → first non-header/non-footer row. Unit-test-covered via therunAgentDevicetest seam. - Auto-advance race handling.
dismissPicker()(:169) re-probes withisDevClientPickerShowing()before tapping and returns success if the single-server picker auto-advanced between detect and dispatch (:176), closing the ~30% Maestro "unable to find element" race from #136. - Metro-DOWN fast path (no hang). With Metro down,
cdp_status→autoConnect→discover()short-circuits at Metro discovery (discovery.ts:314,DISCOVERY_TIMEOUT_MS = 1500) and returns "Metro not found" in ~10 ms. Verified live by the picker suite (statusMs:10). The #136 sub-2 hang is exclusively a Metro-UP phenomenon. - CDP is non-perturbing to both surfaces.
cdp_status/cdp_targets/cdp_connectare pure Metro-WebSocket discovery + JS introspection — they never drive the device, so they never dismiss the picker or the tutorial (verified across repeated probes in both suites).
- #136 sub-2:
cdp_statushangs ~33.5 s when the picker is up + Metro is up. Reproduced live (DC-Task 8):statusMs = 33,481. Root cause is a chain, not a single line:- The pre-
autoConnectpicker guard instatus.ts:128-132callsisDevClientPickerShowing(), which is gated onhasActiveSession()(dev-client-picker.ts:226). On an MCP cold start there is no active device session, so the guard is a no-op and execution falls through toautoConnect(status.ts:133). - With the picker up, Metro's
/json/liststill advertises stale C++ targets from the prior bundle session (React Native Bridgeless [C++ connection],Reanimated UI runtime [C++ connection],vm:None). These passfilterValidTargetsbecause theirtitle/descriptioninclude "React Native" (discovery.ts:44-47), sodiscover()does NOT throw the fast "No Hermes debug target found" (discovery.ts:332). connectToTarget(connect.ts:215) opens the WebSocket and theRuntime.evaluate('1+1')pre-flight probe passes (the C++ connection answers even with no live JS app context), so it proceeds toctx.setup()(connect.ts:254).- Dominant cost:
performSetupcallswaitForReact(evaluate, REACT_READY_TIMEOUT_MS)(setup.ts:67,REACT_READY_TIMEOUT_MS = 30_000). The picker is blocking the bundle, so React never becomes ready and the poll loop (setup.ts:174-192) burns its full 30 s before loggingReact not ready after 30000ms — helpers will be injected anywayand continuing. ~3.5 s of discovery/connect/probe overhead on top yields the observed ~33.5 s. - Worse than slow: the call ultimately returns
ok:true(status.app.dev:true) rather than surfacing that the picker is blocking the bundle — the user gets a misleading slow "success", not an actionable message. Exposed by the Metro-UP live repro (thedev-client-picker.mjssuite runs Metro-DOWN by design and recordsstatusHung:falsehonestly).
- The pre-
- No MCP-exposed picker-dismiss tool.
handleDevClientPicker()is internal-only — invoked fromstatus.ts(:130,:240),device-reset-state.ts, andstartup-replay.ts, never registered as an MCP tool. An agent cannot dismiss the picker on demand; the only path is the device-session-gated, iOS-perturbing internal call. Both suites recorddismissTool:null/handlerToolName:null. - Tutorial modal is CDP-invisible and unhandled. The expo-dev-menu first-launch tutorial ("This is the developer menu…" + Continue) renders in expo-dev-menu's separate native RN surface, not the app's Hermes context.
__RN_AGENT.getTree()andcdp_component_treereturn only the app tree (no "developer menu"/"Continue");device_snapshotyields no a11y tree in this env (globalagent-devicedaemon interceptsaction=open). There is no non-perturbing programmatic detector for the tutorial text and no dismiss handler —handleDevClientPicker()targets the SERVER picker only. Verified byexpo-tutorial-modal.mjs.
None. The #136 sub-2 hang root cause was diagnosed and reproduced live, but a correct fix exceeds the in-scope budget — see Deferred. No shipped code was changed in this PR (coverage doc only).
- #136 sub-2 hang — fast-fail / clear message when the picker blocks the bundle (Metro-UP). Deferred: no safe ≤50 LOC fix exists in
dev-client-picker.ts/status.ts.- The dominant cost is the 30 s
waitForReactinperformSetup(setup.ts:67), which is shared by every connect path (cold start, reload, soft-reconnect). Shortening it globally would regress legitimate slow first-builds. Threading a status-specific shorter timeout down throughautoConnect → discoverAndConnect → connectToTarget → setup() → performSetuptouches 4-5 files plus theConnectContextinterface — well over 50 LOC of shared connect plumbing. - A pre-
autoConnectpicker detector based on target shape is not reliable: live experiment (DC-Task 8) showed/json/listreturns 0 Hermes targets transiently during every normal bundle reload, not only when the picker is up — and the stale C++ targets passfilterValidTargets. Such a heuristic would false-positive on healthy connects (the false-positive risk the code-quality review already flagged on thepickerDetectedgate). - Proposed follow-up approach (for the issue): in
connectToTarget, after the1+1pre-flight probe succeeds but beforesetup(), run a short bounded "is React reachable?" probe (e.g.waitForReactwith a ~3-5 s budget on the status-initiated connect only) and, on timeout against a target whosevmis notHermes, abort that candidate with a typedPickerBlockingBundleError;status.tsmaps it to a fastfailResult/warnResultlike "Dev Client picker is blocking the bundle — select your Metro server on the simulator, then retry cdp_status." Needs a new error type + a status-vs-other connect-intent flag threaded through the connect chain; estimated >50 LOC acrossconnect.ts/setup.ts/status.ts/cdp-client.ts. Track as a dedicated #136 sub-2 follow-up issue.
- The dominant cost is the 30 s
- No MCP-exposed picker-dismiss tool. Deferred: registering a new
cdp_dismiss_dev_client_pickerMCP tool is a new code path (tool schema +trackedToolregistration + iOS-perturbation handling). Track as a follow-up if agents need on-demand dismissal beyond the internalcdp_status/device_reset_state/startup-replaycall sites. - Tutorial-modal detection + dismissal. Deferred: requires either OCR over
xcrun simctl io screenshot(no programmatic CDP/a11y signal exists for expo-dev-menu's separate surface) or a non-perturbing native bridge into expo-dev-menu — both are new code paths well beyond the budget. Track as a follow-up (relates to #173 sub-4). - CI workflow for the harness suites (planned DC-Task 9). Deferred: the planned
macos-15workflow runs a ~20 min full native build (expo prebuild+expo run:ios) + Metro to drive both suites, but (a) the two suites have opposite Metro requirements — the picker suite needs Metro DOWN, the tutorial suite needs Metro UP — which the single-Metro-start workflow draft does not satisfy, and (b) both suites are reproduction fixtures with intentionally weak assertions (see "documents the gap" above) guarding a bug that is itself deferred-unfixed. A heavy, flaky-prone CI job guarding weak-signal reproduction of an unfixed bug is low ROI today. Revisit when #136 sub-2 is fixed — at that point CI can guard the fix (an asserting suite), and the workflow must run the picker suite Metro-DOWN, then start Metro, then run the tutorial suite Metro-UP. Suites remain runnable on-demand locally meanwhile.