react-native-ai-debugger 1.0.36 → 1.0.38

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.
package/README.md CHANGED
@@ -47,6 +47,9 @@ This repository includes pre-built [Claude Code skills](https://docs.anthropic.c
47
47
  | `layout-check` | Verify UI layout against design specs using screenshots and component data |
48
48
  | `device-interact` | Automate device interaction: tap, swipe, text input, and element finding |
49
49
  | `bundle-check` | Detect and diagnose Metro bundler errors and compilation failures |
50
+ | `native-rebuild` | Rebuild and verify the app after installing native Expo packages |
51
+
52
+ See [`skills/overview.md`](./skills/overview.md) for a decision guide on which skill to use and a recommended workflow.
50
53
 
51
54
  ### Installing Skills (Claude Code)
52
55
 
@@ -65,11 +65,15 @@ export declare function isInspectorActive(): Promise<boolean>;
65
65
  export declare function getInspectorSelection(): Promise<ExecutionResult>;
66
66
  /**
67
67
  * Inspect the React component at a specific (x, y) coordinate.
68
- * Uses the same internal API as React Native's Element Inspector.
69
68
  *
70
- * Note: This API (getInspectorDataForViewAtPoint) is only available in certain
71
- * React Native versions. In newer versions with Fabric, use ios_describe_point
72
- * or android_describe_point as alternatives.
69
+ * Works on both Paper and Fabric (New Architecture). Uses a two-step approach
70
+ * because measureInWindow callbacks fire in a future native event loop tick
71
+ * (not microtasks), so awaitPromise cannot be used to collect them:
72
+ *
73
+ * Step 1 — dispatch: walk the fiber tree, call measureInWindow on each host
74
+ * component, store fiber refs and results in app globals.
75
+ * Step 2 — resolve (after 300ms): read the globals, hit-test against target
76
+ * coordinates, return the innermost matching React component.
73
77
  */
74
78
  export declare function inspectAtPoint(x: number, y: number, options?: {
75
79
  includeProps?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA8N7D,wBAAsB,YAAY,CAC9B,UAAU,EAAE,MAAM,EAClB,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,cAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAoF1B;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC,CAkBjE;AAGD,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAsBhF;AAGD,wBAAsB,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC,CA0H1D;AAgHD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,eAAe,CAAC,CAmOhC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiLhC;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkNhC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6IhC;AAMD;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,eAAe,CAAC,CAqBvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAqC1D;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,eAAe,CAAC,CA2EtE;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAChE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkIhC"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA8R7D,wBAAsB,YAAY,CAC9B,UAAU,EAAE,MAAM,EAClB,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,cAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAoF1B;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC,CAkBjE;AAGD,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAsBhF;AAKD,wBAAsB,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC,CAsI1D;AAgHD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,eAAe,CAAC,CAoOhC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkLhC;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkNhC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6IhC;AAMD;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,eAAe,CAAC,CAqBvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAqC1D;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,eAAe,CAAC,CA2EtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAChE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyMhC"}
@@ -85,6 +85,58 @@ function validateAndPreprocessExpression(expression) {
85
85
  expression: cleaned
86
86
  };
87
87
  }
88
+ /**
89
+ * Detect if an expression contains multiple statements or declarations.
90
+ * These cannot be wrapped with `return (expr)` — they need block wrapping.
91
+ *
92
+ * Examples:
93
+ * - "console.log('x'); 'result'" → multi-statement (semicolon between statements)
94
+ * - "var x = 1; btoa(x)" → declaration + multi-statement
95
+ * - "var x = 1" → declaration
96
+ * - "JSON.stringify(obj)" → single expression (NOT multi-statement)
97
+ * - "JSON.stringify({a: 'x;y'})" → single expression with semicolon in string
98
+ */
99
+ function isMultiStatementExpression(expr) {
100
+ const trimmed = expr.trim();
101
+ // Starts with a declaration or statement keyword
102
+ if (/^(var|let|const|function|class|for|while|if|switch|try|do|throw)\b/.test(trimmed)) {
103
+ return true;
104
+ }
105
+ // Check for semicolons that separate statements (not trailing, not inside strings)
106
+ // Remove the trailing semicolon and whitespace first
107
+ const withoutTrailing = trimmed.replace(/;\s*$/, '');
108
+ // Simple heuristic: check if there's a semicolon that's likely between statements
109
+ // Skip semicolons inside string literals by tracking quote state
110
+ let inSingle = false;
111
+ let inDouble = false;
112
+ let inTemplate = false;
113
+ let escaped = false;
114
+ for (let i = 0; i < withoutTrailing.length; i++) {
115
+ const ch = withoutTrailing[i];
116
+ if (escaped) {
117
+ escaped = false;
118
+ continue;
119
+ }
120
+ if (ch === '\\') {
121
+ escaped = true;
122
+ continue;
123
+ }
124
+ if (ch === "'" && !inDouble && !inTemplate) {
125
+ inSingle = !inSingle;
126
+ }
127
+ else if (ch === '"' && !inSingle && !inTemplate) {
128
+ inDouble = !inDouble;
129
+ }
130
+ else if (ch === '`' && !inSingle && !inDouble) {
131
+ inTemplate = !inTemplate;
132
+ }
133
+ else if (ch === ';' && !inSingle && !inDouble && !inTemplate) {
134
+ // Found a semicolon outside of strings — multi-statement
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
88
140
  // Error patterns that indicate a stale/destroyed context
89
141
  const CONTEXT_ERROR_PATTERNS = [
90
142
  "cannot find context",
@@ -94,6 +146,7 @@ const CONTEXT_ERROR_PATTERNS = [
94
146
  "session closed",
95
147
  "context with specified id",
96
148
  "no execution context",
149
+ "runningdetached",
97
150
  ];
98
151
  /**
99
152
  * Check if an error indicates a stale page context
@@ -133,7 +186,7 @@ async function attemptQuickReconnect(preferredPort) {
133
186
  /**
134
187
  * Execute expression on a connected app (core implementation without retry)
135
188
  */
136
- async function executeExpressionCore(expression, awaitPromise) {
189
+ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000) {
137
190
  const app = getFirstConnectedApp();
138
191
  if (!app) {
139
192
  return { success: false, error: "No apps connected. Run 'scan_metro' first." };
@@ -147,10 +200,14 @@ async function executeExpressionCore(expression, awaitPromise) {
147
200
  return { success: false, error: validation.error };
148
201
  }
149
202
  const cleanedExpression = validation.expression;
150
- const TIMEOUT_MS = 10000;
203
+ const TIMEOUT_MS = timeoutMs;
151
204
  const currentMessageId = getNextMessageId();
152
205
  // Wrap expression with global polyfill for Hermes compatibility
153
- const wrappedExpression = `(function() { ${GLOBAL_POLYFILL} return (${cleanedExpression}); })()`;
206
+ // Multi-statement expressions (var x = 1; foo(x)) can't use `return (expr)` wrapping
207
+ // because declarations and semicolons are invalid inside parenthesized expressions
208
+ const wrappedExpression = isMultiStatementExpression(cleanedExpression)
209
+ ? `(function() { ${GLOBAL_POLYFILL} ${cleanedExpression} })()`
210
+ : `(function() { ${GLOBAL_POLYFILL} return (${cleanedExpression}); })()`;
154
211
  return new Promise((resolve) => {
155
212
  const timeoutId = setTimeout(() => {
156
213
  pendingExecutions.delete(currentMessageId);
@@ -182,7 +239,7 @@ async function executeExpressionCore(expression, awaitPromise) {
182
239
  }
183
240
  // Execute JavaScript in the connected React Native app with retry logic
184
241
  export async function executeInApp(expression, awaitPromise = true, options = {}) {
185
- const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true } = options;
242
+ const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
186
243
  let lastError;
187
244
  let preferredPort;
188
245
  // Get preferred port from current connection if available
@@ -225,7 +282,7 @@ export async function executeInApp(expression, awaitPromise = true, options = {}
225
282
  return { success: false, error: "WebSocket connection is not open." };
226
283
  }
227
284
  // Execute the expression
228
- const result = await executeExpressionCore(expression, awaitPromise);
285
+ const result = await executeExpressionCore(expression, awaitPromise, timeoutMs);
229
286
  // Success - return result
230
287
  if (result.success) {
231
288
  return result;
@@ -301,6 +358,8 @@ export async function inspectGlobal(objectName) {
301
358
  return executeInApp(expression, false);
302
359
  }
303
360
  // Reload the React Native app using __ReactRefresh (Page.reload is not supported by Hermes)
361
+ // Uses fire-and-forget: sends the reload command without waiting for a response,
362
+ // since the JS context is destroyed during reload and would always timeout.
304
363
  export async function reloadApp() {
305
364
  // Get current connection info before reload
306
365
  let app = getFirstConnectedApp();
@@ -340,35 +399,48 @@ export async function reloadApp() {
340
399
  }
341
400
  }
342
401
  const port = app.port;
343
- // Use __ReactRefresh.performFullRefresh() which is available in Metro bundler dev mode
344
- // This works with Hermes unlike the CDP Page.reload method
345
- const expression = `
346
- (function() {
347
- try {
348
- // Use React Refresh's full refresh - most reliable method
349
- if (typeof __ReactRefresh !== 'undefined' && typeof __ReactRefresh.performFullRefresh === 'function') {
350
- __ReactRefresh.performFullRefresh('mcp-reload');
351
- return 'Reload triggered via __ReactRefresh.performFullRefresh';
352
- }
353
- // Fallback: Try DevSettings if available on global
354
- if (typeof global !== 'undefined' && global.DevSettings && typeof global.DevSettings.reload === 'function') {
355
- global.DevSettings.reload();
356
- return 'Reload triggered via DevSettings';
357
- }
358
- return 'Reload not available - make sure app is in development mode with Metro bundler';
359
- } catch (e) {
360
- return 'Reload failed: ' + e.message;
402
+ // Fire-and-forget: send reload command via CDP without waiting for response.
403
+ // The JS context is destroyed during reload, so Runtime.evaluate would always timeout.
404
+ const reloadExpression = `(function() {
405
+ try {
406
+ if (typeof __ReactRefresh !== 'undefined' && typeof __ReactRefresh.performFullRefresh === 'function') {
407
+ __ReactRefresh.performFullRefresh('mcp-reload');
408
+ return 'ok';
361
409
  }
362
- })()
363
- `;
364
- const result = await executeInApp(expression, false);
365
- if (!result.success) {
366
- return result;
410
+ if (typeof global !== 'undefined' && global.DevSettings && typeof global.DevSettings.reload === 'function') {
411
+ global.DevSettings.reload();
412
+ return 'ok';
413
+ }
414
+ return 'no-method';
415
+ } catch (e) { return 'error:' + e.message; }
416
+ })()`;
417
+ try {
418
+ if (app.ws.readyState !== WebSocket.OPEN) {
419
+ return { success: false, error: "WebSocket connection is not open." };
420
+ }
421
+ // Send without registering a pending execution — fire and forget
422
+ const messageId = getNextMessageId();
423
+ app.ws.send(JSON.stringify({
424
+ id: messageId,
425
+ method: "Runtime.evaluate",
426
+ params: {
427
+ expression: reloadExpression,
428
+ returnByValue: true,
429
+ awaitPromise: false,
430
+ userGesture: true,
431
+ },
432
+ }));
433
+ }
434
+ catch (error) {
435
+ return {
436
+ success: false,
437
+ error: `Failed to send reload command: ${error instanceof Error ? error.message : String(error)}`
438
+ };
367
439
  }
368
440
  // Auto-reconnect after reload
369
441
  try {
370
442
  // Wait for app to reload (give it time to restart JS context)
371
- await new Promise(resolve => setTimeout(resolve, 2000));
443
+ await delay(2000);
372
444
  // Close existing connections to this port and cancel any pending auto-reconnections
373
445
  // This prevents the dual-reconnection bug where both auto-reconnect and manual reconnect compete
374
446
  for (const [key, connectedApp] of connectedApps.entries()) {
@@ -385,7 +457,7 @@ export async function reloadApp() {
385
457
  }
386
458
  }
387
459
  // Small delay to ensure cleanup
388
- await new Promise(resolve => setTimeout(resolve, 500));
460
+ await delay(500);
389
461
  // Reconnect to Metro on the same port with auto-reconnection DISABLED
390
462
  // We're doing a manual reconnection here, so we don't want the auto-reconnect
391
463
  // system to also try reconnecting and compete with us
@@ -679,7 +751,8 @@ export async function getComponentTree(options = {}) {
679
751
  return { tree };
680
752
  })()
681
753
  `;
682
- const result = await executeInApp(expression, false);
754
+ // Use a longer timeout for component tree traversal — large apps can exceed 10s
755
+ const result = await executeInApp(expression, false, { timeoutMs: 30000 });
683
756
  // Apply formatting if requested
684
757
  if (result.success && result.result) {
685
758
  try {
@@ -862,7 +935,8 @@ export async function getScreenLayout(options = {}) {
862
935
  };
863
936
  })()
864
937
  `;
865
- const result = await executeInApp(expression, false);
938
+ // Use a longer timeout for layout traversal — large component trees can exceed 10s
939
+ const result = await executeInApp(expression, false, { timeoutMs: 30000 });
866
940
  // Apply TONL formatting if requested
867
941
  if (format === 'tonl' && result.success && result.result) {
868
942
  try {
@@ -1389,139 +1463,213 @@ export async function getInspectorSelection() {
1389
1463
  }
1390
1464
  /**
1391
1465
  * Inspect the React component at a specific (x, y) coordinate.
1392
- * Uses the same internal API as React Native's Element Inspector.
1393
1466
  *
1394
- * Note: This API (getInspectorDataForViewAtPoint) is only available in certain
1395
- * React Native versions. In newer versions with Fabric, use ios_describe_point
1396
- * or android_describe_point as alternatives.
1467
+ * Works on both Paper and Fabric (New Architecture). Uses a two-step approach
1468
+ * because measureInWindow callbacks fire in a future native event loop tick
1469
+ * (not microtasks), so awaitPromise cannot be used to collect them:
1470
+ *
1471
+ * Step 1 — dispatch: walk the fiber tree, call measureInWindow on each host
1472
+ * component, store fiber refs and results in app globals.
1473
+ * Step 2 — resolve (after 300ms): read the globals, hit-test against target
1474
+ * coordinates, return the innermost matching React component.
1397
1475
  */
1398
1476
  export async function inspectAtPoint(x, y, options = {}) {
1399
1477
  const { includeProps = true, includeFrame = true } = options;
1400
- const expression = `
1478
+ // --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
1479
+ const dispatchExpression = `
1401
1480
  (function() {
1402
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1481
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1403
1482
  if (!hook) return { error: 'React DevTools hook not available. Make sure you are running a development build.' };
1404
- if (!hook.renderers) return { error: 'No renderers found in DevTools hook.' };
1405
-
1406
- // Find a renderer with getInspectorDataForViewAtPoint
1407
- let inspectorFn = null;
1408
- let rendererId = null;
1409
- for (const [id, renderer] of hook.renderers) {
1410
- if (renderer.getInspectorDataForViewAtPoint) {
1411
- inspectorFn = renderer.getInspectorDataForViewAtPoint.bind(renderer);
1412
- rendererId = id;
1413
- break;
1483
+
1484
+ var roots = [];
1485
+ if (hook.getFiberRoots) {
1486
+ try { roots = Array.from(hook.getFiberRoots(1) || []); } catch(e) {}
1487
+ }
1488
+ if (roots.length === 0 && hook.renderers) {
1489
+ for (var entry of hook.renderers) {
1490
+ try {
1491
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
1492
+ if (r.length > 0) { roots = r; break; }
1493
+ } catch(e) {}
1414
1494
  }
1415
1495
  }
1496
+ if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
1416
1497
 
1417
- if (!inspectorFn) {
1418
- // This API is not available in newer React Native versions (Fabric/New Architecture)
1419
- return {
1420
- error: 'getInspectorDataForViewAtPoint not available in this React Native version.',
1421
- hint: 'This API was removed in newer React Native versions. Use ios_describe_point or android_describe_point instead to get native element info at coordinates, then use find_components to locate the React component.',
1422
- point: { x: ${x}, y: ${y} },
1423
- alternatives: [
1424
- 'ios_describe_point(x, y) - get native accessibility element at point',
1425
- 'android_describe_point(x, y) - get native UI element at point',
1426
- 'find_components(pattern) - find React components by name pattern'
1427
- ]
1428
- };
1498
+ // Paper: measureInWindow is on stateNode directly.
1499
+ // Fabric: measureInWindow is on stateNode.canonical.publicInstance.
1500
+ function getMeasurable(fiber) {
1501
+ var sn = fiber.stateNode;
1502
+ if (!sn) return null;
1503
+ if (typeof sn.measureInWindow === 'function') return sn;
1504
+ if (sn.canonical && sn.canonical.publicInstance &&
1505
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1506
+ return sn.canonical.publicInstance;
1507
+ }
1508
+ return null;
1429
1509
  }
1430
1510
 
1431
- // Call the inspector API with coordinates
1432
- // Note: This is a callback-based API that we need to wrap
1433
- return new Promise((resolve) => {
1511
+ var hostFibers = [];
1512
+ function walkFibers(fiber, depth) {
1513
+ var cur = fiber;
1514
+ while (cur) {
1515
+ if (hostFibers.length >= 500) return;
1516
+ if (typeof cur.type === 'string' && getMeasurable(cur)) hostFibers.push(cur);
1517
+ if (cur.child && depth < 250) walkFibers(cur.child, depth + 1);
1518
+ cur = cur.sibling;
1519
+ }
1520
+ }
1521
+ for (var root of roots) { walkFibers(root.current, 0); }
1522
+
1523
+ if (hostFibers.length === 0) return { error: 'No measurable host components found. App may not be fully rendered.' };
1524
+
1525
+ globalThis.__inspectFibers = hostFibers;
1526
+ globalThis.__inspectMeasurements = new Array(hostFibers.length).fill(null);
1527
+
1528
+ hostFibers.forEach(function(fiber, i) {
1434
1529
  try {
1435
- inspectorFn(
1436
- null, // containerRef (null = root view)
1437
- ${x}, // x coordinate
1438
- ${y}, // y coordinate
1439
- (viewData, viewTag, touchedViewTag, fiber, measure) => {
1440
- if (!fiber) {
1441
- resolve({
1442
- point: { x: ${x}, y: ${y} },
1443
- error: 'No component found at this point. The coordinates may be outside the app bounds or on a native-only element.'
1444
- });
1445
- return;
1446
- }
1530
+ getMeasurable(fiber).measureInWindow(function(fx, fy, fw, fh) {
1531
+ globalThis.__inspectMeasurements[i] = { x: fx, y: fy, width: fw, height: fh };
1532
+ });
1533
+ } catch(e) {}
1534
+ });
1447
1535
 
1448
- // Build hierarchy from the selected fiber by walking up the tree
1449
- const hierarchy = [];
1450
- let current = fiber;
1451
- while (current) {
1452
- const name = current.type?.displayName ||
1453
- current.type?.name ||
1454
- (typeof current.type === 'string' ? current.type : null);
1455
- if (name) hierarchy.unshift(name);
1456
- current = current.return;
1457
- }
1536
+ return { count: hostFibers.length };
1537
+ })()
1538
+ `;
1539
+ const dispatchResult = await executeInApp(dispatchExpression, false);
1540
+ if (!dispatchResult.success)
1541
+ return dispatchResult;
1542
+ try {
1543
+ const parsed = JSON.parse(dispatchResult.result || '{}');
1544
+ if (parsed.error)
1545
+ return { success: false, error: parsed.error };
1546
+ }
1547
+ catch { /* ignore parse errors */ }
1548
+ // Wait for native measureInWindow callbacks to fire
1549
+ await delay(300);
1550
+ // --- Step 2: read measurements, hit-test, return result ---
1551
+ const resolveExpression = `
1552
+ (function() {
1553
+ var fibers = globalThis.__inspectFibers;
1554
+ var measurements = globalThis.__inspectMeasurements;
1555
+ globalThis.__inspectFibers = null;
1556
+ globalThis.__inspectMeasurements = null;
1557
+
1558
+ if (!fibers || !measurements) return { error: 'No measurement data available. Run inspect_at_point again.' };
1559
+
1560
+ var targetX = ${x};
1561
+ var targetY = ${y};
1562
+
1563
+ var hits = [];
1564
+ for (var i = 0; i < measurements.length; i++) {
1565
+ var m = measurements[i];
1566
+ if (m && m.width > 0 && m.height > 0 &&
1567
+ targetX >= m.x && targetX <= m.x + m.width &&
1568
+ targetY >= m.y && targetY <= m.y + m.height) {
1569
+ hits.push({ fiber: fibers[i], x: m.x, y: m.y, width: m.width, height: m.height });
1570
+ }
1571
+ }
1458
1572
 
1459
- const result = {
1460
- point: { x: ${x}, y: ${y} },
1461
- element: hierarchy[hierarchy.length - 1] || 'Unknown',
1462
- path: hierarchy.join(' > ')
1463
- };
1464
-
1465
- // Include props if requested (exclude children and functions for cleaner output)
1466
- if (${includeProps} && viewData?.props) {
1467
- const props = {};
1468
- for (const key of Object.keys(viewData.props)) {
1469
- if (key === 'children') continue;
1470
- const val = viewData.props[key];
1471
- if (typeof val === 'function') {
1472
- props[key] = '[Function]';
1473
- } else if (typeof val === 'object' && val !== null) {
1474
- // Shallow serialize objects
1475
- try {
1476
- const str = JSON.stringify(val);
1477
- if (str.length > 200) {
1478
- props[key] = Array.isArray(val) ? '[Array(' + val.length + ')]' : '[Object]';
1479
- } else {
1480
- props[key] = val;
1481
- }
1482
- } catch {
1483
- props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
1484
- }
1485
- } else {
1486
- props[key] = val;
1487
- }
1488
- }
1489
- if (Object.keys(props).length > 0) result.props = props;
1490
- }
1573
+ if (hits.length === 0) {
1574
+ return { point: { x: targetX, y: targetY }, error: 'No component found at this point. Coordinates may be outside the app bounds or over a native-only element.' };
1575
+ }
1491
1576
 
1492
- // Include frame/dimensions if requested
1493
- if (${includeFrame}) {
1494
- if (viewData?.frame) {
1495
- result.frame = viewData.frame;
1496
- }
1497
- if (measure) {
1498
- // measure is a callback in some versions, an object in others
1499
- if (typeof measure === 'object') {
1500
- result.measure = measure;
1501
- }
1502
- }
1577
+ // Smallest area = innermost (most specific) component
1578
+ hits.sort(function(a, b) { return (a.width * a.height) - (b.width * b.height); });
1579
+ var best = hits[0];
1580
+
1581
+ // RN primitives and internal components to skip when surfacing the "element" name.
1582
+ // We want the nearest *custom* component, not a library wrapper.
1583
+ var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|Modal|SafeAreaView|KeyboardAvoidingView|Animated\(.*|withAnimated.*|ForwardRef.*|memo\(.*|Context\.Consumer|Context\.Provider|VirtualizedList.*|CellRenderer.*|FrameSizeProvider|MaybeScreenContainer|RCT.*|RNS.*|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer)$/;
1584
+
1585
+ function getNearestNamed(fiber, skipPrimitives) {
1586
+ var cur = fiber;
1587
+ var fallback = null;
1588
+ while (cur) {
1589
+ if (cur.type && typeof cur.type !== 'string') {
1590
+ var name = cur.type.displayName || cur.type.name;
1591
+ if (name) {
1592
+ if (!fallback) fallback = { name: name, fiber: cur };
1593
+ if (!skipPrimitives || !RN_PRIMITIVES.test(name)) {
1594
+ return { name: name, fiber: cur };
1503
1595
  }
1596
+ }
1597
+ }
1598
+ cur = cur.return;
1599
+ }
1600
+ return fallback;
1601
+ }
1602
+
1603
+ function buildPath(fiber) {
1604
+ var path = [];
1605
+ var cur = fiber;
1606
+ while (cur) {
1607
+ if (cur.type) {
1608
+ var n = typeof cur.type === 'string'
1609
+ ? cur.type
1610
+ : (cur.type.displayName || cur.type.name);
1611
+ if (n) path.unshift(n);
1612
+ }
1613
+ cur = cur.return;
1614
+ }
1615
+ return path.slice(-8).join(' > ');
1616
+ }
1617
+
1618
+ // Find nearest custom component (skipping RN primitives) for the element name,
1619
+ // but fall back to the nearest named component if nothing custom is found.
1620
+ var named = getNearestNamed(best.fiber.return || best.fiber, true);
1621
+ var result = {
1622
+ point: { x: targetX, y: targetY },
1623
+ element: named ? named.name : best.fiber.type,
1624
+ nativeElement: best.fiber.type,
1625
+ path: buildPath(best.fiber)
1626
+ };
1504
1627
 
1505
- resolve(result);
1628
+ if (${includeFrame}) {
1629
+ result.frame = { x: best.x, y: best.y, width: best.width, height: best.height };
1630
+ }
1631
+
1632
+ if (${includeProps} && named && named.fiber.memoizedProps) {
1633
+ var props = {};
1634
+ var keys = Object.keys(named.fiber.memoizedProps);
1635
+ for (var i = 0; i < keys.length; i++) {
1636
+ var key = keys[i];
1637
+ if (key === 'children') continue;
1638
+ var val = named.fiber.memoizedProps[key];
1639
+ if (typeof val === 'function') {
1640
+ props[key] = '[Function]';
1641
+ } else if (typeof val === 'object' && val !== null) {
1642
+ try {
1643
+ var str = JSON.stringify(val);
1644
+ props[key] = str.length > 200
1645
+ ? (Array.isArray(val) ? '[Array(' + val.length + ')]' : '[Object]')
1646
+ : val;
1647
+ } catch(e) {
1648
+ props[key] = '[Object]';
1506
1649
  }
1507
- );
1508
-
1509
- // Timeout after 3 seconds in case callback never fires
1510
- setTimeout(() => {
1511
- resolve({
1512
- point: { x: ${x}, y: ${y} },
1513
- error: 'Timeout: Inspector callback did not respond. The coordinates may be invalid or outside app bounds.'
1514
- });
1515
- }, 3000);
1516
- } catch (e) {
1517
- resolve({
1518
- point: { x: ${x}, y: ${y} },
1519
- error: 'Exception calling inspector: ' + (e.message || String(e))
1650
+ } else {
1651
+ props[key] = val;
1652
+ }
1653
+ }
1654
+ if (Object.keys(props).length > 0) result.props = props;
1655
+ }
1656
+
1657
+ // Hierarchy: custom-named component for each hit, deduped, innermost→outermost
1658
+ var hierarchy = [];
1659
+ for (var j = 0; j < Math.min(hits.length, 15); j++) {
1660
+ var n2 = getNearestNamed(hits[j].fiber.return, true) || getNearestNamed(hits[j].fiber, true);
1661
+ if (n2 && !hierarchy.some(function(h) { return h.name === n2.name; })) {
1662
+ hierarchy.push({
1663
+ name: n2.name,
1664
+ frame: { x: hits[j].x, y: hits[j].y, width: hits[j].width, height: hits[j].height }
1520
1665
  });
1521
1666
  }
1522
- });
1667
+ }
1668
+ if (hierarchy.length > 1) result.hierarchy = hierarchy;
1669
+
1670
+ return result;
1523
1671
  })()
1524
1672
  `;
1525
- return executeInApp(expression, true);
1673
+ return executeInApp(resolveExpression, false);
1526
1674
  }
1527
1675
  //# sourceMappingURL=executor.js.map