react-native-ai-debugger 1.10.8 → 1.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/build/__tests__/setup.js +11 -5
  2. package/build/__tests__/setup.js.map +1 -1
  3. package/build/__tests__/unit/iconSemantics.test.d.ts +2 -0
  4. package/build/__tests__/unit/iconSemantics.test.d.ts.map +1 -0
  5. package/build/__tests__/unit/iconSemantics.test.js +80 -0
  6. package/build/__tests__/unit/iconSemantics.test.js.map +1 -0
  7. package/build/__tests__/unit/screenState.test.d.ts +2 -0
  8. package/build/__tests__/unit/screenState.test.d.ts.map +1 -0
  9. package/build/__tests__/unit/screenState.test.js +186 -0
  10. package/build/__tests__/unit/screenState.test.js.map +1 -0
  11. package/build/core/executor.d.ts +1 -0
  12. package/build/core/executor.d.ts.map +1 -1
  13. package/build/core/executor.js +1 -0
  14. package/build/core/executor.js.map +1 -1
  15. package/build/core/iconSemantics.d.ts +18 -0
  16. package/build/core/iconSemantics.d.ts.map +1 -0
  17. package/build/core/iconSemantics.js +114 -0
  18. package/build/core/iconSemantics.js.map +1 -0
  19. package/build/core/index.d.ts +3 -1
  20. package/build/core/index.d.ts.map +1 -1
  21. package/build/core/index.js +3 -1
  22. package/build/core/index.js.map +1 -1
  23. package/build/core/pressables.d.ts +2 -0
  24. package/build/core/pressables.d.ts.map +1 -1
  25. package/build/core/pressables.js +222 -18
  26. package/build/core/pressables.js.map +1 -1
  27. package/build/core/screenState.d.ts +98 -0
  28. package/build/core/screenState.d.ts.map +1 -0
  29. package/build/core/screenState.js +1120 -0
  30. package/build/core/screenState.js.map +1 -0
  31. package/build/tools/componentTools.d.ts.map +1 -1
  32. package/build/tools/componentTools.js +48 -5
  33. package/build/tools/componentTools.js.map +1 -1
  34. package/build/tools/screenshotTools.d.ts.map +1 -1
  35. package/build/tools/screenshotTools.js +105 -40
  36. package/build/tools/screenshotTools.js.map +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1,1120 @@
1
+ import { executeInApp, delay } from "./jsExecute.js";
2
+ import { iconSemanticHint } from "./iconSemantics.js";
3
+ // ============================================================================
4
+ // Pure helpers (exported for unit tests)
5
+ // ============================================================================
6
+ export function markPressablesCoveredByOverlay(pressables, overlayBounds) {
7
+ for (const p of pressables) {
8
+ const b = p.bounds;
9
+ const fullyCovered = b.x >= overlayBounds.x &&
10
+ b.y >= overlayBounds.y &&
11
+ b.x + b.width <= overlayBounds.x + overlayBounds.width &&
12
+ b.y + b.height <= overlayBounds.y + overlayBounds.height;
13
+ if (fullyCovered)
14
+ p.blockedByOverlay = true;
15
+ }
16
+ return pressables;
17
+ }
18
+ /**
19
+ * Replace component-name fallback labels with semantic icon labels when the
20
+ * pressable's icon child name carries recognizable semantics:
21
+ * { label: "[FloatingHeader]", icon: "SvgChevronBackward" }
22
+ * → label "[SvgChevronBackward — possibly back button]"
23
+ * Labels from text/a11y are never touched (icon is null for those).
24
+ */
25
+ export function applyIconHintToLabel(p) {
26
+ if (!p.icon)
27
+ return p;
28
+ const hint = iconSemanticHint(p.icon);
29
+ if (hint)
30
+ p.label = `[${p.icon} — ${hint}]`;
31
+ else if (!p.label)
32
+ p.label = `[${p.icon}]`;
33
+ return p;
34
+ }
35
+ const identityCoords = (p) => ({ center: p.center, frame: p.bounds });
36
+ /**
37
+ * Render a ScreenState as the compact orientation summary used by
38
+ * get_screen_state and the screenshot tools:
39
+ * 📍 Checkout [Tabs > Cart > Checkout]
40
+ * 🎯 Pressables:
41
+ * (40, 100) <FloatingHeader /> "[SvgChevronBackward — possibly back button]" frame:(16,76 48x48)
42
+ */
43
+ export function formatScreenStateSummary(ss, convert = identityCoords) {
44
+ const lines = [];
45
+ if (ss.route) {
46
+ lines.push(`📍 Currently focused screen: "${ss.route.name}" [navigation stack: ${ss.route.stack.join(" > ")}]`);
47
+ if (ss.route.params && Object.keys(ss.route.params).length > 0) {
48
+ const params = JSON.stringify(ss.route.params);
49
+ lines.push(` route params: ${params.length > 600 ? params.slice(0, 600) + "…" : params}`);
50
+ }
51
+ }
52
+ else {
53
+ lines.push("📍 Currently focused screen: unknown (no React Navigation / Expo Router detected)");
54
+ }
55
+ const formatPressable = (p) => {
56
+ const { center, frame } = convert(p);
57
+ return ` (${center.x}, ${center.y})${p.component ? ` <${p.component} />` : ""} ${p.label ? `"${p.label}"` : "(unlabeled)"}` +
58
+ `${p.nearbyText ? ` near "${p.nearbyText}"` : ""}${p.onPressHint ? ` ${p.onPressHint}` : ""}` +
59
+ `${p.testID ? ` testID="${p.testID}"` : ""}${p.isInput ? " [input]" : ""}` +
60
+ ` frame:(${frame.x},${frame.y} ${frame.width}x${frame.height})`;
61
+ };
62
+ if (ss.overlays.length > 0) {
63
+ for (const overlay of ss.overlays) {
64
+ lines.push(`\n🔲 ${overlay.type}${overlay.title ? ` — "${overlay.title}"` : ""}:`);
65
+ if (overlay.pressables.length > 0) {
66
+ overlay.pressables.forEach((p) => lines.push(formatPressable(p)));
67
+ }
68
+ else {
69
+ lines.push(" (no pressables)");
70
+ }
71
+ }
72
+ const reachable = ss.pressables.filter((p) => !p.blockedByOverlay);
73
+ const blocked = ss.pressables.filter((p) => p.blockedByOverlay);
74
+ lines.push(`\n🎯 Root pressables: ${reachable.length > 0 ? "" : "(none reachable)"}`);
75
+ reachable.forEach((p) => lines.push(formatPressable(p)));
76
+ if (blocked.length > 0) {
77
+ lines.push(`\n🚫 Blocked by overlay (visible on the underlying screen but taps will NOT reach them until the overlay closes):`);
78
+ blocked.forEach((p) => lines.push(formatPressable(p)));
79
+ }
80
+ }
81
+ else {
82
+ lines.push("\n🎯 Pressables:");
83
+ if (ss.pressables.length > 0) {
84
+ ss.pressables.forEach((p) => lines.push(formatPressable(p)));
85
+ }
86
+ else {
87
+ lines.push(" (none)");
88
+ }
89
+ }
90
+ return lines.join("\n");
91
+ }
92
+ /**
93
+ * Turn raw onPress handler info ({ n: fn.name, s: source head }) into a
94
+ * displayable hint, or null when nothing meaningful survives:
95
+ * - real names ("handleSubmit", "bound goBack") → "handleSubmit()" / "goBack()"
96
+ * - generic/minified names ("onPress", "anonymous", "t12") are rejected
97
+ * - anonymous with retained source → "{() => setAccepted(prev => !prev)…}"
98
+ * - Hermes bytecode bundles (source = "[bytecode]", stripped in-app) → null
99
+ */
100
+ const GENERIC_HANDLER_NAMES = new Set(["", "anonymous", "onpress", "onclick", "handler", "callback", "fn", "press", "value"]);
101
+ function meaningfulHandlerName(n) {
102
+ let name = (n || "").trim();
103
+ if (name.startsWith("bound "))
104
+ name = name.slice(6).trim();
105
+ if (!name || GENERIC_HANDLER_NAMES.has(name.toLowerCase()))
106
+ return null;
107
+ if (/^[a-zA-Z_$]\d{1,4}$/.test(name) || name.length === 1)
108
+ return null; // minified (t12, e)
109
+ return name;
110
+ }
111
+ export function describePressHandler(raw) {
112
+ if (!raw || typeof raw !== "object")
113
+ return null;
114
+ const { n, s } = raw;
115
+ const name = meaningfulHandlerName(n);
116
+ if (name)
117
+ return `${name}()`;
118
+ const src = (s || "").trim();
119
+ if (src)
120
+ return `{${src.length > 70 ? src.slice(0, 70) + "…" : src}}`;
121
+ return null;
122
+ }
123
+ /**
124
+ * Fallback handler context from the custom component's on* props, for when the
125
+ * direct onPress is anonymous (Hermes discards source even in dev bundles, and
126
+ * names like navigation.goBack are lost to computed assignment):
127
+ * - prefer the prop whose value IS the touchable's onPress (pass-through), else
128
+ * the only on* function prop; ambiguity (several candidates) yields null
129
+ * - named fn → "onBack=goBack()"; nameless → "onPress→onBack" (the prop name
130
+ * alone is greppable context); a bare nameless "onPress" prop adds nothing → null
131
+ */
132
+ export function describePropHandlers(raw) {
133
+ if (!Array.isArray(raw) || raw.length === 0)
134
+ return null;
135
+ const entries = raw.filter((e) => !!e && typeof e === "object" && typeof e.p === "string");
136
+ const pick = entries.find((e) => e.same) ?? (entries.length === 1 ? entries[0] : null);
137
+ if (!pick)
138
+ return null;
139
+ const name = meaningfulHandlerName(pick.n);
140
+ if (name)
141
+ return `${pick.p}=${name}()`;
142
+ if (pick.p.toLowerCase() === "onpress")
143
+ return null;
144
+ return `onPress→${pick.p}`;
145
+ }
146
+ export function parseScreenStateResponse(raw) {
147
+ if (!raw || typeof raw !== "object")
148
+ return null;
149
+ const r = raw;
150
+ if (r.error)
151
+ return null;
152
+ return {
153
+ route: r.route ?? null,
154
+ overlays: r.overlays ?? [],
155
+ pressables: r.pressables ?? [],
156
+ };
157
+ }
158
+ // ============================================================================
159
+ // Main function (dispatch phase — Task 3; resolve phase added in Task 4)
160
+ // ============================================================================
161
+ export async function getScreenState(options = {}) {
162
+ const { device } = options;
163
+ const dispatchExpression = `
164
+ (function() {
165
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
166
+ if (!hook) return { error: 'React DevTools hook not found.' };
167
+
168
+ var roots = [];
169
+ if (hook.getFiberRoots) roots = Array.from(hook.getFiberRoots(1) || []);
170
+ if (roots.length === 0 && hook.renderers) {
171
+ for (var entry of hook.renderers) {
172
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
173
+ if (r.length > 0) { roots = r; break; }
174
+ }
175
+ }
176
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
177
+
178
+ // ------------------------------------------------------------------
179
+ // Shared utilities
180
+ // ------------------------------------------------------------------
181
+
182
+ function getComponentName(fiber) {
183
+ if (!fiber || !fiber.type) return null;
184
+ if (typeof fiber.type === 'string') return fiber.type;
185
+ return fiber.type.displayName || fiber.type.name || null;
186
+ }
187
+
188
+ function getMeasurable(fiber) {
189
+ var sn = fiber.stateNode;
190
+ if (!sn) return null;
191
+ if (typeof sn.measureInWindow === 'function') return sn;
192
+ if (sn.canonical && sn.canonical.publicInstance &&
193
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
194
+ return sn.canonical.publicInstance;
195
+ }
196
+ if (sn.node && globalThis.nativeFabricUIManager &&
197
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
198
+ var node = sn.node;
199
+ return {
200
+ measureInWindow: function(cb) {
201
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
202
+ }
203
+ };
204
+ }
205
+ return null;
206
+ }
207
+
208
+ function findFirstHost(fiber, depth) {
209
+ if (!fiber || depth > 20) return null;
210
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
211
+ var child = fiber.child;
212
+ while (child) {
213
+ var found = findFirstHost(child, depth + 1);
214
+ if (found) return found;
215
+ child = child.sibling;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ function findHostsInSubtree(fiber, depth, hosts, limit) {
221
+ if (!fiber || depth > 20 || hosts.length >= limit) return;
222
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
223
+ hosts.push(fiber);
224
+ return;
225
+ }
226
+ var child = fiber.child;
227
+ while (child && hosts.length < limit) {
228
+ findHostsInSubtree(child, depth + 1, hosts, limit);
229
+ child = child.sibling;
230
+ }
231
+ }
232
+
233
+ function collectText(fiber, d) {
234
+ if (!fiber || d > 20) return '';
235
+ var props = fiber.memoizedProps;
236
+ // Host text fibers carry the raw string as memoizedProps — needed when a
237
+ // mixed-children Text falls through to the fiber-child walk below.
238
+ if (typeof props === 'string') return props;
239
+ if (typeof props === 'number') return String(props);
240
+ if (props) {
241
+ var ch = props.children;
242
+ if (typeof ch === 'string') return ch;
243
+ if (typeof ch === 'number') return String(ch);
244
+ if (Array.isArray(ch)) {
245
+ var inline = [];
246
+ var hasElement = false;
247
+ for (var ci = 0; ci < ch.length; ci++) {
248
+ if (typeof ch[ci] === 'string') inline.push(ch[ci]);
249
+ else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
250
+ else if (ch[ci] != null && ch[ci] !== false) hasElement = true;
251
+ }
252
+ // Mixed content ("Create " + <Text>2 digital items</Text>): fall through
253
+ // to the fiber-child walk so nested Text elements are included; the host
254
+ // text fibers re-supply the inline strings, so nothing is lost or doubled.
255
+ if (inline.length > 0 && !hasElement) return inline.join('');
256
+ }
257
+ }
258
+ var parts = [];
259
+ var child = fiber.child;
260
+ while (child) {
261
+ var t = collectText(child, d + 1);
262
+ if (t) parts.push(t);
263
+ child = child.sibling;
264
+ }
265
+ return parts.join(' ').replace(/\\s+/g, ' ').trim();
266
+ }
267
+
268
+ // ------------------------------------------------------------------
269
+ // 1. Route detection
270
+ // ------------------------------------------------------------------
271
+
272
+ var route = null;
273
+
274
+ // SDK navigation ref (highest priority — works even when NavigationContainer is wrapped)
275
+ try {
276
+ var sdkNav = globalThis.__RN_AI_DEVTOOLS__ && globalThis.__RN_AI_DEVTOOLS__.navigation;
277
+ if (sdkNav && typeof sdkNav.getCurrentRoute === 'function') {
278
+ var currentRoute = sdkNav.getCurrentRoute();
279
+ var rootState = typeof sdkNav.getRootState === 'function' ? sdkNav.getRootState() : null;
280
+ if (currentRoute && currentRoute.name) {
281
+ var sdkStack = [];
282
+ if (rootState && rootState.routes) {
283
+ (function collectStack(state) {
284
+ if (!state || !state.routes) return;
285
+ var idx = typeof state.index === 'number' ? state.index : state.routes.length - 1;
286
+ if (state.type === 'stack') {
287
+ // show full stack history for stack navigators
288
+ for (var i = 0; i < state.routes.length; i++) {
289
+ sdkStack.push(state.routes[i].name || state.routes[i].key || 'unknown');
290
+ }
291
+ var focused = state.routes[idx];
292
+ if (focused && focused.state) collectStack(focused.state);
293
+ } else {
294
+ // for tab/drawer just follow the focused screen
295
+ var focused = state.routes[idx];
296
+ if (focused) {
297
+ sdkStack.push(focused.name || focused.key || 'unknown');
298
+ if (focused.state) collectStack(focused.state);
299
+ }
300
+ }
301
+ })(rootState);
302
+ }
303
+ if (sdkStack.length === 0) sdkStack.push(currentRoute.name);
304
+ route = {
305
+ name: currentRoute.name,
306
+ params: currentRoute.params || null,
307
+ stack: sdkStack
308
+ };
309
+ }
310
+ }
311
+ } catch(e) {}
312
+
313
+ if (!route) try {
314
+ // React Navigation v5+: walk fiber tree for NavigationContainer ref
315
+ function findNavState(fiber, depth) {
316
+ if (!fiber || depth > 200) return null;
317
+ var name = getComponentName(fiber);
318
+ // NavigationContainer stores ref on stateNode; check state
319
+ if (name === 'NavigationContainer' || name === 'BaseNavigationContainer') {
320
+ var sn = fiber.stateNode;
321
+ if (sn && typeof sn.getRootState === 'function') {
322
+ return sn.getRootState();
323
+ }
324
+ // Hook-based: memoizedState chain
325
+ var mState = fiber.memoizedState;
326
+ while (mState) {
327
+ if (mState.memoizedState && mState.memoizedState.routes) {
328
+ return mState.memoizedState;
329
+ }
330
+ mState = mState.next;
331
+ }
332
+ }
333
+ var child = fiber.child;
334
+ while (child) {
335
+ var r = findNavState(child, depth + 1);
336
+ if (r) return r;
337
+ child = child.sibling;
338
+ }
339
+ return null;
340
+ }
341
+
342
+ // Also try __reactNavigationContainerRef global
343
+ var navState = null;
344
+ if (globalThis.__reactNavigationContainerRef && globalThis.__reactNavigationContainerRef.current) {
345
+ var ref = globalThis.__reactNavigationContainerRef.current;
346
+ if (typeof ref.getState === 'function') navState = ref.getState();
347
+ }
348
+ if (!navState) navState = findNavState(roots[0].current, 0);
349
+
350
+ // Fallback: scan all fibers for memoizedState with navigation shape
351
+ // (catches anonymous BaseNavigationContainer when displayName/name is null)
352
+ if (!navState) {
353
+ function isNavState(v) {
354
+ return v && typeof v === 'object' && Array.isArray(v.routes) && v.routes.length > 0
355
+ && typeof v.routes[0].name === 'string' && typeof v.index === 'number';
356
+ }
357
+ function findNavStateByShape(fiber, depth) {
358
+ if (!fiber || depth > 400) return null;
359
+ var s = fiber.memoizedState;
360
+ var checked = 0;
361
+ while (s && checked < 20) {
362
+ if (isNavState(s.memoizedState)) return s.memoizedState;
363
+ s = s.next;
364
+ checked++;
365
+ }
366
+ var result = findNavStateByShape(fiber.child, depth + 1);
367
+ if (result) return result;
368
+ return findNavStateByShape(fiber.sibling, depth + 1);
369
+ }
370
+ navState = findNavStateByShape(roots[0].current, 0);
371
+ }
372
+
373
+ if (navState && navState.routes) {
374
+ var fiberStack = [];
375
+ var fiberLeafParams = null;
376
+ (function collectFiberStack(state) {
377
+ if (!state || !state.routes || state.routes.length === 0) return;
378
+ var idx = (typeof state.index === 'number') ? state.index : state.routes.length - 1;
379
+ if (state.type === 'stack' || !state.type) {
380
+ for (var i = 0; i < state.routes.length; i++) {
381
+ fiberStack.push(state.routes[i].name || state.routes[i].key || 'unknown');
382
+ }
383
+ var focused = state.routes[idx];
384
+ if (focused) {
385
+ fiberLeafParams = focused.params || null;
386
+ if (focused.state) collectFiberStack(focused.state);
387
+ }
388
+ } else {
389
+ var focused = state.routes[idx];
390
+ if (focused) {
391
+ fiberStack.push(focused.name || focused.key || 'unknown');
392
+ fiberLeafParams = focused.params || null;
393
+ if (focused.state) collectFiberStack(focused.state);
394
+ }
395
+ }
396
+ })(navState);
397
+ if (fiberStack.length > 0) {
398
+ route = {
399
+ name: fiberStack[fiberStack.length - 1],
400
+ params: fiberLeafParams,
401
+ stack: fiberStack
402
+ };
403
+ }
404
+ }
405
+ } catch(e) {}
406
+
407
+ // ------------------------------------------------------------------
408
+ // 2. Overlay detection — build bounds for each overlay
409
+ // ------------------------------------------------------------------
410
+
411
+ var OVERLAY_NAMES = /^(Modal|BottomSheet|BottomSheetModal|BottomSheetView|ActionSheet|Alert)$/;
412
+ var overlayFiberMeta = []; // { type, fiberRoot, hostFibers }
413
+
414
+ function classifyOverlay(name) {
415
+ if (name === 'Modal') return 'Modal';
416
+ if (name === 'Alert') return 'Alert';
417
+ if (/ActionSheet/i.test(name)) return 'ActionSheet';
418
+ if (/BottomSheet/i.test(name)) return 'BottomSheet';
419
+ return 'Unknown';
420
+ }
421
+
422
+ // Walk to find viewport dimensions (reused for heuristic overlay detection)
423
+ var viewportW = 9999, viewportH = 9999;
424
+ var rootHostFiber = findFirstHost(roots[0].current, 0);
425
+
426
+ function walkForOverlays(fiber, depth) {
427
+ if (!fiber || depth > 5000) return;
428
+ var name = getComponentName(fiber);
429
+ var props = fiber.memoizedProps || {};
430
+
431
+ if (name && OVERLAY_NAMES.test(name)) {
432
+ // Collect host fibers in this overlay subtree
433
+ var hosts = [];
434
+ findHostsInSubtree(fiber, 0, hosts, 64);
435
+ if (hosts.length > 0) {
436
+ overlayFiberMeta.push({ type: classifyOverlay(name), fiber: fiber, hostFibers: hosts });
437
+ }
438
+ // Don't recurse deeper — nested overlays are unusual
439
+ return;
440
+ }
441
+
442
+ // Heuristic: absolute-positioned node with high zIndex covering > 40% screen
443
+ // Only fires if it has at least one pressable child
444
+ if (typeof fiber.type === 'string') {
445
+ var style = props.style;
446
+ if (style && typeof style === 'object' && !Array.isArray(style)) {
447
+ if ((style.zIndex > 999 || style.position === 'absolute') &&
448
+ typeof style.width === 'number' && typeof style.height === 'number') {
449
+ if (viewportW < 9999) {
450
+ var area = style.width * style.height;
451
+ var vArea = viewportW * viewportH;
452
+ if (area > vArea * 0.4) {
453
+ // Check if it has a pressable child
454
+ var hasPressable = false;
455
+ (function checkPress(f, d) {
456
+ if (!f || d > 10 || hasPressable) return;
457
+ if (f.memoizedProps && typeof f.memoizedProps.onPress === 'function') { hasPressable = true; return; }
458
+ checkPress(f.child, d + 1);
459
+ if (!hasPressable) checkPress(f.sibling, d);
460
+ })(fiber.child, 0);
461
+ if (hasPressable) {
462
+ var hosts2 = [];
463
+ findHostsInSubtree(fiber, 0, hosts2, 64);
464
+ if (hosts2.length > 0) {
465
+ overlayFiberMeta.push({ type: 'Unknown', fiber: fiber, hostFibers: hosts2 });
466
+ }
467
+ return;
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ var child = fiber.child;
476
+ while (child) {
477
+ walkForOverlays(child, depth + 1);
478
+ child = child.sibling;
479
+ }
480
+ }
481
+ walkForOverlays(roots[0].current, 0);
482
+
483
+ // ------------------------------------------------------------------
484
+ // 3. Pressable extraction — reuse PressabilityDebugView logic
485
+ // (same logic as get_pressable_elements; inline to avoid second CDP call)
486
+ // ------------------------------------------------------------------
487
+
488
+ var hostFibers = [];
489
+ var fiberMeta = [];
490
+
491
+ var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|ScrollViewContext(Base)?|VirtualizedListContext(Resetter)?|TextInputContext|KeyboardAvoidingViewContext|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
492
+ var GENERIC_COMPONENT = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText|Unknown)$/;
493
+ var PDV_OWNER_COMPONENT = /^(Pressable|Touchable(Opacity|Highlight|WithoutFeedback|NativeFeedback|Bounce))$/;
494
+
495
+ var PAGE_COMPONENT = /^(.*Screen|.*Page|.*View$|.*Container$|.*Layout$|.*Root$|ExpoRoot|App$)/;
496
+
497
+ // Layout-only and touch-wrapper components skipped when scanning a pressable's
498
+ // children for a meaningful icon component name (e.g. SvgChevronBack).
499
+ var SKIP_IN_CHILD_SCAN = /^(View|Text|Image|ImageBackground|ScrollView|FlatList|SectionList|KeyboardAvoidingView|SafeAreaView|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|TouchableNativeFeedback|Pressable|TextInput|ActivityIndicator|Switch|Modal|StatusBar|VirtualizedList|RefreshControl|Animated\\(.*|withAnimated.*|AnimatedComponent.*)$/;
500
+
501
+ function findMeaningfulChildName(fiber) {
502
+ function scan(f, d) {
503
+ if (!f || d > 12) return null;
504
+ var n = getComponentName(f);
505
+ if (n && typeof f.type !== 'string' && !RN_PRIMITIVES.test(n) && !SKIP_IN_CHILD_SCAN.test(n)) return n;
506
+ var c = f.child;
507
+ while (c) {
508
+ var r = scan(c, d + 1);
509
+ if (r) return r;
510
+ c = c.sibling;
511
+ }
512
+ return null;
513
+ }
514
+ return scan(fiber.child, 0);
515
+ }
516
+
517
+ function findMeaningfulAncestorName(fiber) {
518
+ var cur = fiber.return;
519
+ var depth = 0;
520
+ var best = null;
521
+ while (cur && depth < 20) {
522
+ var n = getComponentName(cur);
523
+ if (n && typeof cur.type !== 'string' && !RN_PRIMITIVES.test(n) && !GENERIC_COMPONENT.test(n)) {
524
+ if (PAGE_COMPONENT.test(n)) break; // stop — too high, not useful
525
+ best = n;
526
+ break;
527
+ }
528
+ cur = cur.return;
529
+ depth++;
530
+ }
531
+ return best;
532
+ }
533
+
534
+ // Nearest custom component FIBER for a pressable — its name is what an agent
535
+ // can grep for in the codebase (OrderStepRow, CheckBox, Button). Own fiber when
536
+ // the pressable itself is custom, else a capped climb over composite ancestors
537
+ // (mirrors get_pressable_elements: stopping at the first composite is wrong,
538
+ // generic wrappers like ForwardRef(View) sit in between).
539
+ function resolveComponentFiber(fiber) {
540
+ var ownName = getComponentName(fiber);
541
+ if (ownName && typeof fiber.type !== 'string' && !GENERIC_COMPONENT.test(ownName) && !RN_PRIMITIVES.test(ownName)) return fiber;
542
+ var an = fiber.return;
543
+ var composites = 0;
544
+ var dep = 0;
545
+ while (an && dep < 12 && composites < 4) {
546
+ if (typeof an.type !== 'string' && an.type !== null) {
547
+ var n = getComponentName(an);
548
+ if (n) {
549
+ composites++;
550
+ if (!RN_PRIMITIVES.test(n) && !GENERIC_COMPONENT.test(n)) return an;
551
+ }
552
+ }
553
+ an = an.return;
554
+ dep++;
555
+ }
556
+ return null;
557
+ }
558
+
559
+ function resolveComponentName(fiber) {
560
+ var cf = resolveComponentFiber(fiber);
561
+ return cf ? getComponentName(cf) : null;
562
+ }
563
+
564
+ // Event-handler props (onBack, onSelect, ...) of the custom component — context
565
+ // for what the press triggers when the handler itself is anonymous. 'same' marks
566
+ // identity with the touchable's onPress (pass-through props like onPress={onBack}),
567
+ // which is the strongest signal of which prop the handler arrived through.
568
+ function collectPropHandlers(fiber, directFn) {
569
+ var cf = resolveComponentFiber(fiber);
570
+ if (!cf || !cf.memoizedProps) return null;
571
+ var ps = cf.memoizedProps;
572
+ var out = [];
573
+ for (var k in ps) {
574
+ if (out.length >= 6) break;
575
+ if (!/^on[A-Z]/.test(k)) continue;
576
+ if (typeof ps[k] !== 'function') continue;
577
+ out.push({ p: k, n: ps[k].name || '', same: directFn ? ps[k] === directFn : false });
578
+ }
579
+ return out.length ? out : null;
580
+ }
581
+
582
+ // Raw onPress handler info — name + source head. Whether it's displayable is
583
+ // decided on the TS side (describePressHandler): Hermes bytecode bundles yield
584
+ // minified names and '[bytecode]' source, which are filtered out there.
585
+ function handlerHint(fn) {
586
+ try {
587
+ var n = fn.name || '';
588
+ var s = '';
589
+ try { s = String(fn); } catch(e2) {}
590
+ if (s.indexOf('[bytecode]') !== -1 || s.indexOf('[native code]') !== -1) s = '';
591
+ return { n: n, s: s ? s.replace(/\\s+/g, ' ').slice(0, 160) : '' };
592
+ } catch(e) { return null; }
593
+ }
594
+
595
+ var ROLE_LABELS = {
596
+ checkbox: 'Checkbox', switch: 'Switch', radio: 'Radio', button: 'Button',
597
+ image: 'Image', imagebutton: 'Image Button', link: 'Link', menuitem: 'Menu Item',
598
+ tab: 'Tab', togglebutton: 'Toggle Button', spinbutton: 'Spin Button'
599
+ };
600
+
601
+ function resolveLabel(primaryFiber, hostFiber, baseLabel, baseTestID) {
602
+ if (baseLabel) return baseLabel;
603
+ // accessibilityRole fallback
604
+ var pProps = primaryFiber ? (primaryFiber.memoizedProps || {}) : {};
605
+ var hProps = hostFiber ? (hostFiber.memoizedProps || {}) : {};
606
+ var role = (hProps.accessibilityRole || pProps.accessibilityRole || hProps.role || pProps.role || '').toLowerCase();
607
+ if (role && ROLE_LABELS[role]) {
608
+ var state = hProps.accessibilityState || pProps.accessibilityState || {};
609
+ var stateStr = '';
610
+ if (state.checked === true) stateStr = ': checked';
611
+ else if (state.checked === false) stateStr = ': unchecked';
612
+ else if (state.selected === true) stateStr = ': selected';
613
+ return '[' + ROLE_LABELS[role] + stateStr + ']';
614
+ }
615
+ // Meaningful ancestor component name fallback
616
+ var ancestorName = primaryFiber ? findMeaningfulAncestorName(primaryFiber) : null;
617
+ if (ancestorName) return '[' + ancestorName + ']';
618
+ return baseTestID || null;
619
+ }
620
+
621
+ function isScreenHidden(name, props) {
622
+ if (!props) return false;
623
+ if (name === 'MaybeScreen' && props.active === 0) return true;
624
+ if (name === 'SceneView' && props.focused === false) return true;
625
+ if (name === 'RNSScreen' && props['aria-hidden'] === true) return true;
626
+ return false;
627
+ }
628
+
629
+ function walkPressabilityDebugViews(fiber, depth, hidden, ovIdx) {
630
+ if (!fiber || depth > 5000) return;
631
+ var name = getComponentName(fiber);
632
+ var props = fiber.memoizedProps;
633
+ var nextHidden = hidden || isScreenHidden(name, props);
634
+
635
+ // Track which overlay subtree we're inside — membership by ancestry, not
636
+ // geometry. A bottom sheet's subtree includes a full-screen backdrop, so
637
+ // geometric containment wrongly swallows the underlying screen's pressables.
638
+ if (ovIdx == null) {
639
+ for (var ofi = 0; ofi < overlayFiberMeta.length; ofi++) {
640
+ if (overlayFiberMeta[ofi].fiber === fiber) { ovIdx = ofi; break; }
641
+ }
642
+ }
643
+
644
+ if (!nextHidden && name === 'PressabilityDebugView') {
645
+ var hostFiber = fiber.return;
646
+ if (hostFiber && getMeasurable(hostFiber)) {
647
+ var pressableFiber = hostFiber;
648
+ var cur2 = hostFiber.return;
649
+ var upD = 0;
650
+ while (cur2 && upD < 10) {
651
+ if (typeof cur2.type !== 'string' && cur2.type !== null) {
652
+ var cn = getComponentName(cur2);
653
+ if (cn && PDV_OWNER_COMPONENT.test(cn)) { pressableFiber = cur2; break; }
654
+ }
655
+ cur2 = cur2.return;
656
+ upD++;
657
+ }
658
+ var pProps = pressableFiber.memoizedProps || {};
659
+ var hProps = hostFiber.memoizedProps || {};
660
+ var text = collectText(pressableFiber, 0);
661
+ var testID = hProps.testID || hProps.nativeID || pProps.testID || pProps.nativeID || null;
662
+ var a11y = hProps.accessibilityLabel || pProps.accessibilityLabel || null;
663
+ var baseLabel = a11y || (text && text.length > 0 ? text.slice(0, 80) : null) || null;
664
+ var label = resolveLabel(pressableFiber, hostFiber, baseLabel, testID);
665
+ var icon = baseLabel ? null : findMeaningfulChildName(pressableFiber);
666
+ if (!baseLabel && !icon) {
667
+ var ownName = getComponentName(pressableFiber);
668
+ if (ownName && !GENERIC_COMPONENT.test(ownName) && !RN_PRIMITIVES.test(ownName)) icon = ownName;
669
+ }
670
+ var hostIdx = hostFibers.length;
671
+ hostFibers.push(hostFiber);
672
+ var pressFn = (typeof pProps.onPress === 'function' && pProps.onPress) || (typeof hProps.onPress === 'function' && hProps.onPress) || null;
673
+ fiberMeta.push({ label: label, testID: testID, hostIdx: hostIdx, icon: icon, overlayIdx: ovIdx, component: resolveComponentName(pressableFiber), hasOwnLabel: !!baseLabel, handler: pressFn ? handlerHint(pressFn) : null, propHandlers: collectPropHandlers(pressableFiber, pressFn) });
674
+ }
675
+ return;
676
+ }
677
+
678
+ // Fallback: onPress-based detection when PDV not present (production builds)
679
+ if (!nextHidden && props && typeof props.onPress === 'function') {
680
+ var hosts3 = [];
681
+ findHostsInSubtree(fiber, 0, hosts3, 8);
682
+ if (hosts3.length > 0) {
683
+ var p2 = fiber.memoizedProps || {};
684
+ var text2 = collectText(fiber, 0);
685
+ var a11y2 = p2.accessibilityLabel || null;
686
+ var testID2 = p2.testID || p2.nativeID || null;
687
+ var baseLabel2 = a11y2 || (text2 && text2.length > 0 ? text2.slice(0, 80) : null) || null;
688
+ var label2 = resolveLabel(fiber, hosts3[0], baseLabel2, testID2);
689
+ var icon2 = baseLabel2 ? null : findMeaningfulChildName(fiber);
690
+ if (!baseLabel2 && !icon2) {
691
+ var ownName2 = getComponentName(fiber);
692
+ if (ownName2 && !GENERIC_COMPONENT.test(ownName2) && !RN_PRIMITIVES.test(ownName2)) icon2 = ownName2;
693
+ }
694
+ var hostIdx2 = hostFibers.length;
695
+ hostFibers.push(hosts3[0]);
696
+ fiberMeta.push({ label: label2, testID: testID2, hostIdx: hostIdx2, icon: icon2, overlayIdx: ovIdx, component: resolveComponentName(fiber), hasOwnLabel: !!baseLabel2, handler: handlerHint(props.onPress), propHandlers: collectPropHandlers(fiber, props.onPress) });
697
+ }
698
+ }
699
+
700
+ // TextInputs — not covered by PressabilityDebugView or onPress. Label falls
701
+ // back to the placeholder so empty form fields stay identifiable.
702
+ if (!nextHidden && props && typeof props.onPress !== 'function' &&
703
+ (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function')) {
704
+ var hostsI = [];
705
+ findHostsInSubtree(fiber, 0, hostsI, 8);
706
+ if (hostsI.length > 0) {
707
+ var pI = fiber.memoizedProps || {};
708
+ var textI = collectText(fiber, 0);
709
+ var placeholderI = (typeof pI.placeholder === 'string' && pI.placeholder.length > 0) ? pI.placeholder : null;
710
+ var testIDI = pI.testID || pI.nativeID || null;
711
+ var baseLabelI = pI.accessibilityLabel || (textI && textI.length > 0 ? textI.slice(0, 80) : null) || (placeholderI ? placeholderI.slice(0, 80) : null) || null;
712
+ var labelI = resolveLabel(fiber, hostsI[0], baseLabelI, testIDI);
713
+ var iconI = baseLabelI ? null : findMeaningfulChildName(fiber);
714
+ var hostIdxI = hostFibers.length;
715
+ hostFibers.push(hostsI[0]);
716
+ fiberMeta.push({ label: labelI, testID: testIDI, hostIdx: hostIdxI, icon: iconI, overlayIdx: ovIdx, component: resolveComponentName(fiber), isInput: true, hasOwnLabel: !!baseLabelI });
717
+ }
718
+ }
719
+
720
+ var child = fiber.child;
721
+ while (child) {
722
+ walkPressabilityDebugViews(child, depth + 1, nextHidden, ovIdx);
723
+ child = child.sibling;
724
+ }
725
+ }
726
+ walkPressabilityDebugViews(roots[0].current, 0, false, null);
727
+
728
+ // ------------------------------------------------------------------
729
+ // 3b. Standalone texts — proximity labels for icon-only pressables
730
+ // (checkbox/radio rows where the label is a sibling Text)
731
+ // ------------------------------------------------------------------
732
+
733
+ var textFibers = [];
734
+ var textContents = [];
735
+
736
+ function extractTextString(fiber) {
737
+ var p = fiber.memoizedProps;
738
+ if (!p) return '';
739
+ var ch = p.children;
740
+ if (typeof ch === 'string') return ch;
741
+ if (typeof ch === 'number') return String(ch);
742
+ if (Array.isArray(ch)) {
743
+ var parts = [];
744
+ for (var k = 0; k < ch.length; k++) {
745
+ if (typeof ch[k] === 'string') parts.push(ch[k]);
746
+ else if (typeof ch[k] === 'number') parts.push(String(ch[k]));
747
+ }
748
+ if (parts.length > 0) return parts.join('');
749
+ }
750
+ return '';
751
+ }
752
+
753
+ function walkTexts(fiber, depth, insidePressable, inHidden) {
754
+ if (!fiber || depth > 5000) return;
755
+ var name = getComponentName(fiber);
756
+ var props = fiber.memoizedProps;
757
+ var nextHidden = inHidden || isScreenHidden(name, props);
758
+
759
+ var hasOnPress = props && typeof props.onPress === 'function';
760
+ var isInputHere = props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
761
+ var nextInside = insidePressable || !!hasOnPress || !!isInputHere;
762
+
763
+ // Record standalone text when outside any pressable — its own text already
764
+ // labels the pressable it belongs to. Climb to the nearest measurable host
765
+ // for proxy bounds (Fabric RCTText has no publicInstance).
766
+ if (!insidePressable && !nextHidden && name !== 'RCTText' && typeof fiber.type !== 'string') {
767
+ var str = extractTextString(fiber);
768
+ if (str && str.length > 0 && str.length <= 120) {
769
+ var up = fiber;
770
+ var upDepth = 0;
771
+ var measurableT = null;
772
+ while (up && upDepth < 20) {
773
+ if (typeof up.type === 'string' && getMeasurable(up)) {
774
+ measurableT = up;
775
+ break;
776
+ }
777
+ up = up.return;
778
+ upDepth++;
779
+ }
780
+ if (measurableT) {
781
+ textFibers.push(measurableT);
782
+ textContents.push(str);
783
+ }
784
+ }
785
+ }
786
+
787
+ var child = fiber.child;
788
+ while (child) {
789
+ walkTexts(child, depth + 1, nextInside, nextHidden);
790
+ child = child.sibling;
791
+ }
792
+ }
793
+ walkTexts(roots[0].current, 0, false, false);
794
+
795
+ // ------------------------------------------------------------------
796
+ // 4. Store everything in globalThis for the resolve call
797
+ // ------------------------------------------------------------------
798
+
799
+ // Store overlay host fibers separately
800
+ var overlayHostFibers = [];
801
+ var overlayMetaList = overlayFiberMeta.map(function(om) {
802
+ var startIdx = overlayHostFibers.length;
803
+ for (var hi = 0; hi < om.hostFibers.length; hi++) {
804
+ overlayHostFibers.push(om.hostFibers[hi]);
805
+ }
806
+ var title = collectText(om.fiber, 0);
807
+ return {
808
+ type: om.type,
809
+ title: (title && title.length > 2) ? title.slice(0, 60) : null,
810
+ hostStart: startIdx,
811
+ hostEnd: overlayHostFibers.length
812
+ };
813
+ });
814
+
815
+ // Measure root for viewport
816
+ var rootIdx = -1;
817
+ if (rootHostFiber) {
818
+ rootIdx = hostFibers.length;
819
+ hostFibers.push(rootHostFiber);
820
+ }
821
+
822
+ globalThis.__screenStateFibers = hostFibers;
823
+ globalThis.__screenStateMeta = fiberMeta;
824
+ globalThis.__screenStateMeasurements = new Array(hostFibers.length).fill(null);
825
+ globalThis.__screenStateRootIdx = rootIdx;
826
+ globalThis.__screenStateRoute = route;
827
+ globalThis.__screenStateOverlayHostFibers = overlayHostFibers;
828
+ globalThis.__screenStateOverlayMeta = overlayMetaList;
829
+ globalThis.__screenStateOverlayMeasurements = new Array(overlayHostFibers.length).fill(null);
830
+ globalThis.__screenStateTextContents = textContents;
831
+ globalThis.__screenStateTextMeasurements = new Array(textFibers.length).fill(null);
832
+
833
+ // Dispatch all measureInWindow calls (pressables + root + overlay hosts)
834
+ for (var i = 0; i < hostFibers.length; i++) {
835
+ try {
836
+ (function(idx) {
837
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
838
+ globalThis.__screenStateMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
839
+ });
840
+ })(i);
841
+ } catch(e) {}
842
+ }
843
+ for (var oi = 0; oi < overlayHostFibers.length; oi++) {
844
+ try {
845
+ (function(idx) {
846
+ getMeasurable(overlayHostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
847
+ globalThis.__screenStateOverlayMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
848
+ });
849
+ })(oi);
850
+ } catch(e) {}
851
+ }
852
+ for (var txi = 0; txi < textFibers.length; txi++) {
853
+ try {
854
+ (function(idx) {
855
+ getMeasurable(textFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
856
+ globalThis.__screenStateTextMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
857
+ });
858
+ })(txi);
859
+ } catch(e) {}
860
+ }
861
+
862
+ return { count: hostFibers.length, overlayCount: overlayFiberMeta.length };
863
+ })()
864
+ `;
865
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000, originatingToolName: "get_screen_state" }, device);
866
+ if (!dispatchResult.success)
867
+ return dispatchResult;
868
+ try {
869
+ const dp = JSON.parse(dispatchResult.result || "{}");
870
+ if (dp.error)
871
+ return { success: false, error: dp.error };
872
+ }
873
+ catch { /* ignore */ }
874
+ await delay(300);
875
+ const resolveExpression = `
876
+ (function() {
877
+ var hostFibers = globalThis.__screenStateFibers;
878
+ var meta = globalThis.__screenStateMeta;
879
+ var measurements = globalThis.__screenStateMeasurements;
880
+ var rootIdx = globalThis.__screenStateRootIdx;
881
+ var route = globalThis.__screenStateRoute;
882
+ var overlayMeta = globalThis.__screenStateOverlayMeta || [];
883
+ var overlayMeasurements = globalThis.__screenStateOverlayMeasurements || [];
884
+ var textContents = globalThis.__screenStateTextContents || [];
885
+ var textMeasurements = globalThis.__screenStateTextMeasurements || [];
886
+ globalThis.__screenStateTextContents = null;
887
+ globalThis.__screenStateTextMeasurements = null;
888
+ globalThis.__screenStateFibers = null;
889
+ globalThis.__screenStateMeta = null;
890
+ globalThis.__screenStateMeasurements = null;
891
+ globalThis.__screenStateRootIdx = null;
892
+ globalThis.__screenStateRoute = null;
893
+ globalThis.__screenStateOverlayHostFibers = null;
894
+ globalThis.__screenStateOverlayMeta = null;
895
+ globalThis.__screenStateOverlayMeasurements = null;
896
+
897
+ if (!hostFibers || !measurements || !meta) {
898
+ return { error: 'No measurement data. Run get_screen_state again.' };
899
+ }
900
+
901
+ // Viewport
902
+ var viewportW = 9999, viewportH = 9999;
903
+ var rootM = (rootIdx != null && rootIdx >= 0) ? measurements[rootIdx] : null;
904
+ if (rootM && rootM.width > 0 && rootM.height > 0) {
905
+ viewportW = rootM.width;
906
+ viewportH = rootM.height + (rootM.y > 0 ? rootM.y : 0);
907
+ }
908
+
909
+ // Build overlay bounds by unioning their host measurements.
910
+ // blockBounds: union of ALL hosts (incl. full-screen backdrop) — what the overlay
911
+ // visually blocks; used to exclude unreachable root pressables.
912
+ // contentBounds: union excluding near-viewport-sized hosts (backdrops) — the
913
+ // actual panel; used as geometric fallback for portaled overlay content.
914
+ var overlays = [];
915
+ var vArea = viewportW * viewportH;
916
+ for (var oi = 0; oi < overlayMeta.length; oi++) {
917
+ var om = overlayMeta[oi];
918
+ var bMinX = Infinity, bMinY = Infinity, bMaxX = -Infinity, bMaxY = -Infinity, bValid = false;
919
+ var cMinX = Infinity, cMinY = Infinity, cMaxX = -Infinity, cMaxY = -Infinity, cValid = false;
920
+ for (var hi = om.hostStart; hi < om.hostEnd; hi++) {
921
+ var mm = overlayMeasurements[hi];
922
+ if (!mm || mm.width <= 0 || mm.height <= 0) continue;
923
+ bValid = true;
924
+ if (mm.x < bMinX) bMinX = mm.x;
925
+ if (mm.y < bMinY) bMinY = mm.y;
926
+ if (mm.x + mm.width > bMaxX) bMaxX = mm.x + mm.width;
927
+ if (mm.y + mm.height > bMaxY) bMaxY = mm.y + mm.height;
928
+ if (mm.width * mm.height >= vArea * 0.9) continue; // backdrop-sized → block only
929
+ cValid = true;
930
+ if (mm.x < cMinX) cMinX = mm.x;
931
+ if (mm.y < cMinY) cMinY = mm.y;
932
+ if (mm.x + mm.width > cMaxX) cMaxX = mm.x + mm.width;
933
+ if (mm.y + mm.height > cMaxY) cMaxY = mm.y + mm.height;
934
+ }
935
+ if (!bValid) continue;
936
+ var blockBounds = { x: Math.round(bMinX), y: Math.round(bMinY), width: Math.round(bMaxX - bMinX), height: Math.round(bMaxY - bMinY) };
937
+ // When every host is backdrop-sized (gorhom sheets measure only full-screen
938
+ // containers), there is no usable content rect — geometric fallback would
939
+ // swallow the underlying screen's pressables, so it is disabled (hasContent).
940
+ var contentBounds = cValid
941
+ ? { x: Math.round(cMinX), y: Math.round(cMinY), width: Math.round(cMaxX - cMinX), height: Math.round(cMaxY - cMinY) }
942
+ : blockBounds;
943
+ overlays.push({ origIdx: oi, type: om.type, title: om.title, blockBounds: blockBounds, contentBounds: contentBounds, hasContent: cValid, pressables: [] });
944
+ }
945
+
946
+ // Build pressable list
947
+ var allPressables = [];
948
+ for (var i = 0; i < meta.length; i++) {
949
+ if (i === rootIdx) continue;
950
+ var m = measurements[i];
951
+ if (!m || m.width <= 0 || m.height <= 0) continue;
952
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
953
+ if (m.x > viewportW || m.y > viewportH) continue;
954
+ allPressables.push({
955
+ label: meta[i].label,
956
+ component: meta[i].component || null,
957
+ center: { x: Math.round(m.x + m.width / 2), y: Math.round(m.y + m.height / 2) },
958
+ bounds: { x: Math.round(m.x), y: Math.round(m.y), width: Math.round(m.width), height: Math.round(m.height) },
959
+ testID: meta[i].testID,
960
+ icon: meta[i].icon || null,
961
+ isInput: !!meta[i].isInput,
962
+ overlayIdx: (meta[i].overlayIdx != null ? meta[i].overlayIdx : null),
963
+ hasOwnLabel: !!meta[i].hasOwnLabel,
964
+ handler: meta[i].handler || null,
965
+ propHandlers: meta[i].propHandlers || null
966
+ });
967
+ }
968
+
969
+ // Attach nearbyText to pressables without their own text/a11y label.
970
+ // Row siblings first (checkbox labels), center distance as fallback.
971
+ var textBoxes = [];
972
+ for (var tmi = 0; tmi < textMeasurements.length; tmi++) {
973
+ var tm = textMeasurements[tmi];
974
+ var tc = textContents[tmi];
975
+ if (!tm || !tc || tm.width <= 0 || tm.height <= 0) continue;
976
+ if (tm.x + tm.width < 0 || tm.y + tm.height < 0) continue;
977
+ if (tm.x > viewportW || tm.y > viewportH) continue;
978
+ textBoxes.push({ text: tc, x: tm.x, y: tm.y, width: tm.width, height: tm.height, cx: tm.x + tm.width / 2, cy: tm.y + tm.height / 2 });
979
+ }
980
+ for (var ni = 0; ni < allPressables.length; ni++) {
981
+ var pn = allPressables[ni];
982
+ if (pn.hasOwnLabel || textBoxes.length === 0) continue;
983
+ var bestT = null;
984
+ var bestD = Infinity;
985
+ for (var tbi = 0; tbi < textBoxes.length; tbi++) {
986
+ var tb = textBoxes[tbi];
987
+ if (tb.x >= pn.bounds.x && tb.y >= pn.bounds.y &&
988
+ tb.x + tb.width <= pn.bounds.x + pn.bounds.width &&
989
+ tb.y + tb.height <= pn.bounds.y + pn.bounds.height) continue;
990
+ var rowAligned = Math.abs(tb.cy - pn.center.y) <= Math.max(24, pn.bounds.height / 2);
991
+ var gapL = pn.bounds.x - (tb.x + tb.width);
992
+ var gapR = tb.x - (pn.bounds.x + pn.bounds.width);
993
+ var hGap = Math.max(gapL, gapR);
994
+ if (hGap < 0) hGap = 0;
995
+ var d;
996
+ if (rowAligned && hGap <= 80) {
997
+ d = hGap;
998
+ } else {
999
+ var dxT = tb.cx - pn.center.x;
1000
+ var dyT = tb.cy - pn.center.y;
1001
+ d = Math.sqrt(dxT * dxT + dyT * dyT);
1002
+ if (d > 120) continue;
1003
+ }
1004
+ if (d < bestD) { bestD = d; bestT = tb; }
1005
+ }
1006
+ if (bestT) pn.nearbyText = bestT.text.slice(0, 80);
1007
+ }
1008
+
1009
+ // Assign pressables to overlays vs root:
1010
+ // 1. fiber ancestry (pressable rendered inside the overlay subtree)
1011
+ // 2. geometric containment in contentBounds (portaled overlay content)
1012
+ // 3. fully covered by blockBounds (incl. backdrop) → unreachable, drop
1013
+ // 4. otherwise root
1014
+ var rootPressables = [];
1015
+ function inside(b, ob) {
1016
+ return b.x >= ob.x && b.y >= ob.y &&
1017
+ b.x + b.width <= ob.x + ob.width &&
1018
+ b.y + b.height <= ob.y + ob.height;
1019
+ }
1020
+ for (var pi = 0; pi < allPressables.length; pi++) {
1021
+ var p = allPressables[pi];
1022
+ var assignedToOverlay = false;
1023
+ for (var ov = 0; ov < overlays.length; ov++) {
1024
+ if (p.overlayIdx === overlays[ov].origIdx ||
1025
+ (p.overlayIdx == null && overlays[ov].hasContent && inside(p.bounds, overlays[ov].contentBounds))) {
1026
+ overlays[ov].pressables.push(p);
1027
+ assignedToOverlay = true;
1028
+ break;
1029
+ }
1030
+ }
1031
+ if (!assignedToOverlay) {
1032
+ // Covered pressables stay in the list but are flagged — agents see what's
1033
+ // behind the sheet while knowing taps won't reach it until it closes.
1034
+ for (var ov2 = 0; ov2 < overlays.length; ov2++) {
1035
+ if (inside(p.bounds, overlays[ov2].blockBounds)) { p.blockedByOverlay = true; break; }
1036
+ }
1037
+ rootPressables.push(p);
1038
+ }
1039
+ }
1040
+ for (var si = 0; si < allPressables.length; si++) {
1041
+ delete allPressables[si].overlayIdx;
1042
+ delete allPressables[si].hasOwnLabel;
1043
+ }
1044
+
1045
+ // Sort visually (top-to-bottom, left-to-right) — walk order is mount order,
1046
+ // which puts late-mounted overlays like floating headers at the end.
1047
+ function byPosition(a, b) {
1048
+ if (a.center.y !== b.center.y) return a.center.y - b.center.y;
1049
+ return a.center.x - b.center.x;
1050
+ }
1051
+ rootPressables.sort(byPosition);
1052
+ for (var so = 0; so < overlays.length; so++) overlays[so].pressables.sort(byPosition);
1053
+
1054
+ // Strip bounds from overlay objects (not in public interface)
1055
+ var cleanOverlays = overlays.map(function(o) {
1056
+ return { type: o.type, title: o.title, pressables: o.pressables };
1057
+ });
1058
+
1059
+ return { route: route, overlays: cleanOverlays, pressables: rootPressables };
1060
+ })()
1061
+ `;
1062
+ const resolveResult = await executeInApp(resolveExpression, false, { timeoutMs: 15000, originatingToolName: "get_screen_state" }, device);
1063
+ if (!resolveResult.success)
1064
+ return resolveResult;
1065
+ let screenState;
1066
+ try {
1067
+ const parsed = JSON.parse(resolveResult.result || "{}");
1068
+ if (parsed.error)
1069
+ return { success: false, error: parsed.error };
1070
+ screenState = parseScreenStateResponse(parsed) ?? undefined;
1071
+ }
1072
+ catch {
1073
+ return { success: false, error: "Failed to parse screen state response" };
1074
+ }
1075
+ if (!screenState)
1076
+ return { success: false, error: "Empty screen state response" };
1077
+ // Deduplicate pressables by center coordinates (PDV + onPress fallback can both fire)
1078
+ const dedupPressables = (list) => {
1079
+ const seen = new Set();
1080
+ return list.filter((p) => {
1081
+ const key = `${p.center.x},${p.center.y}`;
1082
+ if (seen.has(key))
1083
+ return false;
1084
+ seen.add(key);
1085
+ return true;
1086
+ });
1087
+ };
1088
+ screenState.pressables = dedupPressables(screenState.pressables);
1089
+ for (const overlay of screenState.overlays) {
1090
+ overlay.pressables = dedupPressables(overlay.pressables);
1091
+ }
1092
+ // Semantic icon hints + onPress handler hints
1093
+ const decorate = (p) => {
1094
+ applyIconHintToLabel(p);
1095
+ const direct = describePressHandler(p.handler);
1096
+ const hint = direct ? `onPress=${direct}` : describePropHandlers(p.propHandlers);
1097
+ if (hint)
1098
+ p.onPressHint = hint;
1099
+ delete p.handler;
1100
+ delete p.propHandlers;
1101
+ };
1102
+ screenState.pressables.forEach(decorate);
1103
+ for (const overlay of screenState.overlays) {
1104
+ overlay.pressables.forEach(decorate);
1105
+ }
1106
+ // Apply TS-side overlay marking as a safety pass
1107
+ for (const overlay of screenState.overlays) {
1108
+ if (overlay.pressables.length === 0)
1109
+ continue;
1110
+ const minX = Math.min(...overlay.pressables.map((p) => p.bounds.x));
1111
+ const minY = Math.min(...overlay.pressables.map((p) => p.bounds.y));
1112
+ const maxX = Math.max(...overlay.pressables.map((p) => p.bounds.x + p.bounds.width));
1113
+ const maxY = Math.max(...overlay.pressables.map((p) => p.bounds.y + p.bounds.height));
1114
+ const overlayBounds = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1115
+ markPressablesCoveredByOverlay(screenState.pressables, overlayBounds);
1116
+ }
1117
+ const json = JSON.stringify(screenState, null, 2);
1118
+ return { success: true, result: json, screenState };
1119
+ }
1120
+ //# sourceMappingURL=screenState.js.map