react-native-ai-debugger 1.8.0 → 1.8.1

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 (107) hide show
  1. package/build/core/appDetection.d.ts +7 -0
  2. package/build/core/appDetection.d.ts.map +1 -1
  3. package/build/core/appDetection.js +38 -17
  4. package/build/core/appDetection.js.map +1 -1
  5. package/build/core/componentSearch.d.ts +38 -0
  6. package/build/core/componentSearch.d.ts.map +1 -0
  7. package/build/core/componentSearch.js +419 -0
  8. package/build/core/componentSearch.js.map +1 -0
  9. package/build/core/componentTree.d.ts +12 -0
  10. package/build/core/componentTree.d.ts.map +1 -0
  11. package/build/core/componentTree.js +275 -0
  12. package/build/core/componentTree.js.map +1 -0
  13. package/build/core/connection.d.ts +1 -0
  14. package/build/core/connection.d.ts.map +1 -1
  15. package/build/core/connection.js +97 -88
  16. package/build/core/connection.js.map +1 -1
  17. package/build/core/debugGlobals.d.ts +5 -0
  18. package/build/core/debugGlobals.d.ts.map +1 -0
  19. package/build/core/debugGlobals.js +300 -0
  20. package/build/core/debugGlobals.js.map +1 -0
  21. package/build/core/executor.d.ts +7 -206
  22. package/build/core/executor.d.ts.map +1 -1
  23. package/build/core/executor.js +10 -4333
  24. package/build/core/executor.js.map +1 -1
  25. package/build/core/index.d.ts +3 -2
  26. package/build/core/index.d.ts.map +1 -1
  27. package/build/core/index.js +3 -2
  28. package/build/core/index.js.map +1 -1
  29. package/build/core/inspector.d.ts +50 -0
  30. package/build/core/inspector.d.ts.map +1 -0
  31. package/build/core/inspector.js +604 -0
  32. package/build/core/inspector.js.map +1 -0
  33. package/build/core/jsExecute.d.ts +27 -0
  34. package/build/core/jsExecute.d.ts.map +1 -0
  35. package/build/core/jsExecute.js +453 -0
  36. package/build/core/jsExecute.js.map +1 -0
  37. package/build/core/pressables.d.ts +44 -0
  38. package/build/core/pressables.d.ts.map +1 -0
  39. package/build/core/pressables.js +1480 -0
  40. package/build/core/pressables.js.map +1 -0
  41. package/build/core/register.d.ts +8 -0
  42. package/build/core/register.d.ts.map +1 -0
  43. package/build/core/register.js +216 -0
  44. package/build/core/register.js.map +1 -0
  45. package/build/core/screenLayout.d.ts +52 -0
  46. package/build/core/screenLayout.d.ts.map +1 -0
  47. package/build/core/screenLayout.js +828 -0
  48. package/build/core/screenLayout.js.map +1 -0
  49. package/build/core/toolHelpers.d.ts +24 -0
  50. package/build/core/toolHelpers.d.ts.map +1 -0
  51. package/build/core/toolHelpers.js +118 -0
  52. package/build/core/toolHelpers.js.map +1 -0
  53. package/build/core/types.d.ts +11 -6
  54. package/build/core/types.d.ts.map +1 -1
  55. package/build/index.d.ts +2 -4
  56. package/build/index.d.ts.map +1 -1
  57. package/build/index.js +37 -4440
  58. package/build/index.js.map +1 -1
  59. package/build/tools/accountTools.d.ts +2 -0
  60. package/build/tools/accountTools.d.ts.map +1 -1
  61. package/build/tools/accountTools.js +5 -0
  62. package/build/tools/accountTools.js.map +1 -1
  63. package/build/tools/bundleTools.d.ts +3 -0
  64. package/build/tools/bundleTools.d.ts.map +1 -0
  65. package/build/tools/bundleTools.js +368 -0
  66. package/build/tools/bundleTools.js.map +1 -0
  67. package/build/tools/componentTools.d.ts +3 -0
  68. package/build/tools/componentTools.d.ts.map +1 -0
  69. package/build/tools/componentTools.js +639 -0
  70. package/build/tools/componentTools.js.map +1 -0
  71. package/build/tools/connectionTools.d.ts +3 -0
  72. package/build/tools/connectionTools.d.ts.map +1 -0
  73. package/build/tools/connectionTools.js +564 -0
  74. package/build/tools/connectionTools.js.map +1 -0
  75. package/build/tools/deviceTools.d.ts +3 -0
  76. package/build/tools/deviceTools.d.ts.map +1 -0
  77. package/build/tools/deviceTools.js +296 -0
  78. package/build/tools/deviceTools.js.map +1 -0
  79. package/build/tools/executionTools.d.ts +3 -0
  80. package/build/tools/executionTools.d.ts.map +1 -0
  81. package/build/tools/executionTools.js +173 -0
  82. package/build/tools/executionTools.js.map +1 -0
  83. package/build/tools/interactionTools.d.ts +3 -0
  84. package/build/tools/interactionTools.d.ts.map +1 -0
  85. package/build/tools/interactionTools.js +414 -0
  86. package/build/tools/interactionTools.js.map +1 -0
  87. package/build/tools/logTools.d.ts +3 -0
  88. package/build/tools/logTools.d.ts.map +1 -0
  89. package/build/tools/logTools.js +371 -0
  90. package/build/tools/logTools.js.map +1 -0
  91. package/build/tools/metaTools.d.ts +7 -0
  92. package/build/tools/metaTools.d.ts.map +1 -0
  93. package/build/tools/metaTools.js +205 -0
  94. package/build/tools/metaTools.js.map +1 -0
  95. package/build/tools/networkTools.d.ts +3 -0
  96. package/build/tools/networkTools.d.ts.map +1 -0
  97. package/build/tools/networkTools.js +478 -0
  98. package/build/tools/networkTools.js.map +1 -0
  99. package/build/tools/reduxTools.d.ts +3 -0
  100. package/build/tools/reduxTools.d.ts.map +1 -0
  101. package/build/tools/reduxTools.js +83 -0
  102. package/build/tools/reduxTools.js.map +1 -0
  103. package/build/tools/screenshotTools.d.ts +3 -0
  104. package/build/tools/screenshotTools.d.ts.map +1 -0
  105. package/build/tools/screenshotTools.js +606 -0
  106. package/build/tools/screenshotTools.js.map +1 -0
  107. package/package.json +1 -1
@@ -0,0 +1,639 @@
1
+ import { z } from "zod";
2
+ import { registerToolWithTelemetry } from "../core/register.js";
3
+ import { getComponentTree, getScreenLayout, getPressableElements, inspectComponent, findComponents, toggleElementInspector, getInspectorSelection, getInspectorSelectionAtPoint, inspectAtPoint, iosScreenshot, androidScreenshot, androidGetDensity, getDevicePixelRatio, getIOSSafeAreaTop, inferIOSDevicePixelRatio, metroMissingHintIfAbsent, hasMetro, getConnectedAppByDevice, getFirstConnectedApp, } from "../core/index.js";
4
+ import { primaryInteractionBanner } from "../core/toolHelpers.js";
5
+ export function registerComponentTools(server) {
6
+ // Tool: Get full screen layout (all components with layout styles)
7
+ registerToolWithTelemetry(server, "get_screen_layout", {
8
+ description: "Get a screen map showing visible components as an indented tree with actual screen positions. Uses measureInWindow for real coordinates and filters out off-screen components. Returns meaningful component names with text content and frame data (x,y width x height). Coordinates are in **points** (iOS) or **dp** (Android) — NOT screenshot pixels. Use tap(text=...) or tap(testID=...) to interact with discovered components. Use extended=true to include layout styles (padding, margin, flex, backgroundColor, etc.)." +
9
+ primaryInteractionBanner() + "\n" +
10
+ "PURPOSE: Quickest textual map of what is actually on screen right now — component names, positions, and text — so you can plan taps and inspections without guessing.\n" +
11
+ "WHEN TO USE: First step whenever the user asks \"what's on screen\", \"why is X covering Y\", or before tapping a visually ambiguous element.\n" +
12
+ "WORKFLOW: get_screen_layout -> find_components(pattern=\"...\") or inspect_component(componentName=\"...\") -> tap(testID=...) -> get_screen_layout again to confirm.\n" +
13
+ "LIMITATIONS: Coordinates are points/dp, not screenshot pixels — pass them to tap() which handles conversion, do not multiply by devicePixelRatio yourself.\n" +
14
+ "GOOD: get_screen_layout({ extended: true })\n" +
15
+ "BAD: get_screen_layout({ summary: true }) when you actually need to pick a specific element — summary hides the tree.\n" +
16
+ "SEE ALSO: call get_usage_guide(topic=\"layout\") for the full layout-check playbook.",
17
+ inputSchema: {
18
+ extended: z
19
+ .boolean()
20
+ .optional()
21
+ .default(false)
22
+ .describe("Include layout styles (padding, margin, flex, backgroundColor, borderRadius, etc.) for each component. Default: false for compact output."),
23
+ summary: z
24
+ .boolean()
25
+ .optional()
26
+ .default(false)
27
+ .describe("Return only component counts by name instead of full tree (default: false)"),
28
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
29
+ }
30
+ }, async ({ extended, summary, device }) => {
31
+ if (!hasMetro()) {
32
+ const hint = await metroMissingHintIfAbsent("get_screen_layout");
33
+ return {
34
+ content: [{ type: "text", text: `Screen Layout unavailable.${hint}` }],
35
+ isError: true
36
+ };
37
+ }
38
+ const result = await getScreenLayout({ extended, summary, device });
39
+ if (!result.success) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: `Error: ${result.error}`
45
+ }
46
+ ],
47
+ isError: true
48
+ };
49
+ }
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: `Screen Layout:\n\n${result.result}`
55
+ }
56
+ ]
57
+ };
58
+ });
59
+ // ============================================================================
60
+ // React Component Inspection Tools
61
+ // ============================================================================
62
+ // Tool: Get the React component tree
63
+ registerToolWithTelemetry(server, "get_component_tree", {
64
+ description: "Get the full React component tree from the running app. Shows the complete fiber hierarchy including providers, navigation wrappers, and internal components. For a screen overview with positions and text, use get_screen_layout instead. Use structureOnly=true for compact names-only output.\n" +
65
+ "PURPOSE: Expose the entire fiber tree — including providers, navigators, and off-screen subtrees — when get_screen_layout's visible-only view isn't enough.\n" +
66
+ "WHEN TO USE: Debugging context propagation, navigation wrappers, hidden modals, or when you need to understand the full React architecture.\n" +
67
+ "WORKFLOW: get_component_tree(structureOnly=true) for overview -> find_components for targeted lookup -> inspect_component for props/state.\n" +
68
+ "LIMITATIONS: Full trees can be large; always start with structureOnly=true. Ignores non-React native views. Minified builds return display names that may be opaque.\n" +
69
+ "GOOD: get_component_tree({ structureOnly: true })\n" +
70
+ "BAD: get_component_tree({ includeProps: true, includeStyles: true }) on a large app — likely hits response-size limits. Prefer inspect_component for specific nodes.\n" +
71
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
72
+ inputSchema: {
73
+ structureOnly: z
74
+ .boolean()
75
+ .optional()
76
+ .default(false)
77
+ .describe("Return ultra-compact structure with just component names (no props, styles, or paths). Use this first for overview, then drill down with inspect_component."),
78
+ maxDepth: z
79
+ .number()
80
+ .optional()
81
+ .describe("Maximum tree depth (default: 5000)"),
82
+ includeProps: z
83
+ .boolean()
84
+ .optional()
85
+ .default(false)
86
+ .describe("Include component props (excluding children and style). Ignored if structureOnly=true."),
87
+ includeStyles: z
88
+ .boolean()
89
+ .optional()
90
+ .default(false)
91
+ .describe("Include layout styles (padding, margin, flex, etc.). Ignored if structureOnly=true."),
92
+ hideInternals: z
93
+ .boolean()
94
+ .optional()
95
+ .default(true)
96
+ .describe("Hide internal RN components (RCTView, RNS*, Animated, etc.) for cleaner output (default: true)"),
97
+ format: z
98
+ .enum(["json", "tonl"])
99
+ .optional()
100
+ .default("tonl")
101
+ .describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true."),
102
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
103
+ }
104
+ }, async ({ structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format, device }) => {
105
+ const result = await getComponentTree({
106
+ structureOnly,
107
+ maxDepth,
108
+ includeProps,
109
+ includeStyles,
110
+ hideInternals,
111
+ format,
112
+ device
113
+ });
114
+ if (!result.success) {
115
+ return {
116
+ content: [
117
+ {
118
+ type: "text",
119
+ text: `Error: ${result.error}`
120
+ }
121
+ ],
122
+ isError: true
123
+ };
124
+ }
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `React Component Tree:\n\n${result.result}`
130
+ }
131
+ ]
132
+ };
133
+ });
134
+ // Tool: Get all pressable elements on screen
135
+ registerToolWithTelemetry(server, "get_pressable_elements", {
136
+ description: "Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, ready-to-tap center coordinates in SCREENSHOT PIXELS (same space as ios_screenshot/android_screenshot — pass directly to tap(x, y)), text labels, testID, accessibilityLabel, and a spatial nearbyText hint for icon-only buttons. Each element includes hasLabel (true if it contains text) and isInput (true for TextInput fields).\n" +
137
+ "HELPER — call before `tap` when you need to enumerate candidate elements before committing to a target; not a replacement for tap itself.\n" +
138
+ "PURPOSE: Produce a ready-to-tap inventory of every interactive element on screen with screenshot-pixel coordinates that tap(x, y) accepts directly.\n" +
139
+ "WHEN TO USE: Before tapping icon-only buttons, when text-based tap keeps failing, or to enumerate what the user can actually interact with.\n" +
140
+ "WORKFLOW: ios_screenshot / android_screenshot -> get_pressable_elements -> tap(testID=\"...\") or tap(x, y) using the center coordinates.\n" +
141
+ "LIMITATIONS: Visible-only (off-screen pressables are excluded). Requires a live React connection. Coordinates are in screenshot pixels — the tap tool converts to points internally.\n" +
142
+ "GOOD: get_pressable_elements()\n" +
143
+ "BAD: Calling when tap(testID=\"...\") already works — testID matching is faster and more stable.\n" +
144
+ "SEE ALSO: call get_usage_guide(topic=\"interact\") for the full interaction playbook.",
145
+ inputSchema: {
146
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
147
+ }
148
+ }, async ({ device }) => {
149
+ const result = await getPressableElements({ device });
150
+ if (!result.success) {
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: `Error: ${result.error}`
156
+ }
157
+ ],
158
+ isError: true
159
+ };
160
+ }
161
+ const elements = result.parsedElements;
162
+ if (!elements || elements.length === 0) {
163
+ return {
164
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
165
+ };
166
+ }
167
+ // Resolve target app so we can pick the right simulator / android device
168
+ const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
169
+ if (!targetApp) {
170
+ // No connected app — return raw (points) output; better than nothing
171
+ return {
172
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
173
+ };
174
+ }
175
+ // Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
176
+ // Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
177
+ let lines = [];
178
+ try {
179
+ if (targetApp.platform === "ios") {
180
+ const udid = targetApp.simulatorUdid;
181
+ const shot = await iosScreenshot(undefined, udid);
182
+ const screenshotScale = shot.scaleFactor || 1;
183
+ const devicePixelRatio = (shot.originalWidth && shot.originalHeight
184
+ ? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
185
+ : null) ?? (await getDevicePixelRatio(udid)) ?? 3;
186
+ // Fallback to 59pt (iPhone typical) when the UI driver preflight can't
187
+ // resolve the true inset — matches ios_screenshot's default. Without this
188
+ // shift, react-native-screens modal-presented screens report y relative
189
+ // to content origin and taps land in the status bar instead of the button.
190
+ const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
191
+ // Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
192
+ // same scaleFactor when converting our pixel coords back to points.
193
+ if (shot.originalWidth && shot.originalHeight) {
194
+ targetApp.lastScreenshot = {
195
+ originalWidth: shot.originalWidth,
196
+ originalHeight: shot.originalHeight,
197
+ scaleFactor: screenshotScale
198
+ };
199
+ }
200
+ lines = formatPressablesInPixels(elements, {
201
+ platform: "ios",
202
+ devicePixelRatio,
203
+ screenshotScale,
204
+ safeAreaTop
205
+ });
206
+ }
207
+ else {
208
+ // Metro's `deviceName` is the device model (e.g. "sdk_gphone16k_arm64"),
209
+ // not the adb serial (e.g. "emulator-5554"), so passing it as -s makes
210
+ // adb miss the device and androidScreenshot/androidGetDensity silently
211
+ // return defaults (scale=1, density=160). That leaves coords in raw
212
+ // device pixels — off by ~1.2× from the screenshot/JPEG space the tool
213
+ // description promises. Pass undefined to let adb auto-pick (matches
214
+ // how android_screenshot works when called without deviceId). Multi-
215
+ // Android-device support tracks separately under the multi-device
216
+ // refactor; this path is a single-Android-device fix.
217
+ const shot = await androidScreenshot(undefined, undefined);
218
+ const screenshotScale = shot.scaleFactor || 1;
219
+ const density = await androidGetDensity(undefined).catch(() => ({ density: 160 }));
220
+ const devicePixelRatio = (density.density || 160) / 160;
221
+ if (shot.originalWidth && shot.originalHeight) {
222
+ targetApp.lastScreenshot = {
223
+ originalWidth: shot.originalWidth,
224
+ originalHeight: shot.originalHeight,
225
+ scaleFactor: screenshotScale
226
+ };
227
+ }
228
+ lines = formatPressablesInPixels(elements, {
229
+ platform: "android",
230
+ devicePixelRatio,
231
+ screenshotScale,
232
+ safeAreaTop: 0
233
+ });
234
+ }
235
+ }
236
+ catch {
237
+ // Fallback to points if screenshot/metadata unavailable
238
+ return {
239
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
240
+ };
241
+ }
242
+ const iconCount = elements.filter((e) => !e.hasLabel).length;
243
+ const labeledCount = elements.length - iconCount;
244
+ const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
245
+ const text = [summary, "", ...lines].join("\n");
246
+ return {
247
+ content: [{ type: "text", text }]
248
+ };
249
+ });
250
+ function formatPressablesInPixels(elements, opts) {
251
+ const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
252
+ const out = [];
253
+ for (let i = 0; i < elements.length; i++) {
254
+ const el = elements[i];
255
+ // react-native-screens modal presentations report y relative to content origin;
256
+ // shift into the window frame when the measurement falls inside the safe-area band.
257
+ let cy = el.center.y;
258
+ let fy = el.frame.y;
259
+ if (platform === "ios" && safeAreaTop > 0) {
260
+ if (cy < safeAreaTop)
261
+ cy += safeAreaTop;
262
+ if (fy < safeAreaTop)
263
+ fy += safeAreaTop;
264
+ }
265
+ // iOS: fiber returns points → convert points × DPR / screenshotScale = JPEG px.
266
+ // Android: getPressableElements reconciles fiber DP against uiautomator device-pixel
267
+ // bounds (executor.ts, 2026-05-17). After reconciliation, coords are already in
268
+ // device pixels — only the JPEG downscale needs to be applied. Multiplying by DPR
269
+ // here would re-inflate them by ~density/160 (~2.6× on a 420dpi device), reproducing
270
+ // the original out-of-bounds bug.
271
+ const toPx = (v) => platform === "android"
272
+ ? Math.round(v / screenshotScale)
273
+ : Math.round((v * devicePixelRatio) / screenshotScale);
274
+ const cx = toPx(el.center.x);
275
+ const cyPx = toPx(cy);
276
+ const fx = toPx(el.frame.x);
277
+ const fyPx = toPx(fy);
278
+ const fw = toPx(el.frame.width);
279
+ const fh = toPx(el.frame.height);
280
+ const num = i + 1;
281
+ const label = el.hasLabel
282
+ ? `"${el.text}"`
283
+ : el.intent
284
+ ? `(${el.intent} icon)`
285
+ : "(icon/image)";
286
+ const ids = [];
287
+ if (el.testID)
288
+ ids.push(`testID="${el.testID}"`);
289
+ if (el.accessibilityLabel)
290
+ ids.push(`a11y="${el.accessibilityLabel}"`);
291
+ const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
292
+ const inputStr = el.isInput ? " (input)" : "";
293
+ const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
294
+ const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
295
+ out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
296
+ if (el.path)
297
+ out.push(` path: ${el.path}`);
298
+ }
299
+ return out;
300
+ }
301
+ // Tool: Inspect a specific component by name
302
+ registerToolWithTelemetry(server, "inspect_component", {
303
+ description: "Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after get_screen_layout or find_components to identify which component to inspect. Returns props, style, state (hooks), and optionally children tree. Use childrenDepth to control how deep nested children go." +
304
+ primaryInteractionBanner() + "\n" +
305
+ "PURPOSE: Reveal a mounted component's live props, hook state, and (optionally) child subtree so you can reason about why it renders the way it does.\n" +
306
+ "WHEN TO USE: User asks \"why is this button disabled\", \"what props does X receive\", or you need to confirm state changed after a tap.\n" +
307
+ "WORKFLOW: get_screen_layout or find_components -> inspect_component(componentName=\"Foo\") -> tap or execute_in_app to change state -> inspect_component again.\n" +
308
+ "LIMITATIONS: Requires the component to be currently mounted in the fiber tree. Name matching is exact; use find_components for fuzzy/regex lookup.\n" +
309
+ "GOOD: inspect_component({ componentName: \"SneakerCard\", index: 0 })\n" +
310
+ "BAD: inspect_component({ componentName: \"Card\" }) when many Card instances exist — pass index or narrow via find_components.\n" +
311
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
312
+ inputSchema: {
313
+ componentName: z
314
+ .string()
315
+ .describe("Name of the component to inspect (e.g., 'Button', 'HomeScreen', 'FlatList')"),
316
+ index: z
317
+ .number()
318
+ .optional()
319
+ .default(0)
320
+ .describe("If multiple instances exist, which one to inspect (0-based index, default: 0)"),
321
+ includeState: z
322
+ .boolean()
323
+ .optional()
324
+ .default(true)
325
+ .describe("Include component state/hooks (default: true)"),
326
+ includeChildren: z.boolean().optional().default(false).describe("Include children component tree"),
327
+ childrenDepth: z
328
+ .number()
329
+ .optional()
330
+ .default(1)
331
+ .describe("How many levels deep to show children (default: 1 = direct children only, 2+ = nested tree)"),
332
+ shortPath: z.boolean().optional().default(true).describe("Show only last 3 path segments (default: true)"),
333
+ simplifyHooks: z
334
+ .boolean()
335
+ .optional()
336
+ .default(true)
337
+ .describe("Simplify hooks output by hiding effects and reducing depth (default: true)"),
338
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
339
+ }
340
+ }, async ({ componentName, index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks, device }) => {
341
+ const result = await inspectComponent(componentName, {
342
+ index,
343
+ includeState,
344
+ includeChildren,
345
+ childrenDepth,
346
+ shortPath,
347
+ simplifyHooks,
348
+ device
349
+ });
350
+ if (!result.success) {
351
+ return {
352
+ content: [
353
+ {
354
+ type: "text",
355
+ text: `Error: ${result.error}`
356
+ }
357
+ ],
358
+ isError: true
359
+ };
360
+ }
361
+ return {
362
+ content: [
363
+ {
364
+ type: "text",
365
+ text: `Component Inspection: ${componentName}\n\n${result.result}`
366
+ }
367
+ ]
368
+ };
369
+ });
370
+ // Tool: Find components matching a pattern
371
+ registerToolWithTelemetry(server, "find_components", {
372
+ description: "Find components matching a name pattern. **TARGETED SEARCH**: Use after get_screen_layout or get_component_tree(structureOnly=true) to find specific components by pattern. Use includeLayout=true to get padding/margin/flex styles." +
373
+ primaryInteractionBanner() + "\n" +
374
+ "PURPOSE: Fast regex search over the entire fiber tree — including off-screen and wrapper components — to locate every instance of a component by name.\n" +
375
+ "WHEN TO USE: You know roughly what the component is called (e.g., \"Button\", \"Screen$\") but not where it lives, or you need counts/paths before drilling in with inspect_component.\n" +
376
+ "WORKFLOW: get_screen_layout (orient) -> find_components(pattern=\"...\") -> inspect_component(componentName=\"...\", index=N).\n" +
377
+ "LIMITATIONS: Matches the React display name only; minified builds may return opaque names. Large result sets — use maxResults or a tighter pattern.\n" +
378
+ "GOOD: find_components({ pattern: \"Button\" }); find_components({ pattern: \"Screen$\" })\n" +
379
+ "BAD: find_components({ pattern: \".*\" }) — floods the response; narrow the regex.\n" +
380
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
381
+ inputSchema: {
382
+ pattern: z
383
+ .string()
384
+ .describe("Regex pattern to match component names (case-insensitive). Examples: 'Button', 'Screen$', 'List.*Item'"),
385
+ maxResults: z.number().optional().default(20).describe("Maximum number of results to return (default: 20)"),
386
+ includeLayout: z
387
+ .boolean()
388
+ .optional()
389
+ .default(false)
390
+ .describe("Include layout styles (padding, margin, flex) for each matched component"),
391
+ shortPath: z.boolean().optional().default(true).describe("Show only last 3 path segments (default: true)"),
392
+ summary: z
393
+ .boolean()
394
+ .optional()
395
+ .default(false)
396
+ .describe("Return only component counts by name instead of full list (default: false)"),
397
+ format: z
398
+ .enum(["json", "tonl"])
399
+ .optional()
400
+ .default("tonl")
401
+ .describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)"),
402
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
403
+ }
404
+ }, async ({ pattern, maxResults, includeLayout, shortPath, summary, format, device }) => {
405
+ const result = await findComponents(pattern, { maxResults, includeLayout, shortPath, summary, format, device });
406
+ if (!result.success) {
407
+ return {
408
+ content: [
409
+ {
410
+ type: "text",
411
+ text: `Error: ${result.error}`
412
+ }
413
+ ],
414
+ isError: true
415
+ };
416
+ }
417
+ return {
418
+ content: [
419
+ {
420
+ type: "text",
421
+ text: `Find Components (pattern: "${pattern}"):\n\n${result.result}`
422
+ }
423
+ ]
424
+ };
425
+ });
426
+ // Tool: Toggle Element Inspector programmatically
427
+ registerToolWithTelemetry(server, "toggle_element_inspector", {
428
+ description: "Toggle React Native's Element Inspector overlay on/off. Rarely needed directly — get_inspector_selection auto-toggles the overlay on for capture and back off afterward. Use only for edge cases (e.g., leaving the overlay visible on screen for a user-facing screenshot).\n" +
429
+ "PURPOSE: Manual control over the on-device inspector overlay.\n" +
430
+ "WHEN TO USE: Only for special cases like capturing a screenshot WITH the inspector visible. Normal inspection workflows should call get_inspector_selection directly.\n" +
431
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
432
+ inputSchema: {
433
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
434
+ }
435
+ }, async ({ device }) => {
436
+ const result = await toggleElementInspector(device);
437
+ if (!result.success) {
438
+ return {
439
+ content: [
440
+ {
441
+ type: "text",
442
+ text: `Error: ${result.error}`
443
+ }
444
+ ],
445
+ isError: true
446
+ };
447
+ }
448
+ try {
449
+ const parsed = JSON.parse(result.result || "{}");
450
+ if (parsed.error) {
451
+ return {
452
+ content: [
453
+ {
454
+ type: "text",
455
+ text: `Failed to toggle Element Inspector: ${parsed.error}`
456
+ }
457
+ ],
458
+ isError: true
459
+ };
460
+ }
461
+ return {
462
+ content: [
463
+ {
464
+ type: "text",
465
+ text: parsed.message || "Element Inspector toggled successfully"
466
+ }
467
+ ]
468
+ };
469
+ }
470
+ catch {
471
+ return {
472
+ content: [
473
+ {
474
+ type: "text",
475
+ text: result.result || "Element Inspector toggled"
476
+ }
477
+ ]
478
+ };
479
+ }
480
+ });
481
+ // Tool: Get currently selected element from Element Inspector
482
+ registerToolWithTelemetry(server, "get_inspector_selection", {
483
+ description: "Identify the React component at a screen location AND read its full styling. Returns RN's curated owner-tree hierarchy with PER-COMPONENT STYLE (padding, margin, border, layout, colors, fontSize, etc.) — the same rich data the on-device Element Inspector shows. Works on Bridgeless / new arch by invoking RN's inspector programmatically. If x/y provided: toggles the overlay on, captures the selection, and toggles it back off (no screenshot pollution). If no coordinates: reads the current selection from a manually-driven overlay.\n" +
484
+ "PURPOSE: Identity + styling. Answers \"what is this and how is it styled?\" — the primary tool for visual/style debugging at a coordinate.\n" +
485
+ "WHEN TO USE: You see a visual issue at a pixel and want the component name AND its style values (e.g. \"why is borderRadius 14 instead of 16?\"). Best for style/CSS-style debugging.\n" +
486
+ "WORKFLOW: ios_screenshot / android_screenshot -> note the suspect pixel -> get_inspector_selection(x, y) -> edit the returned style values.\n" +
487
+ "LIMITATIONS: Requires RN dev mode (__DEV__). x/y are in points/dp. Brief overlay flicker (~600ms total). Source file paths are pre-wired but null on React 19 (where _debugSource was dropped); component name + style is always returned.\n" +
488
+ "DIFFERENCE vs inspect_at_point: get_inspector_selection returns RICH STYLE per ancestor (the inspector's curated view) but only ONE frame (the inspected element). inspect_at_point returns FRAME PER ANCESTOR plus PROPS (handlers, refs, non-style props) but no rich style merging. Use this tool for style/identity; use inspect_at_point for layout measurements and props.\n" +
489
+ "GOOD: get_inspector_selection({ x: 180, y: 420 }) // \"what is this gradient card and what's its borderRadius?\"\n" +
490
+ "BAD: Calling it in a tight loop — prefer inspect_at_point (no overlay toggle, faster, no visual side effect).\n" +
491
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
492
+ inputSchema: {
493
+ x: z
494
+ .number()
495
+ .optional()
496
+ .describe("X coordinate (in points). If provided with y, auto-taps at this location."),
497
+ y: z
498
+ .number()
499
+ .optional()
500
+ .describe("Y coordinate (in points). If provided with x, auto-taps at this location."),
501
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
502
+ }
503
+ }, async ({ x, y, device }) => {
504
+ if (!hasMetro()) {
505
+ const hint = await metroMissingHintIfAbsent("get_inspector_selection");
506
+ return {
507
+ content: [{ type: "text", text: `Inspector selection unavailable.${hint}` }],
508
+ isError: true
509
+ };
510
+ }
511
+ // Coordinate path: use fiber-based hit testing (works on Bridgeless / new arch
512
+ // where RN's built-in inspector cannot populate hierarchy via UIManager.findSubviewIn).
513
+ // Avoids toggling the on-device overlay so screenshots stay clean.
514
+ const result = x !== undefined && y !== undefined
515
+ ? await getInspectorSelectionAtPoint(x, y, device)
516
+ : await getInspectorSelection(device);
517
+ if (!result.success) {
518
+ return {
519
+ content: [{ type: "text", text: `Error: ${result.error}` }],
520
+ isError: true
521
+ };
522
+ }
523
+ try {
524
+ const parsed = JSON.parse(result.result || "{}");
525
+ if (parsed.error) {
526
+ const hint = parsed.hint ? `\n\n${parsed.hint}` : "";
527
+ return {
528
+ content: [{ type: "text", text: `${parsed.error}${hint}` }],
529
+ isError: true
530
+ };
531
+ }
532
+ let output = `Element: ${parsed.element}\n`;
533
+ output += `Path: ${parsed.path}\n`;
534
+ if (parsed.frame) {
535
+ const f = parsed.frame;
536
+ output += `Frame: (${f.left?.toFixed(1)}, ${f.top?.toFixed(1)}) ${f.width?.toFixed?.(1) ?? f.width}x${f.height?.toFixed?.(1) ?? f.height}\n`;
537
+ }
538
+ if (parsed.style) {
539
+ output += `Style: ${JSON.stringify(parsed.style, null, 2)}\n`;
540
+ }
541
+ if (Array.isArray(parsed.hierarchy) && parsed.hierarchy.length > 0) {
542
+ output += `\nHierarchy:\n`;
543
+ for (const h of parsed.hierarchy) {
544
+ output += ` - ${h.name}`;
545
+ if (h.source)
546
+ output += ` (${h.source})`;
547
+ output += `\n`;
548
+ if (h.style && Object.keys(h.style).length > 0) {
549
+ const styleStr = JSON.stringify(h.style);
550
+ output += ` style: ${styleStr.length > 300 ? styleStr.slice(0, 300) + "…" : styleStr}\n`;
551
+ }
552
+ }
553
+ }
554
+ return {
555
+ content: [{ type: "text", text: output }]
556
+ };
557
+ }
558
+ catch {
559
+ return {
560
+ content: [{ type: "text", text: result.result || "No selection data" }]
561
+ };
562
+ }
563
+ });
564
+ // Tool: Inspect component at coordinates (like Element Inspector)
565
+ registerToolWithTelemetry(server, "inspect_at_point", {
566
+ description: "Inspect layout AND props at (x, y). Returns FRAME PER ANCESTOR (position/size in dp for every ancestor that hit-tested the point), plus the innermost component's PROPS (handlers as `[Function]`, refs, custom props like onPress/data/testID). Pure JS hit-test via fiber tree + measureInWindow — no on-device overlay toggled, zero visual side effect. Works on Paper and Fabric.\n" +
567
+ "PURPOSE: Layout/props diagnosis. Answers \"where is each ancestor positioned, and what props does the touched component expose?\"\n" +
568
+ "WHEN TO USE: A button is clipped, an element's hit area is wrong, an animated frame is unexpected — or you need handler/ref/non-style props that the inspector doesn't surface. Also preferred for tight loops or before/after comparisons (no overlay flicker).\n" +
569
+ "WORKFLOW: ios_screenshot -> find suspect pixel -> convert to dp (pixel / pixelRatio) -> inspect_at_point(x, y).\n" +
570
+ "LIMITATIONS: Coordinates MUST be in dp, not screenshot pixels — wrong unit = wrong component. Style is shown as a reference (no rich merging) — for style debugging use get_inspector_selection.\n" +
571
+ "DIFFERENCE vs get_inspector_selection: inspect_at_point returns FRAME PER ANCESTOR + PROPS, no overlay flicker, no rich style. get_inspector_selection returns RICH STYLE per ancestor (padding/margin/border) but only ONE frame and toggles the overlay briefly. Use get_inspector_selection for style/identity; use this tool for layout measurements and props.\n" +
572
+ "GOOD: inspect_at_point({ x: 205, y: 360 }) // \"why is this button's hit area too small?\"\n" +
573
+ "BAD: inspect_at_point({ x: 540, y: 960 }) // raw screenshot pixels — picks the wrong node.\n" +
574
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
575
+ inputSchema: {
576
+ x: z
577
+ .number()
578
+ .describe("X coordinate in dp (logical pixels). Convert from screenshot pixels by dividing by the device pixel ratio."),
579
+ y: z
580
+ .number()
581
+ .describe("Y coordinate in dp (logical pixels). Convert from screenshot pixels by dividing by the device pixel ratio."),
582
+ includeProps: z
583
+ .boolean()
584
+ .optional()
585
+ .default(true)
586
+ .describe("Include component props in the output (default: true)"),
587
+ includeFrame: z
588
+ .boolean()
589
+ .optional()
590
+ .default(true)
591
+ .describe("Include position/dimensions (frame) in the output (default: true)"),
592
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
593
+ }
594
+ }, async ({ x, y, includeProps, includeFrame, device }) => {
595
+ const result = await inspectAtPoint(x, y, { includeProps, includeFrame, device });
596
+ if (!result.success) {
597
+ return {
598
+ content: [
599
+ {
600
+ type: "text",
601
+ text: `Error: ${result.error}`
602
+ }
603
+ ],
604
+ isError: true
605
+ };
606
+ }
607
+ // Parse the result to check for errors in the response
608
+ try {
609
+ const parsed = JSON.parse(result.result || "{}");
610
+ if (parsed.error) {
611
+ const hint = parsed.hint ? `\n\n${parsed.hint}` : "";
612
+ const alternatives = parsed.alternatives
613
+ ? `\n\nAlternatives:\n${parsed.alternatives.map((a) => ` - ${a}`).join("\n")}`
614
+ : "";
615
+ return {
616
+ content: [
617
+ {
618
+ type: "text",
619
+ text: `Inspect at (${x}, ${y}): ${parsed.error}${hint}${alternatives}`
620
+ }
621
+ ],
622
+ isError: true
623
+ };
624
+ }
625
+ }
626
+ catch {
627
+ // If parsing fails, just return the raw result
628
+ }
629
+ return {
630
+ content: [
631
+ {
632
+ type: "text",
633
+ text: `Element at (${x}, ${y}):\n\n${result.result}`
634
+ }
635
+ ]
636
+ };
637
+ });
638
+ }
639
+ //# sourceMappingURL=componentTools.js.map