Guides
Dev Client picker + tutorial modal — Coverage Status
Dev Client picker + tutorial modal — Coverage Status
Section titled “Dev Client picker + tutorial modal — Coverage Status”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 packages/rn-dev-agent-core/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 |
What works (verified by harness)
Section titled “What works (verified by harness)”- 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).
What’s broken (verified by harness)
Section titled “What’s broken (verified by harness)”- #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-
- 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.
Fixed in this PR
Section titled “Fixed in this PR”cdp_dismiss_dev_client_picker(Android) — on-demand picker dismissal. New MCP tool wrappinghandleDevClientPicker()through the guardedclearDevClientPickerIfPresent()seam (tools/dev-client-picker.ts).device_deeplinknow best-effort dismisses on Android success. iOS is guarded with an actionable manual-select message (never invokes the legacyagent-device findpath). Cross-platform iOS re-path remains deferred (see Deferred).
Deferred (>50 LOC or new code path)
Section titled “Deferred (>50 LOC or new code path)”- #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
- Cross-platform
cdp_dismiss_dev_client_pickeriOS re-path. The Android path is shipped (PR #136 sub-3). iOS returns an actionable manual-select message today (no legacyagent-device findcall). A native iOS dismiss (XCTest-based, viarn-fast-runner) needs ~50+ LOC of new device verb + error-typing — track as a follow-up. - 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).- Update (#335): the non-perturbing expo-dev-menu bridge now exists.
cdp_dev_settings action:hideDevMenucallsExpoDevMenu.hideMenu()/closeMenu()over CDP (client.evaluate, resolver intools/expo-dev-menu.ts) to dismiss the expo-dev-menu bottom sheet without touch — so it never triggers the touch-induced Hermes detach — andcdp_reloadbest-effort auto-dismisses it on iOS after reconnect. This closes the “dev menu auto-opens on boot” blocker from #335. It targets the dev-menu bottom sheet; whether the samehideMenu()also clears the first-launch tutorial/onboarding modal (gated byEXDevMenuIsOnboardingFinished) is pending on-device confirmation, so this item stays open for the tutorial surface specifically.
- Update (#335): the non-perturbing expo-dev-menu bridge now exists.
- 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.