react-native-ai-devtools 1.1.4

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 (147) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +1250 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +108 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  8. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  9. package/build/__tests__/integration/connection-health.test.js +151 -0
  10. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  11. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  12. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/execute-in-app.test.js +115 -0
  14. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.d.ts +2 -0
  16. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/tools.test.js +228 -0
  18. package/build/__tests__/integration/tools.test.js.map +1 -0
  19. package/build/__tests__/setup.d.ts +2 -0
  20. package/build/__tests__/setup.d.ts.map +1 -0
  21. package/build/__tests__/setup.js +11 -0
  22. package/build/__tests__/setup.js.map +1 -0
  23. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  24. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/bundle.test.js +53 -0
  26. package/build/__tests__/unit/bundle.test.js.map +1 -0
  27. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  28. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/connection-health.test.js +28 -0
  30. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  31. package/build/__tests__/unit/executor.test.d.ts +2 -0
  32. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/executor.test.js +79 -0
  34. package/build/__tests__/unit/executor.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.d.ts +2 -0
  36. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  37. package/build/__tests__/unit/logs.test.js +81 -0
  38. package/build/__tests__/unit/logs.test.js.map +1 -0
  39. package/build/__tests__/unit/metro.test.d.ts +2 -0
  40. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/metro.test.js +61 -0
  42. package/build/__tests__/unit/metro.test.js.map +1 -0
  43. package/build/__tests__/unit/network.test.d.ts +2 -0
  44. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/network.test.js +102 -0
  46. package/build/__tests__/unit/network.test.js.map +1 -0
  47. package/build/__tests__/unit/tap.test.d.ts +2 -0
  48. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/tap.test.js +157 -0
  50. package/build/__tests__/unit/tap.test.js.map +1 -0
  51. package/build/core/android.d.ts +265 -0
  52. package/build/core/android.d.ts.map +1 -0
  53. package/build/core/android.js +1413 -0
  54. package/build/core/android.js.map +1 -0
  55. package/build/core/bundle.d.ts +49 -0
  56. package/build/core/bundle.d.ts.map +1 -0
  57. package/build/core/bundle.js +368 -0
  58. package/build/core/bundle.js.map +1 -0
  59. package/build/core/connection.d.ts +43 -0
  60. package/build/core/connection.d.ts.map +1 -0
  61. package/build/core/connection.js +963 -0
  62. package/build/core/connection.js.map +1 -0
  63. package/build/core/connectionState.d.ts +108 -0
  64. package/build/core/connectionState.d.ts.map +1 -0
  65. package/build/core/connectionState.js +284 -0
  66. package/build/core/connectionState.js.map +1 -0
  67. package/build/core/errorScreenParser.d.ts +30 -0
  68. package/build/core/errorScreenParser.d.ts.map +1 -0
  69. package/build/core/errorScreenParser.js +198 -0
  70. package/build/core/errorScreenParser.js.map +1 -0
  71. package/build/core/executor.d.ts +113 -0
  72. package/build/core/executor.d.ts.map +1 -0
  73. package/build/core/executor.js +1877 -0
  74. package/build/core/executor.js.map +1 -0
  75. package/build/core/format.d.ts +8 -0
  76. package/build/core/format.d.ts.map +1 -0
  77. package/build/core/format.js +34 -0
  78. package/build/core/format.js.map +1 -0
  79. package/build/core/guides.d.ts +14 -0
  80. package/build/core/guides.d.ts.map +1 -0
  81. package/build/core/guides.js +261 -0
  82. package/build/core/guides.js.map +1 -0
  83. package/build/core/httpServer.d.ts +14 -0
  84. package/build/core/httpServer.d.ts.map +1 -0
  85. package/build/core/httpServer.js +2459 -0
  86. package/build/core/httpServer.js.map +1 -0
  87. package/build/core/httpServerProcess.d.ts +25 -0
  88. package/build/core/httpServerProcess.d.ts.map +1 -0
  89. package/build/core/httpServerProcess.js +153 -0
  90. package/build/core/httpServerProcess.js.map +1 -0
  91. package/build/core/index.d.ts +25 -0
  92. package/build/core/index.d.ts.map +1 -0
  93. package/build/core/index.js +53 -0
  94. package/build/core/index.js.map +1 -0
  95. package/build/core/ios.d.ts +214 -0
  96. package/build/core/ios.d.ts.map +1 -0
  97. package/build/core/ios.js +1232 -0
  98. package/build/core/ios.js.map +1 -0
  99. package/build/core/logs.d.ts +43 -0
  100. package/build/core/logs.d.ts.map +1 -0
  101. package/build/core/logs.js +144 -0
  102. package/build/core/logs.js.map +1 -0
  103. package/build/core/metro.d.ts +23 -0
  104. package/build/core/metro.d.ts.map +1 -0
  105. package/build/core/metro.js +96 -0
  106. package/build/core/metro.js.map +1 -0
  107. package/build/core/network.d.ts +43 -0
  108. package/build/core/network.d.ts.map +1 -0
  109. package/build/core/network.js +217 -0
  110. package/build/core/network.js.map +1 -0
  111. package/build/core/networkInterceptor.d.ts +3 -0
  112. package/build/core/networkInterceptor.d.ts.map +1 -0
  113. package/build/core/networkInterceptor.js +203 -0
  114. package/build/core/networkInterceptor.js.map +1 -0
  115. package/build/core/ocr.d.ts +69 -0
  116. package/build/core/ocr.d.ts.map +1 -0
  117. package/build/core/ocr.js +212 -0
  118. package/build/core/ocr.js.map +1 -0
  119. package/build/core/state.d.ts +17 -0
  120. package/build/core/state.d.ts.map +1 -0
  121. package/build/core/state.js +50 -0
  122. package/build/core/state.js.map +1 -0
  123. package/build/core/tap.d.ts +91 -0
  124. package/build/core/tap.d.ts.map +1 -0
  125. package/build/core/tap.js +542 -0
  126. package/build/core/tap.js.map +1 -0
  127. package/build/core/telemetry.d.ts +4 -0
  128. package/build/core/telemetry.d.ts.map +1 -0
  129. package/build/core/telemetry.js +289 -0
  130. package/build/core/telemetry.js.map +1 -0
  131. package/build/core/types.d.ts +134 -0
  132. package/build/core/types.d.ts.map +1 -0
  133. package/build/core/types.js +2 -0
  134. package/build/core/types.js.map +1 -0
  135. package/build/httpServerStandalone.d.ts +7 -0
  136. package/build/httpServerStandalone.d.ts.map +1 -0
  137. package/build/httpServerStandalone.js +31 -0
  138. package/build/httpServerStandalone.js.map +1 -0
  139. package/build/index.d.ts +3 -0
  140. package/build/index.d.ts.map +1 -0
  141. package/build/index.js +3012 -0
  142. package/build/index.js.map +1 -0
  143. package/build/pro/tap.d.ts +91 -0
  144. package/build/pro/tap.d.ts.map +1 -0
  145. package/build/pro/tap.js +542 -0
  146. package/build/pro/tap.js.map +1 -0
  147. package/package.json +63 -0
@@ -0,0 +1,1877 @@
1
+ import WebSocket from "ws";
2
+ import { pendingExecutions, getNextMessageId, connectedApps } from "./state.js";
3
+ import { getFirstConnectedApp, connectToDevice } from "./connection.js";
4
+ import { fetchDevices, selectMainDevice, scanMetroPorts } from "./metro.js";
5
+ import { DEFAULT_RECONNECTION_CONFIG, cancelReconnectionTimer } from "./connectionState.js";
6
+ // Hermes runtime compatibility: polyfill for 'global' which doesn't exist in Hermes
7
+ // In Hermes, globalThis is the standard way to access global scope
8
+ const GLOBAL_POLYFILL = `var global = typeof global !== 'undefined' ? global : globalThis;`;
9
+ /**
10
+ * Check if a string contains emoji or other problematic Unicode characters
11
+ * Hermes has issues with certain UTF-16 surrogate pairs (like emoji)
12
+ */
13
+ export function containsProblematicUnicode(str) {
14
+ // Detect UTF-16 surrogate pairs (emoji and other characters outside BMP)
15
+ // These cause "Invalid UTF-8 code point" errors in Hermes
16
+ // eslint-disable-next-line no-control-regex
17
+ return /[\uD800-\uDFFF]/.test(str);
18
+ }
19
+ /**
20
+ * Strip leading comments from an expression
21
+ * Users often start with // comments which break the (return expr) wrapping
22
+ */
23
+ export function stripLeadingComments(expression) {
24
+ let result = expression;
25
+ // Strip leading whitespace first
26
+ result = result.trimStart();
27
+ // Repeatedly strip leading single-line comments (// ...)
28
+ while (result.startsWith("//")) {
29
+ const newlineIndex = result.indexOf("\n");
30
+ if (newlineIndex === -1) {
31
+ // Entire expression is a comment
32
+ return "";
33
+ }
34
+ result = result.slice(newlineIndex + 1).trimStart();
35
+ }
36
+ // Strip leading multi-line comments (/* ... */)
37
+ while (result.startsWith("/*")) {
38
+ const endIndex = result.indexOf("*/");
39
+ if (endIndex === -1) {
40
+ // Unclosed comment
41
+ return result;
42
+ }
43
+ result = result.slice(endIndex + 2).trimStart();
44
+ }
45
+ return result;
46
+ }
47
+ /**
48
+ * Validate and preprocess an expression before execution
49
+ * Returns cleaned expression or error with helpful message
50
+ */
51
+ export function validateAndPreprocessExpression(expression) {
52
+ // Check for emoji/problematic Unicode before any processing
53
+ if (containsProblematicUnicode(expression)) {
54
+ return {
55
+ valid: false,
56
+ expression,
57
+ error: "Expression contains emoji or special Unicode characters that Hermes cannot compile. " +
58
+ "Please remove emoji and use ASCII characters only."
59
+ };
60
+ }
61
+ // Strip leading comments that would break the expression wrapper
62
+ const cleaned = stripLeadingComments(expression);
63
+ if (!cleaned.trim()) {
64
+ return {
65
+ valid: false,
66
+ expression,
67
+ error: "Expression is empty or contains only comments."
68
+ };
69
+ }
70
+ // Check for top-level async that Hermes doesn't support in Runtime.evaluate
71
+ // Pattern: starts with (async or async keyword at expression level
72
+ const trimmed = cleaned.trim();
73
+ if (trimmed.startsWith("(async") || trimmed.startsWith("async ") || trimmed.startsWith("async(")) {
74
+ return {
75
+ valid: false,
76
+ expression: cleaned,
77
+ error: "Hermes does not support top-level async functions in Runtime.evaluate. " +
78
+ "Instead of `(async () => { ... })()`, use a synchronous approach or " +
79
+ "execute the async code and access the result via a global variable: " +
80
+ "`global.__result = null; myAsyncFn().then(r => global.__result = r)`"
81
+ };
82
+ }
83
+ return {
84
+ valid: true,
85
+ expression: cleaned
86
+ };
87
+ }
88
+ // Error patterns that indicate a stale/destroyed context
89
+ const CONTEXT_ERROR_PATTERNS = [
90
+ "cannot find context",
91
+ "execution context was destroyed",
92
+ "target closed",
93
+ "inspected target navigated",
94
+ "session closed",
95
+ "context with specified id",
96
+ "no execution context",
97
+ "runningdetached"
98
+ ];
99
+ /**
100
+ * Check if an error indicates a stale page context
101
+ */
102
+ function isContextError(error) {
103
+ if (!error)
104
+ return false;
105
+ const lowerError = error.toLowerCase();
106
+ return CONTEXT_ERROR_PATTERNS.some((pattern) => lowerError.includes(pattern));
107
+ }
108
+ /**
109
+ * Simple delay helper
110
+ */
111
+ function delay(ms) {
112
+ return new Promise((resolve) => setTimeout(resolve, ms));
113
+ }
114
+ /**
115
+ * Attempt quick reconnection to Metro
116
+ */
117
+ async function attemptQuickReconnect(preferredPort) {
118
+ try {
119
+ const ports = await scanMetroPorts();
120
+ const targetPort = preferredPort && ports.includes(preferredPort) ? preferredPort : ports[0];
121
+ if (!targetPort)
122
+ return false;
123
+ const devices = await fetchDevices(targetPort);
124
+ const mainDevice = selectMainDevice(devices);
125
+ if (!mainDevice)
126
+ return false;
127
+ await connectToDevice(mainDevice, targetPort);
128
+ return true;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ /**
135
+ * Execute expression on a connected app (core implementation without retry)
136
+ */
137
+ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000) {
138
+ const app = getFirstConnectedApp();
139
+ if (!app) {
140
+ return { success: false, error: "No apps connected. Run 'scan_metro' first." };
141
+ }
142
+ if (app.ws.readyState !== WebSocket.OPEN) {
143
+ return { success: false, error: "WebSocket connection is not open." };
144
+ }
145
+ // Validate and preprocess the expression
146
+ const validation = validateAndPreprocessExpression(expression);
147
+ if (!validation.valid) {
148
+ return { success: false, error: validation.error };
149
+ }
150
+ const cleanedExpression = validation.expression;
151
+ const TIMEOUT_MS = timeoutMs;
152
+ const currentMessageId = getNextMessageId();
153
+ // Prepend global polyfill for Hermes compatibility
154
+ // Runtime.evaluate returns the completion value of the last expression naturally,
155
+ // so no IIFE wrapping is needed (similar to browser console behavior)
156
+ const wrappedExpression = `${GLOBAL_POLYFILL} ${cleanedExpression}`;
157
+ return new Promise((resolve) => {
158
+ const timeoutId = setTimeout(() => {
159
+ pendingExecutions.delete(currentMessageId);
160
+ resolve({ success: false, error: "Timeout: Expression took too long to evaluate" });
161
+ }, TIMEOUT_MS);
162
+ pendingExecutions.set(currentMessageId, { resolve, timeoutId });
163
+ try {
164
+ app.ws.send(JSON.stringify({
165
+ id: currentMessageId,
166
+ method: "Runtime.evaluate",
167
+ params: {
168
+ expression: wrappedExpression,
169
+ returnByValue: true,
170
+ awaitPromise,
171
+ userGesture: true,
172
+ generatePreview: true
173
+ }
174
+ }));
175
+ }
176
+ catch (error) {
177
+ clearTimeout(timeoutId);
178
+ pendingExecutions.delete(currentMessageId);
179
+ resolve({
180
+ success: false,
181
+ error: `Failed to send: ${error instanceof Error ? error.message : String(error)}`
182
+ });
183
+ }
184
+ });
185
+ }
186
+ // Execute JavaScript in the connected React Native app with retry logic
187
+ export async function executeInApp(expression, awaitPromise = true, options = {}) {
188
+ const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
189
+ let lastError;
190
+ let preferredPort;
191
+ // Get preferred port from current connection if available
192
+ const currentApp = getFirstConnectedApp();
193
+ if (currentApp) {
194
+ preferredPort = currentApp.port;
195
+ }
196
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
197
+ const app = getFirstConnectedApp();
198
+ // No connection - try to reconnect if enabled
199
+ if (!app) {
200
+ if (autoReconnect && attempt < maxRetries) {
201
+ console.error(`[rn-ai-debugger] No connection, attempting reconnect (attempt ${attempt + 1}/${maxRetries})...`);
202
+ const reconnected = await attemptQuickReconnect(preferredPort);
203
+ if (reconnected) {
204
+ await delay(retryDelayMs);
205
+ continue;
206
+ }
207
+ }
208
+ return { success: false, error: "No apps connected. Run 'scan_metro' first." };
209
+ }
210
+ // WebSocket not open - try to reconnect
211
+ if (app.ws.readyState !== WebSocket.OPEN) {
212
+ if (autoReconnect && attempt < maxRetries) {
213
+ console.error(`[rn-ai-debugger] WebSocket not open, attempting reconnect (attempt ${attempt + 1}/${maxRetries})...`);
214
+ // Close stale connection
215
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
216
+ cancelReconnectionTimer(appKey);
217
+ try {
218
+ app.ws.close();
219
+ }
220
+ catch {
221
+ /* ignore */
222
+ }
223
+ connectedApps.delete(appKey);
224
+ const reconnected = await attemptQuickReconnect(app.port);
225
+ if (reconnected) {
226
+ await delay(retryDelayMs);
227
+ continue;
228
+ }
229
+ }
230
+ return { success: false, error: "WebSocket connection is not open." };
231
+ }
232
+ // Execute the expression
233
+ const result = await executeExpressionCore(expression, awaitPromise, timeoutMs);
234
+ // Success - return result
235
+ if (result.success) {
236
+ return result;
237
+ }
238
+ lastError = result.error;
239
+ // Check if this is a context error that might be recoverable
240
+ if (isContextError(result.error)) {
241
+ if (autoReconnect && attempt < maxRetries) {
242
+ console.error(`[rn-ai-debugger] Context error detected, attempting reconnect (attempt ${attempt + 1}/${maxRetries})...`);
243
+ // Close and reconnect
244
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
245
+ cancelReconnectionTimer(appKey);
246
+ try {
247
+ app.ws.close();
248
+ }
249
+ catch {
250
+ /* ignore */
251
+ }
252
+ connectedApps.delete(appKey);
253
+ const reconnected = await attemptQuickReconnect(app.port);
254
+ if (reconnected) {
255
+ await delay(retryDelayMs);
256
+ continue;
257
+ }
258
+ }
259
+ }
260
+ // Non-context error or no more retries - return error
261
+ return result;
262
+ }
263
+ return {
264
+ success: false,
265
+ error: lastError ?? "Execution failed after all retries. Connection may be stale."
266
+ };
267
+ }
268
+ // List globally available debugging objects in the app
269
+ export async function listDebugGlobals() {
270
+ const expression = `
271
+ (function() {
272
+ const globals = Object.keys(globalThis);
273
+ const categories = {
274
+ 'Apollo Client': globals.filter(k => k.includes('APOLLO')),
275
+ 'Redux': globals.filter(k => k.includes('REDUX')),
276
+ 'React DevTools': globals.filter(k => k.includes('REACT_DEVTOOLS')),
277
+ 'Reanimated': globals.filter(k => k.includes('reanimated') || k.includes('worklet')),
278
+ 'Expo': globals.filter(k => k.includes('Expo') || k.includes('expo')),
279
+ 'Metro': globals.filter(k => k.includes('METRO')),
280
+ 'Other Debug': globals.filter(k => k.startsWith('__') && !k.includes('APOLLO') && !k.includes('REDUX') && !k.includes('REACT_DEVTOOLS') && !k.includes('reanimated') && !k.includes('worklet') && !k.includes('Expo') && !k.includes('expo') && !k.includes('METRO'))
281
+ };
282
+ return categories;
283
+ })()
284
+ `;
285
+ return executeInApp(expression, false);
286
+ }
287
+ // Inspect a global object to see its properties and types
288
+ export async function inspectGlobal(objectName) {
289
+ const expression = `
290
+ (function() {
291
+ const obj = ${objectName};
292
+ if (obj === undefined) return { error: 'Object not found' };
293
+ const result = {};
294
+ for (const key of Object.keys(obj)) {
295
+ const val = obj[key];
296
+ const type = typeof val;
297
+ if (type === 'function') {
298
+ result[key] = { type: 'function', callable: true };
299
+ } else if (type === 'object' && val !== null) {
300
+ result[key] = { type: Array.isArray(val) ? 'array' : 'object', callable: false, preview: JSON.stringify(val).slice(0, 100) };
301
+ } else {
302
+ result[key] = { type, callable: false, value: val };
303
+ }
304
+ }
305
+ return result;
306
+ })()
307
+ `;
308
+ return executeInApp(expression, false);
309
+ }
310
+ // Reload the React Native app using __ReactRefresh (Page.reload is not supported by Hermes)
311
+ // Uses fire-and-forget: sends the reload command without waiting for a response,
312
+ // since the JS context is destroyed during reload and would always timeout.
313
+ export async function reloadApp() {
314
+ // Get current connection info before reload
315
+ let app = getFirstConnectedApp();
316
+ // Auto-connect if no connection exists
317
+ if (!app) {
318
+ console.error("[rn-ai-debugger] No connection for reload, attempting auto-connect...");
319
+ // Try to find and connect to a Metro server
320
+ const ports = await scanMetroPorts();
321
+ if (ports.length === 0) {
322
+ return {
323
+ success: false,
324
+ error: "No apps connected and no Metro server found. Make sure Metro bundler is running (npm start or expo start), then try again."
325
+ };
326
+ }
327
+ // Try to connect to the first available Metro server
328
+ for (const port of ports) {
329
+ const devices = await fetchDevices(port);
330
+ const mainDevice = selectMainDevice(devices);
331
+ if (mainDevice) {
332
+ try {
333
+ await connectToDevice(mainDevice, port);
334
+ console.error(`[rn-ai-debugger] Auto-connected to ${mainDevice.title} on port ${port}`);
335
+ app = getFirstConnectedApp();
336
+ break;
337
+ }
338
+ catch (error) {
339
+ console.error(`[rn-ai-debugger] Failed to connect to port ${port}: ${error}`);
340
+ }
341
+ }
342
+ }
343
+ // Check if auto-connect succeeded
344
+ if (!app) {
345
+ return {
346
+ success: false,
347
+ error: "No apps connected. Found Metro server but could not connect to any device. Make sure the React Native app is running."
348
+ };
349
+ }
350
+ }
351
+ const port = app.port;
352
+ // Fire-and-forget: send reload command via CDP without waiting for response.
353
+ // The JS context is destroyed during reload, so Runtime.evaluate would always timeout.
354
+ const reloadExpression = `(function() {
355
+ try {
356
+ if (typeof __ReactRefresh !== 'undefined' && typeof __ReactRefresh.performFullRefresh === 'function') {
357
+ __ReactRefresh.performFullRefresh('mcp-reload');
358
+ return 'ok';
359
+ }
360
+ if (typeof global !== 'undefined' && global.DevSettings && typeof global.DevSettings.reload === 'function') {
361
+ global.DevSettings.reload();
362
+ return 'ok';
363
+ }
364
+ return 'no-method';
365
+ } catch (e) { return 'error:' + e.message; }
366
+ })()`;
367
+ try {
368
+ if (app.ws.readyState !== WebSocket.OPEN) {
369
+ return { success: false, error: "WebSocket connection is not open." };
370
+ }
371
+ // Send without registering a pending execution — fire and forget
372
+ const messageId = getNextMessageId();
373
+ app.ws.send(JSON.stringify({
374
+ id: messageId,
375
+ method: "Runtime.evaluate",
376
+ params: {
377
+ expression: reloadExpression,
378
+ returnByValue: true,
379
+ awaitPromise: false,
380
+ userGesture: true
381
+ }
382
+ }));
383
+ }
384
+ catch (error) {
385
+ return {
386
+ success: false,
387
+ error: `Failed to send reload command: ${error instanceof Error ? error.message : String(error)}`
388
+ };
389
+ }
390
+ // Auto-reconnect after reload
391
+ try {
392
+ // Wait for app to reload (give it time to restart JS context)
393
+ await delay(2000);
394
+ // Close existing connections to this port and cancel any pending auto-reconnections
395
+ // This prevents the dual-reconnection bug where both auto-reconnect and manual reconnect compete
396
+ for (const [key, connectedApp] of connectedApps.entries()) {
397
+ if (connectedApp.port === port) {
398
+ // Cancel any pending reconnection timer BEFORE closing
399
+ cancelReconnectionTimer(key);
400
+ try {
401
+ connectedApp.ws.close();
402
+ }
403
+ catch {
404
+ // Ignore close errors
405
+ }
406
+ connectedApps.delete(key);
407
+ }
408
+ }
409
+ // Small delay to ensure cleanup
410
+ await delay(500);
411
+ // Reconnect to Metro on the same port with auto-reconnection DISABLED
412
+ // We're doing a manual reconnection here, so we don't want the auto-reconnect
413
+ // system to also try reconnecting and compete with us
414
+ const devices = await fetchDevices(port);
415
+ const mainDevice = selectMainDevice(devices);
416
+ if (mainDevice) {
417
+ await connectToDevice(mainDevice, port, {
418
+ isReconnection: false,
419
+ reconnectionConfig: { ...DEFAULT_RECONNECTION_CONFIG, enabled: false }
420
+ });
421
+ return {
422
+ success: true,
423
+ result: `App reloaded and reconnected to ${mainDevice.title}`
424
+ };
425
+ }
426
+ else {
427
+ return {
428
+ success: true,
429
+ result: "App reloaded but could not auto-reconnect. Run 'scan_metro' to reconnect."
430
+ };
431
+ }
432
+ }
433
+ catch (error) {
434
+ return {
435
+ success: true,
436
+ result: `App reloaded but auto-reconnect failed: ${error instanceof Error ? error.message : String(error)}. Run 'scan_metro' to reconnect.`
437
+ };
438
+ }
439
+ }
440
+ function formatTreeToTonl(node, indent = 0) {
441
+ const prefix = " ".repeat(indent);
442
+ let result = `${prefix}${node.component}`;
443
+ // Add props inline if present
444
+ if (node.props && Object.keys(node.props).length > 0) {
445
+ const propsStr = Object.entries(node.props)
446
+ .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
447
+ .join(",");
448
+ result += ` (${propsStr})`;
449
+ }
450
+ // Add layout inline if present
451
+ if (node.layout && Object.keys(node.layout).length > 0) {
452
+ const layoutStr = Object.entries(node.layout)
453
+ .map(([k, v]) => `${k}:${v}`)
454
+ .join(",");
455
+ result += ` [${layoutStr}]`;
456
+ }
457
+ result += "\n";
458
+ // Recurse children
459
+ if (node.children && node.children.length > 0) {
460
+ for (const child of node.children) {
461
+ result += formatTreeToTonl(child, indent + 1);
462
+ }
463
+ }
464
+ return result;
465
+ }
466
+ // Ultra-compact structure-only tree format (just component names, indented)
467
+ function formatTreeStructureOnly(node, indent = 0) {
468
+ const prefix = " ".repeat(indent);
469
+ let result = `${prefix}${node.component}\n`;
470
+ if (node.children && node.children.length > 0) {
471
+ for (const child of node.children) {
472
+ result += formatTreeStructureOnly(child, indent + 1);
473
+ }
474
+ }
475
+ return result;
476
+ }
477
+ function formatScreenLayoutToTonl(elements) {
478
+ const lines = ["#elements{component,path,depth,layout,id}"];
479
+ for (const el of elements) {
480
+ const layout = el.layout
481
+ ? Object.entries(el.layout)
482
+ .map(([k, v]) => `${k}:${v}`)
483
+ .join(";")
484
+ : "";
485
+ const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
486
+ lines.push(`${el.component}|${el.path}|${el.depth}|${layout}|${id}`);
487
+ }
488
+ return lines.join("\n");
489
+ }
490
+ function formatFoundComponentsToTonl(components) {
491
+ const lines = ["#found{component,path,depth,key,layout}"];
492
+ for (const c of components) {
493
+ const layout = c.layout
494
+ ? Object.entries(c.layout)
495
+ .map(([k, v]) => `${k}:${v}`)
496
+ .join(";")
497
+ : "";
498
+ lines.push(`${c.component}|${c.path}|${c.depth}|${c.key || ""}|${layout}`);
499
+ }
500
+ return lines.join("\n");
501
+ }
502
+ function formatSummaryToTonl(components, total) {
503
+ const lines = [`#summary total=${total}`];
504
+ for (const c of components) {
505
+ lines.push(`${c.component}:${c.count}`);
506
+ }
507
+ return lines.join("\n");
508
+ }
509
+ /**
510
+ * Get the React component tree from the running app.
511
+ * This traverses the fiber tree to extract component hierarchy with names.
512
+ */
513
+ export async function getComponentTree(options = {}) {
514
+ const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false } = options;
515
+ // Use lower default depth for structureOnly to keep output compact (~2-5KB)
516
+ // Full mode uses higher depth since TONL format handles it better
517
+ // focusedOnly mode uses moderate depth since we're already filtering to active screen
518
+ const maxDepth = options.maxDepth ?? (structureOnly ? (focusedOnly ? 25 : 40) : 100);
519
+ const expression = `
520
+ (function() {
521
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
522
+ if (!hook) return { error: 'React DevTools hook not found. Make sure you are running a development build.' };
523
+
524
+ // Try to get fiber roots (renderer ID is usually 1)
525
+ let roots = [];
526
+ if (hook.getFiberRoots) {
527
+ roots = [...(hook.getFiberRoots(1) || [])];
528
+ }
529
+ if (roots.length === 0 && hook.renderers) {
530
+ // Try all renderers
531
+ for (const [id] of hook.renderers) {
532
+ const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
533
+ if (r.length > 0) {
534
+ roots = r;
535
+ break;
536
+ }
537
+ }
538
+ }
539
+ if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
540
+
541
+ const maxDepth = ${maxDepth};
542
+ const includeProps = ${includeProps};
543
+ const includeStyles = ${includeStyles};
544
+ const hideInternals = ${hideInternals};
545
+ const focusedOnly = ${focusedOnly};
546
+
547
+ // Internal RN components to hide
548
+ const internalPatterns = /^(RCT|RNS|Animated\\(|AnimatedComponent|VirtualizedList|CellRenderer|ScrollViewContext|PerformanceLoggerContext|RootTagContext|HeaderShownContext|HeaderHeightContext|HeaderBackContext|SafeAreaFrameContext|SafeAreaInsetsContext|VirtualizedListContext|VirtualizedListCellContextProvider|StaticContainer|DelayedFreeze|Freeze|Suspender|DebugContainer|MaybeNestedStack|SceneView|NavigationContent|PreventRemoveProvider|EnsureSingleNavigator)/;
549
+
550
+ // Screen component patterns - user's actual screens (strict matching)
551
+ // Only match *Screen and *Page to avoid false positives like BottomTabView
552
+ const screenPatterns = /^[A-Z][a-zA-Z0-9]*(Screen|Page)$/;
553
+
554
+ // Navigation/internal screen patterns to SKIP (these look like screens but are framework components)
555
+ const internalScreenPatterns = /^(MaybeScreen|Screen$|ScreenContainer|ScreenStack|SceneView|Background$)/;
556
+
557
+ // Provider/wrapper patterns to skip when finding focused screen
558
+ const wrapperPatterns = /^(App|AppContainer|Provider|Context|SafeArea|Gesture|Theme|Redux|Root|Navigator|Stack|Tab|Drawer|Navigation|Container|Wrapper|Layout|ErrorBoundary|Suspense|PersistGate|LinkingContext|AppState|View|Fragment|NativeStack|BottomTab|Screen$)/i;
559
+
560
+ // Global overlay patterns - stop traversing into these subtrees
561
+ // Be specific to avoid blocking BottomSheetDrawer, PortalProvider, etc.
562
+ const overlayPatterns = /^(BottomSheet$|BottomSheetGlobal|Modal$|Toast$|Snackbar$|Dialog$|Overlay$|Popup$|MyToast$|PaywallModal$|FullScreenBannerModal$)/i;
563
+
564
+ // Navigation container patterns - skip traversing into these (screens inside are nav screens, not focused content)
565
+ const navContainerPatterns = /^(RootNavigation|NativeStackNavigator|BottomTabNavigator|DrawerNavigator|TabNavigator|StackNavigator)/;
566
+
567
+ function getComponentName(fiber) {
568
+ if (!fiber || !fiber.type) return null;
569
+ if (typeof fiber.type === 'string') return fiber.type; // Host component (View, Text, etc.)
570
+ return fiber.type.displayName || fiber.type.name || null;
571
+ }
572
+
573
+ function shouldHide(name) {
574
+ if (!hideInternals || !name) return false;
575
+ return internalPatterns.test(name);
576
+ }
577
+
578
+ function extractLayoutStyles(style) {
579
+ if (!style) return null;
580
+ const merged = Array.isArray(style)
581
+ ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
582
+ : (typeof style === 'object' ? style : {});
583
+
584
+ const layout = {};
585
+ const layoutKeys = [
586
+ 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
587
+ 'paddingHorizontal', 'paddingVertical',
588
+ 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
589
+ 'marginHorizontal', 'marginVertical',
590
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
591
+ 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
592
+ 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
593
+ 'position', 'top', 'bottom', 'left', 'right',
594
+ 'gap', 'rowGap', 'columnGap',
595
+ 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
596
+ ];
597
+
598
+ for (const key of layoutKeys) {
599
+ if (merged[key] !== undefined) layout[key] = merged[key];
600
+ }
601
+ return Object.keys(layout).length > 0 ? layout : null;
602
+ }
603
+
604
+ function walkFiber(fiber, depth) {
605
+ if (!fiber || depth > maxDepth) return null;
606
+
607
+ const name = getComponentName(fiber);
608
+
609
+ // Skip anonymous/internal components unless they have meaningful children
610
+ if (!name || shouldHide(name)) {
611
+ // Still traverse children
612
+ let child = fiber.child;
613
+ const children = [];
614
+ while (child) {
615
+ const childResult = walkFiber(child, depth);
616
+ if (childResult) children.push(childResult);
617
+ child = child.sibling;
618
+ }
619
+ // Return first meaningful child or null
620
+ return children.length === 1 ? children[0] : (children.length > 1 ? { component: '(Fragment)', children } : null);
621
+ }
622
+
623
+ const node = { component: name };
624
+
625
+ // Include props if requested (excluding children and style for cleaner output)
626
+ if (includeProps && fiber.memoizedProps) {
627
+ const props = {};
628
+ for (const key of Object.keys(fiber.memoizedProps)) {
629
+ if (key === 'children' || key === 'style') continue;
630
+ const val = fiber.memoizedProps[key];
631
+ if (typeof val === 'function') {
632
+ props[key] = '[Function]';
633
+ } else if (typeof val === 'object' && val !== null) {
634
+ props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
635
+ } else {
636
+ props[key] = val;
637
+ }
638
+ }
639
+ if (Object.keys(props).length > 0) node.props = props;
640
+ }
641
+
642
+ // Include layout styles if requested
643
+ if (includeStyles && fiber.memoizedProps?.style) {
644
+ const layout = extractLayoutStyles(fiber.memoizedProps.style);
645
+ if (layout) node.layout = layout;
646
+ }
647
+
648
+ // Traverse children
649
+ let child = fiber.child;
650
+ const children = [];
651
+ while (child) {
652
+ const childResult = walkFiber(child, depth + 1);
653
+ if (childResult) children.push(childResult);
654
+ child = child.sibling;
655
+ }
656
+ if (children.length > 0) node.children = children;
657
+
658
+ return node;
659
+ }
660
+
661
+ // Find focused screen if requested
662
+ function findFocusedScreen(fiber, depth = 0) {
663
+ if (!fiber || depth > 200) return null;
664
+
665
+ const name = getComponentName(fiber);
666
+
667
+ // Skip overlays (BottomSheet, Modal, Toast, etc.) - don't traverse into them
668
+ if (name && overlayPatterns.test(name)) {
669
+ return null;
670
+ }
671
+
672
+ // Skip navigation containers - screens inside are nav screens, not focused content
673
+ if (name && navContainerPatterns.test(name)) {
674
+ return null;
675
+ }
676
+
677
+ // Check if this is a user's screen component (not framework internals)
678
+ if (name && screenPatterns.test(name) && !wrapperPatterns.test(name) && !internalScreenPatterns.test(name)) {
679
+ return fiber;
680
+ }
681
+
682
+ // Search children
683
+ let child = fiber.child;
684
+ while (child) {
685
+ const found = findFocusedScreen(child, depth + 1);
686
+ if (found) return found;
687
+ child = child.sibling;
688
+ }
689
+
690
+ return null;
691
+ }
692
+
693
+ let startFiber = roots[0].current;
694
+ let focusedScreenName = null;
695
+
696
+ if (focusedOnly) {
697
+ const focused = findFocusedScreen(roots[0].current);
698
+ if (focused) {
699
+ startFiber = focused;
700
+ focusedScreenName = getComponentName(focused);
701
+ }
702
+ }
703
+
704
+ const tree = walkFiber(startFiber, 0);
705
+
706
+ if (focusedOnly && focusedScreenName) {
707
+ return { focusedScreen: focusedScreenName, tree };
708
+ }
709
+ return { tree };
710
+ })()
711
+ `;
712
+ // Use a longer timeout for component tree traversal — large apps can exceed 10s
713
+ const result = await executeInApp(expression, false, { timeoutMs: 30000 });
714
+ // Apply formatting if requested
715
+ if (result.success && result.result) {
716
+ try {
717
+ const parsed = JSON.parse(result.result);
718
+ if (parsed.tree) {
719
+ const prefix = parsed.focusedScreen ? `Focused: ${parsed.focusedScreen}\n\n` : "";
720
+ // Structure-only mode: ultra-compact format with just component names
721
+ if (structureOnly) {
722
+ const structure = formatTreeStructureOnly(parsed.tree);
723
+ return { success: true, result: prefix + structure };
724
+ }
725
+ // TONL format: compact with props/layout
726
+ if (format === "tonl") {
727
+ const tonl = formatTreeToTonl(parsed.tree);
728
+ return { success: true, result: prefix + tonl };
729
+ }
730
+ }
731
+ }
732
+ catch {
733
+ // If parsing fails, return original result
734
+ }
735
+ }
736
+ return result;
737
+ }
738
+ /**
739
+ * Get layout styles for all components on the current screen.
740
+ * Useful for verifying layout without screenshots.
741
+ */
742
+ export async function getScreenLayout(options = {}) {
743
+ const { maxDepth = 65, componentsOnly = false, shortPath = true, summary = false, format = "tonl" } = options;
744
+ const expression = `
745
+ (function() {
746
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
747
+ if (!hook) return { error: 'React DevTools hook not found.' };
748
+
749
+ let roots = [];
750
+ if (hook.getFiberRoots) {
751
+ roots = [...(hook.getFiberRoots(1) || [])];
752
+ }
753
+ if (roots.length === 0 && hook.renderers) {
754
+ for (const [id] of hook.renderers) {
755
+ const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
756
+ if (r.length > 0) { roots = r; break; }
757
+ }
758
+ }
759
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
760
+
761
+ const maxDepth = ${maxDepth};
762
+ const componentsOnly = ${componentsOnly};
763
+ const shortPath = ${shortPath};
764
+ const summaryMode = ${summary};
765
+ const pathSegments = 3; // Number of path segments to show in shortPath mode
766
+
767
+ function getComponentName(fiber) {
768
+ if (!fiber || !fiber.type) return null;
769
+ if (typeof fiber.type === 'string') return fiber.type;
770
+ return fiber.type.displayName || fiber.type.name || null;
771
+ }
772
+
773
+ function isHostComponent(fiber) {
774
+ return typeof fiber?.type === 'string';
775
+ }
776
+
777
+ function formatPath(pathArray) {
778
+ if (!shortPath || pathArray.length <= pathSegments) {
779
+ return pathArray.join(' > ');
780
+ }
781
+ return '... > ' + pathArray.slice(-pathSegments).join(' > ');
782
+ }
783
+
784
+ function extractAllStyles(style) {
785
+ if (!style) return null;
786
+ const merged = Array.isArray(style)
787
+ ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
788
+ : (typeof style === 'object' ? style : {});
789
+ return Object.keys(merged).length > 0 ? merged : null;
790
+ }
791
+
792
+ function extractLayoutStyles(style) {
793
+ if (!style) return null;
794
+ const merged = Array.isArray(style)
795
+ ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
796
+ : (typeof style === 'object' ? style : {});
797
+
798
+ const layout = {};
799
+ const layoutKeys = [
800
+ 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
801
+ 'paddingHorizontal', 'paddingVertical',
802
+ 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
803
+ 'marginHorizontal', 'marginVertical',
804
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
805
+ 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
806
+ 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
807
+ 'position', 'top', 'bottom', 'left', 'right',
808
+ 'gap', 'rowGap', 'columnGap',
809
+ 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
810
+ 'backgroundColor', 'borderColor', 'borderRadius'
811
+ ];
812
+
813
+ for (const key of layoutKeys) {
814
+ if (merged[key] !== undefined) layout[key] = merged[key];
815
+ }
816
+ return Object.keys(layout).length > 0 ? layout : null;
817
+ }
818
+
819
+ const elements = [];
820
+
821
+ function walkFiber(fiber, depth, path) {
822
+ if (!fiber || depth > maxDepth) return;
823
+
824
+ const name = getComponentName(fiber);
825
+ const isHost = isHostComponent(fiber);
826
+
827
+ // Include host components (View, Text, etc.) or named components
828
+ if (name && (!componentsOnly || !isHost)) {
829
+ const style = fiber.memoizedProps?.style;
830
+ const layout = extractLayoutStyles(style);
831
+
832
+ // Get text content if it's a Text component
833
+ let textContent = null;
834
+ if (name === 'Text' || name === 'RCTText') {
835
+ const children = fiber.memoizedProps?.children;
836
+ if (typeof children === 'string') textContent = children;
837
+ else if (typeof children === 'number') textContent = String(children);
838
+ }
839
+
840
+ const element = {
841
+ component: name,
842
+ path: formatPath(path),
843
+ depth
844
+ };
845
+
846
+ if (layout) element.layout = layout;
847
+ if (textContent) element.text = textContent.slice(0, 100);
848
+
849
+ // Include key props for identification
850
+ if (fiber.memoizedProps) {
851
+ const identifiers = {};
852
+ if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
853
+ if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
854
+ if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
855
+ if (fiber.key) identifiers.key = fiber.key;
856
+ if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
857
+ }
858
+
859
+ elements.push(element);
860
+ }
861
+
862
+ // Traverse children
863
+ let child = fiber.child;
864
+ while (child) {
865
+ const childName = getComponentName(child);
866
+ walkFiber(child, depth + 1, childName ? [...path, childName] : path);
867
+ child = child.sibling;
868
+ }
869
+ }
870
+
871
+ walkFiber(roots[0].current, 0, []);
872
+
873
+ // Summary mode: return counts by component name
874
+ if (summaryMode) {
875
+ const counts = {};
876
+ for (const el of elements) {
877
+ counts[el.component] = (counts[el.component] || 0) + 1;
878
+ }
879
+ // Sort by count descending
880
+ const sorted = Object.entries(counts)
881
+ .sort((a, b) => b[1] - a[1])
882
+ .map(([name, count]) => ({ component: name, count }));
883
+ return {
884
+ totalElements: elements.length,
885
+ uniqueComponents: sorted.length,
886
+ components: sorted
887
+ };
888
+ }
889
+
890
+ return {
891
+ totalElements: elements.length,
892
+ elements: elements
893
+ };
894
+ })()
895
+ `;
896
+ // Use a longer timeout for layout traversal — large component trees can exceed 10s
897
+ const result = await executeInApp(expression, false, { timeoutMs: 30000 });
898
+ // Apply TONL formatting if requested
899
+ if (format === "tonl" && result.success && result.result) {
900
+ try {
901
+ const parsed = JSON.parse(result.result);
902
+ if (parsed.components) {
903
+ // Summary mode
904
+ const tonl = formatSummaryToTonl(parsed.components, parsed.totalElements);
905
+ return { success: true, result: tonl };
906
+ }
907
+ else if (parsed.elements) {
908
+ // Full element list
909
+ const tonl = formatScreenLayoutToTonl(parsed.elements);
910
+ return { success: true, result: tonl };
911
+ }
912
+ }
913
+ catch {
914
+ // If parsing fails, return original result
915
+ }
916
+ }
917
+ return result;
918
+ }
919
+ /**
920
+ * Inspect a specific component by name, returning its props, state, and layout.
921
+ */
922
+ export async function inspectComponent(componentName, options = {}) {
923
+ const { index = 0, includeState = true, includeChildren = false, childrenDepth = 1, shortPath = true, simplifyHooks = true } = options;
924
+ const escapedName = componentName.replace(/'/g, "\\'");
925
+ const expression = `
926
+ (function() {
927
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
928
+ if (!hook) return { error: 'React DevTools hook not found.' };
929
+
930
+ let roots = [];
931
+ if (hook.getFiberRoots) {
932
+ roots = [...(hook.getFiberRoots(1) || [])];
933
+ }
934
+ if (roots.length === 0 && hook.renderers) {
935
+ for (const [id] of hook.renderers) {
936
+ const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
937
+ if (r.length > 0) { roots = r; break; }
938
+ }
939
+ }
940
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
941
+
942
+ const targetName = '${escapedName}';
943
+ const targetIndex = ${index};
944
+ const includeState = ${includeState};
945
+ const includeChildren = ${includeChildren};
946
+ const childrenDepth = ${childrenDepth};
947
+ const shortPath = ${shortPath};
948
+ const simplifyHooks = ${simplifyHooks};
949
+ const pathSegments = 3;
950
+
951
+ function getComponentName(fiber) {
952
+ if (!fiber || !fiber.type) return null;
953
+ if (typeof fiber.type === 'string') return fiber.type;
954
+ return fiber.type.displayName || fiber.type.name || null;
955
+ }
956
+
957
+ function formatPath(pathArray) {
958
+ if (!shortPath || pathArray.length <= pathSegments) {
959
+ return pathArray.join(' > ');
960
+ }
961
+ return '... > ' + pathArray.slice(-pathSegments).join(' > ');
962
+ }
963
+
964
+ function extractStyles(style) {
965
+ if (!style) return null;
966
+ const merged = Array.isArray(style)
967
+ ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
968
+ : (typeof style === 'object' ? style : {});
969
+ return Object.keys(merged).length > 0 ? merged : null;
970
+ }
971
+
972
+ function serializeValue(val, depth = 0) {
973
+ if (depth > 3) return '[Max depth]';
974
+ if (val === null) return null;
975
+ if (val === undefined) return undefined;
976
+ if (typeof val === 'function') return '[Function]';
977
+ if (typeof val !== 'object') return val;
978
+ if (Array.isArray(val)) {
979
+ if (val.length > 10) return '[Array(' + val.length + ')]';
980
+ return val.map(v => serializeValue(v, depth + 1));
981
+ }
982
+ // Object
983
+ const keys = Object.keys(val);
984
+ if (keys.length > 20) return '[Object(' + keys.length + ' keys)]';
985
+ const result = {};
986
+ for (const k of keys) {
987
+ result[k] = serializeValue(val[k], depth + 1);
988
+ }
989
+ return result;
990
+ }
991
+
992
+ function getChildTree(fiber, depth) {
993
+ if (!fiber || depth <= 0) return null;
994
+ const children = [];
995
+ let child = fiber?.child;
996
+ while (child && children.length < 30) {
997
+ const name = getComponentName(child);
998
+ if (name) {
999
+ if (depth === 1) {
1000
+ // Just names for depth 1
1001
+ children.push(name);
1002
+ } else {
1003
+ // Tree structure for depth > 1
1004
+ const nestedChildren = getChildTree(child, depth - 1);
1005
+ children.push(nestedChildren ? { component: name, children: nestedChildren } : name);
1006
+ }
1007
+ }
1008
+ child = child.sibling;
1009
+ }
1010
+ return children.length > 0 ? children : null;
1011
+ }
1012
+
1013
+ const matches = [];
1014
+
1015
+ function findComponent(fiber, path) {
1016
+ if (!fiber) return;
1017
+
1018
+ const name = getComponentName(fiber);
1019
+ if (name === targetName) {
1020
+ matches.push({ fiber, path: [...path, name] });
1021
+ }
1022
+
1023
+ let child = fiber.child;
1024
+ while (child) {
1025
+ const childName = getComponentName(child);
1026
+ findComponent(child, childName ? [...path, childName] : path);
1027
+ child = child.sibling;
1028
+ }
1029
+ }
1030
+
1031
+ findComponent(roots[0].current, []);
1032
+
1033
+ if (matches.length === 0) {
1034
+ return { error: 'Component "' + targetName + '" not found in the component tree.' };
1035
+ }
1036
+
1037
+ if (targetIndex >= matches.length) {
1038
+ return { error: 'Component "' + targetName + '" found ' + matches.length + ' times, but index ' + targetIndex + ' requested.' };
1039
+ }
1040
+
1041
+ const { fiber, path } = matches[targetIndex];
1042
+
1043
+ const result = {
1044
+ component: targetName,
1045
+ path: formatPath(path),
1046
+ instancesFound: matches.length,
1047
+ instanceIndex: targetIndex
1048
+ };
1049
+
1050
+ // Props (excluding children)
1051
+ if (fiber.memoizedProps) {
1052
+ const props = {};
1053
+ for (const key of Object.keys(fiber.memoizedProps)) {
1054
+ if (key === 'children') continue;
1055
+ props[key] = serializeValue(fiber.memoizedProps[key]);
1056
+ }
1057
+ result.props = props;
1058
+ }
1059
+
1060
+ // Style separately for clarity
1061
+ if (fiber.memoizedProps?.style) {
1062
+ result.style = extractStyles(fiber.memoizedProps.style);
1063
+ }
1064
+
1065
+ // State (for hooks, this is a linked list)
1066
+ if (includeState && fiber.memoizedState) {
1067
+ // Simplified hook value serialization
1068
+ function serializeHookValue(val, depth = 0) {
1069
+ if (depth > 2) return '[...]';
1070
+ if (val === null || val === undefined) return val;
1071
+ if (typeof val === 'function') return '[Function]';
1072
+ if (typeof val !== 'object') return val;
1073
+ // Skip React internal structures (effects, refs with destroy/create)
1074
+ if (val.create && val.destroy !== undefined) return '[Effect]';
1075
+ if (val.inst && val.deps) return '[Effect]';
1076
+ if (val.current !== undefined && Object.keys(val).length === 1) {
1077
+ // Ref object - just show current value
1078
+ return { current: serializeHookValue(val.current, depth + 1) };
1079
+ }
1080
+ if (Array.isArray(val)) {
1081
+ if (val.length > 5) return '[Array(' + val.length + ')]';
1082
+ return val.slice(0, 5).map(v => serializeHookValue(v, depth + 1));
1083
+ }
1084
+ const keys = Object.keys(val);
1085
+ if (keys.length > 10) return '[Object(' + keys.length + ' keys)]';
1086
+ const result = {};
1087
+ for (const k of keys.slice(0, 10)) {
1088
+ result[k] = serializeHookValue(val[k], depth + 1);
1089
+ }
1090
+ return result;
1091
+ }
1092
+
1093
+ // For function components with hooks
1094
+ const states = [];
1095
+ let state = fiber.memoizedState;
1096
+ let hookIndex = 0;
1097
+ while (state && hookIndex < 20) {
1098
+ if (state.memoizedState !== undefined) {
1099
+ const hookVal = simplifyHooks
1100
+ ? serializeHookValue(state.memoizedState)
1101
+ : serializeValue(state.memoizedState);
1102
+ // Skip effect hooks in simplified mode
1103
+ if (!simplifyHooks || (hookVal !== '[Effect]' && hookVal !== undefined)) {
1104
+ states.push({
1105
+ hookIndex,
1106
+ value: hookVal
1107
+ });
1108
+ }
1109
+ }
1110
+ state = state.next;
1111
+ hookIndex++;
1112
+ }
1113
+ if (states.length > 0) result.hooks = states;
1114
+
1115
+ // For class components, memoizedState is the state object directly
1116
+ if (states.length === 0 && typeof fiber.memoizedState === 'object') {
1117
+ result.state = serializeValue(fiber.memoizedState);
1118
+ }
1119
+ }
1120
+
1121
+ // Children tree (depth controlled by childrenDepth)
1122
+ if (includeChildren) {
1123
+ result.children = getChildTree(fiber, childrenDepth);
1124
+ }
1125
+
1126
+ return result;
1127
+ })()
1128
+ `;
1129
+ return executeInApp(expression, false);
1130
+ }
1131
+ /**
1132
+ * Find all components matching a name pattern and return summary info.
1133
+ */
1134
+ export async function findComponents(pattern, options = {}) {
1135
+ const { maxResults = 20, includeLayout = false, shortPath = true, summary = false, format = "tonl" } = options;
1136
+ const escapedPattern = pattern.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
1137
+ const expression = `
1138
+ (function() {
1139
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1140
+ if (!hook) return { error: 'React DevTools hook not found.' };
1141
+
1142
+ let roots = [];
1143
+ if (hook.getFiberRoots) {
1144
+ roots = [...(hook.getFiberRoots(1) || [])];
1145
+ }
1146
+ if (roots.length === 0 && hook.renderers) {
1147
+ for (const [id] of hook.renderers) {
1148
+ const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
1149
+ if (r.length > 0) { roots = r; break; }
1150
+ }
1151
+ }
1152
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
1153
+
1154
+ const pattern = '${escapedPattern}';
1155
+ const regex = new RegExp(pattern, 'i');
1156
+ const maxResults = ${maxResults};
1157
+ const includeLayout = ${includeLayout};
1158
+ const shortPath = ${shortPath};
1159
+ const summaryMode = ${summary};
1160
+ const pathSegments = 3;
1161
+
1162
+ function getComponentName(fiber) {
1163
+ if (!fiber || !fiber.type) return null;
1164
+ if (typeof fiber.type === 'string') return fiber.type;
1165
+ return fiber.type.displayName || fiber.type.name || null;
1166
+ }
1167
+
1168
+ function formatPath(pathArray) {
1169
+ if (!shortPath || pathArray.length <= pathSegments) {
1170
+ return pathArray.join(' > ');
1171
+ }
1172
+ return '... > ' + pathArray.slice(-pathSegments).join(' > ');
1173
+ }
1174
+
1175
+ function extractLayoutStyles(style) {
1176
+ if (!style) return null;
1177
+ const merged = Array.isArray(style)
1178
+ ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
1179
+ : (typeof style === 'object' ? style : {});
1180
+
1181
+ const layout = {};
1182
+ const keys = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1183
+ 'paddingHorizontal', 'paddingVertical', 'margin', 'marginTop', 'marginBottom',
1184
+ 'marginLeft', 'marginRight', 'marginHorizontal', 'marginVertical',
1185
+ 'width', 'height', 'flex', 'flexDirection', 'justifyContent', 'alignItems'];
1186
+ for (const k of keys) {
1187
+ if (merged[k] !== undefined) layout[k] = merged[k];
1188
+ }
1189
+ return Object.keys(layout).length > 0 ? layout : null;
1190
+ }
1191
+
1192
+ const results = [];
1193
+
1194
+ function search(fiber, path, depth) {
1195
+ if (!fiber || results.length >= maxResults) return;
1196
+
1197
+ const name = getComponentName(fiber);
1198
+ if (name && regex.test(name)) {
1199
+ const entry = {
1200
+ component: name,
1201
+ path: formatPath(path),
1202
+ depth
1203
+ };
1204
+
1205
+ if (fiber.memoizedProps?.testID) entry.testID = fiber.memoizedProps.testID;
1206
+ if (fiber.key) entry.key = fiber.key;
1207
+
1208
+ if (includeLayout && fiber.memoizedProps?.style) {
1209
+ const layout = extractLayoutStyles(fiber.memoizedProps.style);
1210
+ if (layout) entry.layout = layout;
1211
+ }
1212
+
1213
+ results.push(entry);
1214
+ }
1215
+
1216
+ let child = fiber.child;
1217
+ while (child && results.length < maxResults) {
1218
+ const childName = getComponentName(child);
1219
+ search(child, childName ? [...path, childName] : path, depth + 1);
1220
+ child = child.sibling;
1221
+ }
1222
+ }
1223
+
1224
+ search(roots[0].current, [], 0);
1225
+
1226
+ // Summary mode: just return counts by component name
1227
+ if (summaryMode) {
1228
+ const counts = {};
1229
+ for (const r of results) {
1230
+ counts[r.component] = (counts[r.component] || 0) + 1;
1231
+ }
1232
+ const sorted = Object.entries(counts)
1233
+ .sort((a, b) => b[1] - a[1])
1234
+ .map(([name, count]) => ({ component: name, count }));
1235
+ return {
1236
+ pattern,
1237
+ totalMatches: results.length,
1238
+ uniqueComponents: sorted.length,
1239
+ components: sorted
1240
+ };
1241
+ }
1242
+
1243
+ return {
1244
+ pattern,
1245
+ found: results.length,
1246
+ components: results
1247
+ };
1248
+ })()
1249
+ `;
1250
+ const result = await executeInApp(expression, false);
1251
+ // Apply TONL formatting if requested
1252
+ if (format === "tonl" && result.success && result.result) {
1253
+ try {
1254
+ const parsed = JSON.parse(result.result);
1255
+ if (parsed.components) {
1256
+ if (parsed.totalMatches !== undefined) {
1257
+ // Summary mode
1258
+ const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
1259
+ return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
1260
+ }
1261
+ else {
1262
+ // Full list mode
1263
+ const tonl = formatFoundComponentsToTonl(parsed.components);
1264
+ return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
1265
+ }
1266
+ }
1267
+ }
1268
+ catch {
1269
+ // If parsing fails, return original result
1270
+ }
1271
+ }
1272
+ return result;
1273
+ }
1274
+ // ============================================================================
1275
+ // Press Element (invoke onPress via React Fiber Tree)
1276
+ // ============================================================================
1277
+ /**
1278
+ * Find a pressable element in the React fiber tree and invoke its onPress handler.
1279
+ * Matches by text content, testID, or component name.
1280
+ */
1281
+ export async function pressElement(options) {
1282
+ const { text, testID, component, index = 0, maxTraversalDepth = 15 } = options;
1283
+ if (!text && !testID && !component) {
1284
+ return { success: false, error: "At least one of text, testID, or component must be provided." };
1285
+ }
1286
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1287
+ const textParam = text ? `'${esc(text)}'` : "null";
1288
+ const testIDParam = testID ? `'${esc(testID)}'` : "null";
1289
+ const componentParam = component ? `'${esc(component)}'` : "null";
1290
+ const expression = `
1291
+ (function() {
1292
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1293
+ if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
1294
+
1295
+ var roots = [];
1296
+ if (hook.getFiberRoots) {
1297
+ var renderers = hook.renderers;
1298
+ if (renderers) {
1299
+ for (var entry of renderers) {
1300
+ var r = Array.from(hook.getFiberRoots(entry[0]) || []);
1301
+ if (r.length > 0) { roots = r; break; }
1302
+ }
1303
+ }
1304
+ }
1305
+ if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
1306
+
1307
+ var searchText = ${textParam};
1308
+ var searchTestID = ${testIDParam};
1309
+ var searchComponent = ${componentParam};
1310
+ var targetIndex = ${index};
1311
+
1312
+ function getComponentName(fiber) {
1313
+ if (!fiber || !fiber.type) return null;
1314
+ if (typeof fiber.type === 'string') return fiber.type;
1315
+ return fiber.type.displayName || fiber.type.name || null;
1316
+ }
1317
+
1318
+ function extractText(fiber, depth) {
1319
+ if (!fiber || depth > 15) return '';
1320
+ var parts = [];
1321
+ var props = fiber.memoizedProps;
1322
+ if (props) {
1323
+ var ch = props.children;
1324
+ if (typeof ch === 'string') parts.push(ch);
1325
+ else if (typeof ch === 'number') parts.push(String(ch));
1326
+ else if (Array.isArray(ch)) {
1327
+ for (var i = 0; i < ch.length; i++) {
1328
+ if (typeof ch[i] === 'string') parts.push(ch[i]);
1329
+ else if (typeof ch[i] === 'number') parts.push(String(ch[i]));
1330
+ }
1331
+ }
1332
+ }
1333
+ var child = fiber.child;
1334
+ while (child) {
1335
+ parts.push(extractText(child, depth + 1));
1336
+ child = child.sibling;
1337
+ }
1338
+ return parts.join('');
1339
+ }
1340
+
1341
+ var matches = [];
1342
+
1343
+ function walkFiber(fiber, path) {
1344
+ if (!fiber) return;
1345
+ var name = getComponentName(fiber);
1346
+ var props = fiber.memoizedProps;
1347
+
1348
+ // Skip off-screen subtrees — React Navigation marks hidden screens with aria-hidden
1349
+ if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
1350
+
1351
+ var isPressable = props && typeof props.onPress === 'function';
1352
+ var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1353
+
1354
+ if (isPressable || isInput) {
1355
+ var text = '';
1356
+ if (isPressable) {
1357
+ text = extractText(fiber, 0);
1358
+ } else {
1359
+ // For inputs: use child text, value, defaultValue, or placeholder
1360
+ var val = typeof props.value === 'string' ? props.value : '';
1361
+ var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
1362
+ var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
1363
+ text = extractText(fiber, 0) || val || defVal || ph;
1364
+ }
1365
+ var tid = props.testID || props.nativeID || null;
1366
+ var matched = true;
1367
+
1368
+ if (searchText !== null) {
1369
+ matched = matched && text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
1370
+ }
1371
+ if (searchTestID !== null) {
1372
+ matched = matched && (tid === searchTestID);
1373
+ }
1374
+ if (searchComponent !== null) {
1375
+ matched = matched && (name || '').toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1;
1376
+ }
1377
+
1378
+ if (matched) {
1379
+ matches.push({
1380
+ fiber: fiber,
1381
+ name: name || '(anonymous)',
1382
+ text: text.substring(0, 100),
1383
+ testID: tid,
1384
+ path: path.join(' > '),
1385
+ isInput: isInput
1386
+ });
1387
+ }
1388
+ }
1389
+
1390
+ var child = fiber.child;
1391
+ while (child) {
1392
+ var childName = getComponentName(child);
1393
+ walkFiber(child, childName ? path.concat([childName]) : path);
1394
+ child = child.sibling;
1395
+ }
1396
+ }
1397
+
1398
+ for (var ri = 0; ri < roots.length; ri++) {
1399
+ walkFiber(roots[ri].current, []);
1400
+ }
1401
+
1402
+ // Phase 2: If searching by component and no match found,
1403
+ // look for the component by name and walk UP to find nearest pressable ancestor
1404
+ if (matches.length === 0 && searchComponent !== null) {
1405
+ var componentCandidates = [];
1406
+
1407
+ function findByName(fiber, path) {
1408
+ if (!fiber) return;
1409
+ var name = getComponentName(fiber);
1410
+ var props = fiber.memoizedProps;
1411
+ if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
1412
+
1413
+ if (name && name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1) {
1414
+ componentCandidates.push({ fiber: fiber, name: name, path: path.join(' > ') });
1415
+ }
1416
+ var child = fiber.child;
1417
+ while (child) {
1418
+ var childName = getComponentName(child);
1419
+ findByName(child, childName ? path.concat([childName]) : path);
1420
+ child = child.sibling;
1421
+ }
1422
+ }
1423
+
1424
+ for (var ri2 = 0; ri2 < roots.length; ri2++) {
1425
+ findByName(roots[ri2].current, []);
1426
+ }
1427
+
1428
+ // For each candidate, walk up to find pressable ancestor
1429
+ var maxDepth = ${maxTraversalDepth};
1430
+ for (var ci = 0; ci < componentCandidates.length; ci++) {
1431
+ var candidate = componentCandidates[ci];
1432
+ var parent = candidate.fiber.return;
1433
+ var depth = 0;
1434
+ while (parent && depth < maxDepth) {
1435
+ var parentProps = parent.memoizedProps;
1436
+ if (parentProps && typeof parentProps.onPress === 'function') {
1437
+ var text = extractText(parent, 0);
1438
+ matches.push({
1439
+ fiber: parent,
1440
+ name: candidate.name,
1441
+ text: text.substring(0, 100),
1442
+ testID: parentProps.testID || parentProps.nativeID || null,
1443
+ path: candidate.path
1444
+ });
1445
+ break;
1446
+ }
1447
+ parent = parent.return;
1448
+ depth++;
1449
+ }
1450
+ }
1451
+ }
1452
+
1453
+ if (matches.length === 0) {
1454
+ var criteria = [];
1455
+ if (searchText !== null) criteria.push('text="' + searchText + '"');
1456
+ if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
1457
+ if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
1458
+ return { error: 'No pressable or focusable elements found matching: ' + criteria.join(', ') };
1459
+ }
1460
+
1461
+ if (targetIndex >= matches.length) {
1462
+ return {
1463
+ error: 'Found ' + matches.length + ' match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
1464
+ matches: matches.map(function(m, i) {
1465
+ return { index: i, component: m.name, text: m.text, testID: m.testID };
1466
+ })
1467
+ };
1468
+ }
1469
+
1470
+ var target = matches[targetIndex];
1471
+ try {
1472
+ if (target.isInput) {
1473
+ // Input elements can't be pressed via fiber — return match info
1474
+ // so the orchestrator can fall through to accessibility/coordinate tap
1475
+ var result = {
1476
+ success: true,
1477
+ pressed: target.name,
1478
+ matchIndex: targetIndex,
1479
+ totalMatches: matches.length,
1480
+ text: target.text,
1481
+ testID: target.testID,
1482
+ path: target.path,
1483
+ isInput: true,
1484
+ needsNativeTap: true
1485
+ };
1486
+ if (matches.length > 1) {
1487
+ result.allMatches = matches.map(function(m, i) {
1488
+ return { index: i, component: m.name, text: m.text, testID: m.testID };
1489
+ });
1490
+ }
1491
+ return result;
1492
+ }
1493
+ target.fiber.memoizedProps.onPress();
1494
+ var result = {
1495
+ success: true,
1496
+ pressed: target.name,
1497
+ matchIndex: targetIndex,
1498
+ totalMatches: matches.length,
1499
+ text: target.text,
1500
+ testID: target.testID,
1501
+ path: target.path
1502
+ };
1503
+ if (matches.length > 1) {
1504
+ result.allMatches = matches.map(function(m, i) {
1505
+ return { index: i, component: m.name, text: m.text, testID: m.testID };
1506
+ });
1507
+ }
1508
+ return result;
1509
+ } catch (e) {
1510
+ return { error: 'Handler threw: ' + (e.message || String(e)), component: target.name };
1511
+ }
1512
+ })()
1513
+ `;
1514
+ return executeInApp(expression, false);
1515
+ }
1516
+ // ============================================================================
1517
+ // Coordinate-Based Element Inspection (via DevTools Inspector API)
1518
+ // ============================================================================
1519
+ /**
1520
+ * Toggle the Element Inspector via DevSettings native module.
1521
+ * This enables the inspector overlay programmatically.
1522
+ */
1523
+ export async function toggleElementInspector() {
1524
+ const expression = `
1525
+ (function() {
1526
+ const ds = globalThis.nativeModuleProxy?.DevSettings;
1527
+ if (!ds) return { error: 'DevSettings not available' };
1528
+
1529
+ const proto = Object.getPrototypeOf(ds);
1530
+ if (!proto || typeof proto.toggleElementInspector !== 'function') {
1531
+ return { error: 'toggleElementInspector not found' };
1532
+ }
1533
+
1534
+ try {
1535
+ proto.toggleElementInspector.call(ds);
1536
+ return { success: true, message: 'Element Inspector toggled' };
1537
+ } catch (e) {
1538
+ return { error: 'Failed to toggle: ' + e.message };
1539
+ }
1540
+ })()
1541
+ `;
1542
+ return executeInApp(expression, false);
1543
+ }
1544
+ /**
1545
+ * Check if the Element Inspector overlay is currently active.
1546
+ */
1547
+ export async function isInspectorActive() {
1548
+ const expression = `
1549
+ (function() {
1550
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1551
+ if (!hook) return false;
1552
+
1553
+ let roots = [...(hook.getFiberRoots?.(1) || [])];
1554
+ if (roots.length === 0) {
1555
+ for (const [id] of (hook.renderers || [])) {
1556
+ roots = [...(hook.getFiberRoots?.(id) || [])];
1557
+ if (roots.length > 0) break;
1558
+ }
1559
+ }
1560
+ if (roots.length === 0) return false;
1561
+
1562
+ function findComponent(fiber, targetName, depth = 0) {
1563
+ if (!fiber || depth > 100) return null;
1564
+ const name = fiber.type?.displayName || fiber.type?.name;
1565
+ if (name === targetName) return fiber;
1566
+ let child = fiber.child;
1567
+ while (child) {
1568
+ const found = findComponent(child, targetName, depth + 1);
1569
+ if (found) return found;
1570
+ child = child.sibling;
1571
+ }
1572
+ return null;
1573
+ }
1574
+
1575
+ return !!findComponent(roots[0].current, 'InspectorPanel');
1576
+ })()
1577
+ `;
1578
+ const result = await executeInApp(expression, false);
1579
+ if (result.success && result.result) {
1580
+ return result.result === "true";
1581
+ }
1582
+ return false;
1583
+ }
1584
+ /**
1585
+ * Get the currently selected element from the Element Inspector overlay.
1586
+ * This reads the InspectorPanel component's props to get the hierarchy, frame, and style.
1587
+ * Requires the Element Inspector to be enabled and an element to be selected.
1588
+ */
1589
+ export async function getInspectorSelection() {
1590
+ const expression = `
1591
+ (function() {
1592
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1593
+ if (!hook) return { error: 'React DevTools hook not available.' };
1594
+
1595
+ // Find fiber roots
1596
+ let roots = [...(hook.getFiberRoots?.(1) || [])];
1597
+ if (roots.length === 0) {
1598
+ for (const [id] of (hook.renderers || [])) {
1599
+ roots = [...(hook.getFiberRoots?.(id) || [])];
1600
+ if (roots.length > 0) break;
1601
+ }
1602
+ }
1603
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
1604
+
1605
+ // Find InspectorPanel component
1606
+ function findComponent(fiber, targetName, depth = 0) {
1607
+ if (!fiber || depth > 100) return null;
1608
+ const name = fiber.type?.displayName || fiber.type?.name;
1609
+ if (name === targetName) return fiber;
1610
+ let child = fiber.child;
1611
+ while (child) {
1612
+ const found = findComponent(child, targetName, depth + 1);
1613
+ if (found) return found;
1614
+ child = child.sibling;
1615
+ }
1616
+ return null;
1617
+ }
1618
+
1619
+ const panelFiber = findComponent(roots[0].current, 'InspectorPanel');
1620
+ if (!panelFiber) {
1621
+ return {
1622
+ error: 'Element Inspector is not active.',
1623
+ hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
1624
+ };
1625
+ }
1626
+
1627
+ const props = panelFiber.memoizedProps;
1628
+ if (!props.hierarchy || props.hierarchy.length === 0) {
1629
+ return {
1630
+ error: 'No element selected.',
1631
+ hint: 'Tap on an element in the app to select it for inspection.'
1632
+ };
1633
+ }
1634
+
1635
+ // Build the path from hierarchy
1636
+ const path = props.hierarchy.map(h => h.name).join(' > ');
1637
+ const element = props.hierarchy[props.hierarchy.length - 1]?.name || 'Unknown';
1638
+
1639
+ // Extract style info
1640
+ let style = {};
1641
+ if (props.inspected?.style) {
1642
+ const styles = Array.isArray(props.inspected.style)
1643
+ ? props.inspected.style
1644
+ : [props.inspected.style];
1645
+ for (const s of styles) {
1646
+ if (s && typeof s === 'object') {
1647
+ Object.assign(style, s);
1648
+ }
1649
+ }
1650
+ }
1651
+
1652
+ return {
1653
+ element,
1654
+ path,
1655
+ frame: props.inspected?.frame || null,
1656
+ style: Object.keys(style).length > 0 ? style : null,
1657
+ selection: props.selection,
1658
+ hierarchyLength: props.hierarchy.length
1659
+ };
1660
+ })()
1661
+ `;
1662
+ return executeInApp(expression, false);
1663
+ }
1664
+ /**
1665
+ * Inspect the React component at a specific (x, y) coordinate.
1666
+ *
1667
+ * Works on both Paper and Fabric (New Architecture). Uses a two-step approach
1668
+ * because measureInWindow callbacks fire in a future native event loop tick
1669
+ * (not microtasks), so awaitPromise cannot be used to collect them:
1670
+ *
1671
+ * Step 1 — dispatch: walk the fiber tree, call measureInWindow on each host
1672
+ * component, store fiber refs and results in app globals.
1673
+ * Step 2 — resolve (after 300ms): read the globals, hit-test against target
1674
+ * coordinates, return the innermost matching React component.
1675
+ */
1676
+ export async function inspectAtPoint(x, y, options = {}) {
1677
+ const { includeProps = true, includeFrame = true } = options;
1678
+ // --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
1679
+ const dispatchExpression = `
1680
+ (function() {
1681
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1682
+ if (!hook) return { error: 'React DevTools hook not available. Make sure you are running a development build.' };
1683
+
1684
+ var roots = [];
1685
+ if (hook.getFiberRoots) {
1686
+ try { roots = Array.from(hook.getFiberRoots(1) || []); } catch(e) {}
1687
+ }
1688
+ if (roots.length === 0 && hook.renderers) {
1689
+ for (var entry of hook.renderers) {
1690
+ try {
1691
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
1692
+ if (r.length > 0) { roots = r; break; }
1693
+ } catch(e) {}
1694
+ }
1695
+ }
1696
+ if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
1697
+
1698
+ // Paper: measureInWindow is on stateNode directly.
1699
+ // Fabric: measureInWindow is on stateNode.canonical.publicInstance.
1700
+ function getMeasurable(fiber) {
1701
+ var sn = fiber.stateNode;
1702
+ if (!sn) return null;
1703
+ if (typeof sn.measureInWindow === 'function') return sn;
1704
+ if (sn.canonical && sn.canonical.publicInstance &&
1705
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1706
+ return sn.canonical.publicInstance;
1707
+ }
1708
+ return null;
1709
+ }
1710
+
1711
+ var hostFibers = [];
1712
+ function walkFibers(fiber, depth) {
1713
+ var cur = fiber;
1714
+ while (cur) {
1715
+ if (hostFibers.length >= 500) return;
1716
+ if (typeof cur.type === 'string' && getMeasurable(cur)) hostFibers.push(cur);
1717
+ if (cur.child && depth < 250) walkFibers(cur.child, depth + 1);
1718
+ cur = cur.sibling;
1719
+ }
1720
+ }
1721
+ for (var root of roots) { walkFibers(root.current, 0); }
1722
+
1723
+ if (hostFibers.length === 0) return { error: 'No measurable host components found. App may not be fully rendered.' };
1724
+
1725
+ globalThis.__inspectFibers = hostFibers;
1726
+ globalThis.__inspectMeasurements = new Array(hostFibers.length).fill(null);
1727
+
1728
+ hostFibers.forEach(function(fiber, i) {
1729
+ try {
1730
+ getMeasurable(fiber).measureInWindow(function(fx, fy, fw, fh) {
1731
+ globalThis.__inspectMeasurements[i] = { x: fx, y: fy, width: fw, height: fh };
1732
+ });
1733
+ } catch(e) {}
1734
+ });
1735
+
1736
+ return { count: hostFibers.length };
1737
+ })()
1738
+ `;
1739
+ const dispatchResult = await executeInApp(dispatchExpression, false);
1740
+ if (!dispatchResult.success)
1741
+ return dispatchResult;
1742
+ try {
1743
+ const parsed = JSON.parse(dispatchResult.result || "{}");
1744
+ if (parsed.error)
1745
+ return { success: false, error: parsed.error };
1746
+ }
1747
+ catch {
1748
+ /* ignore parse errors */
1749
+ }
1750
+ // Wait for native measureInWindow callbacks to fire
1751
+ await delay(300);
1752
+ // --- Step 2: read measurements, hit-test, return result ---
1753
+ const resolveExpression = `
1754
+ (function() {
1755
+ var fibers = globalThis.__inspectFibers;
1756
+ var measurements = globalThis.__inspectMeasurements;
1757
+ globalThis.__inspectFibers = null;
1758
+ globalThis.__inspectMeasurements = null;
1759
+
1760
+ if (!fibers || !measurements) return { error: 'No measurement data available. Run inspect_at_point again.' };
1761
+
1762
+ var targetX = ${x};
1763
+ var targetY = ${y};
1764
+
1765
+ var hits = [];
1766
+ for (var i = 0; i < measurements.length; i++) {
1767
+ var m = measurements[i];
1768
+ if (m && m.width > 0 && m.height > 0 &&
1769
+ targetX >= m.x && targetX <= m.x + m.width &&
1770
+ targetY >= m.y && targetY <= m.y + m.height) {
1771
+ hits.push({ fiber: fibers[i], x: m.x, y: m.y, width: m.width, height: m.height });
1772
+ }
1773
+ }
1774
+
1775
+ if (hits.length === 0) {
1776
+ 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.' };
1777
+ }
1778
+
1779
+ // Smallest area = innermost (most specific) component
1780
+ hits.sort(function(a, b) { return (a.width * a.height) - (b.width * b.height); });
1781
+ var best = hits[0];
1782
+
1783
+ // RN primitives and internal components to skip when surfacing the "element" name.
1784
+ // We want the nearest *custom* component, not a library wrapper.
1785
+ 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|Expo.*|LinearGradient|ViewManagerAdapter_.*|Svg.*|Defs|Path|Rect|Circle|G|Line|Polygon|Polyline|Ellipse|ClipPath|GestureHandler.*|NativeViewGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|MaybeScreen|SafeAreaProvider.*|GestureDetector|PanGestureHandler|DropShadow|BlurView|MaskedView.*)$/;
1786
+
1787
+ function getNearestNamed(fiber, skipPrimitives) {
1788
+ var cur = fiber;
1789
+ var fallback = null;
1790
+ while (cur) {
1791
+ if (cur.type && typeof cur.type !== 'string') {
1792
+ var name = cur.type.displayName || cur.type.name;
1793
+ if (name) {
1794
+ if (!fallback) fallback = { name: name, fiber: cur };
1795
+ if (!skipPrimitives || !RN_PRIMITIVES.test(name)) {
1796
+ return { name: name, fiber: cur };
1797
+ }
1798
+ }
1799
+ }
1800
+ cur = cur.return;
1801
+ }
1802
+ return fallback;
1803
+ }
1804
+
1805
+ function buildPath(fiber) {
1806
+ var path = [];
1807
+ var cur = fiber;
1808
+ while (cur) {
1809
+ if (cur.type) {
1810
+ var n = typeof cur.type === 'string'
1811
+ ? cur.type
1812
+ : (cur.type.displayName || cur.type.name);
1813
+ if (n) path.unshift(n);
1814
+ }
1815
+ cur = cur.return;
1816
+ }
1817
+ return path.slice(-8).join(' > ');
1818
+ }
1819
+
1820
+ // Find nearest custom component (skipping RN primitives) for the element name,
1821
+ // but fall back to the nearest named component if nothing custom is found.
1822
+ var named = getNearestNamed(best.fiber.return || best.fiber, true);
1823
+ var result = {
1824
+ point: { x: targetX, y: targetY },
1825
+ element: named ? named.name : best.fiber.type,
1826
+ nativeElement: best.fiber.type,
1827
+ path: buildPath(best.fiber)
1828
+ };
1829
+
1830
+ if (${includeFrame}) {
1831
+ result.frame = { x: best.x, y: best.y, width: best.width, height: best.height };
1832
+ }
1833
+
1834
+ if (${includeProps} && named && named.fiber.memoizedProps) {
1835
+ var props = {};
1836
+ var keys = Object.keys(named.fiber.memoizedProps);
1837
+ for (var i = 0; i < keys.length; i++) {
1838
+ var key = keys[i];
1839
+ if (key === 'children') continue;
1840
+ var val = named.fiber.memoizedProps[key];
1841
+ if (typeof val === 'function') {
1842
+ props[key] = '[Function]';
1843
+ } else if (typeof val === 'object' && val !== null) {
1844
+ try {
1845
+ var str = JSON.stringify(val);
1846
+ props[key] = str.length > 200
1847
+ ? (Array.isArray(val) ? '[Array(' + val.length + ')]' : '[Object]')
1848
+ : val;
1849
+ } catch(e) {
1850
+ props[key] = '[Object]';
1851
+ }
1852
+ } else {
1853
+ props[key] = val;
1854
+ }
1855
+ }
1856
+ if (Object.keys(props).length > 0) result.props = props;
1857
+ }
1858
+
1859
+ // Hierarchy: custom-named component for each hit, deduped, innermost→outermost
1860
+ var hierarchy = [];
1861
+ for (var j = 0; j < Math.min(hits.length, 15); j++) {
1862
+ var n2 = getNearestNamed(hits[j].fiber.return, true) || getNearestNamed(hits[j].fiber, true);
1863
+ if (n2 && !hierarchy.some(function(h) { return h.name === n2.name; })) {
1864
+ hierarchy.push({
1865
+ name: n2.name,
1866
+ frame: { x: hits[j].x, y: hits[j].y, width: hits[j].width, height: hits[j].height }
1867
+ });
1868
+ }
1869
+ }
1870
+ if (hierarchy.length > 1) result.hierarchy = hierarchy;
1871
+
1872
+ return result;
1873
+ })()
1874
+ `;
1875
+ return executeInApp(resolveExpression, false);
1876
+ }
1877
+ //# sourceMappingURL=executor.js.map