react-native-ai-debugger 1.8.0 → 1.9.0

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 (197) hide show
  1. package/README.md +73 -66
  2. package/build/__tests__/integration/auto-reconnect.test.d.ts +2 -0
  3. package/build/__tests__/integration/auto-reconnect.test.d.ts.map +1 -0
  4. package/build/__tests__/integration/auto-reconnect.test.js +118 -0
  5. package/build/__tests__/integration/auto-reconnect.test.js.map +1 -0
  6. package/build/__tests__/integration/per-call-timeout.test.d.ts +2 -0
  7. package/build/__tests__/integration/per-call-timeout.test.d.ts.map +1 -0
  8. package/build/__tests__/integration/per-call-timeout.test.js +103 -0
  9. package/build/__tests__/integration/per-call-timeout.test.js.map +1 -0
  10. package/build/__tests__/setup.js +3 -5
  11. package/build/__tests__/setup.js.map +1 -1
  12. package/build/__tests__/unit/debugGlobals.test.d.ts +2 -0
  13. package/build/__tests__/unit/debugGlobals.test.d.ts.map +1 -0
  14. package/build/__tests__/unit/debugGlobals.test.js +14 -0
  15. package/build/__tests__/unit/debugGlobals.test.js.map +1 -0
  16. package/build/__tests__/unit/escapeNonAscii.test.d.ts +2 -0
  17. package/build/__tests__/unit/escapeNonAscii.test.d.ts.map +1 -0
  18. package/build/__tests__/unit/escapeNonAscii.test.js +76 -0
  19. package/build/__tests__/unit/escapeNonAscii.test.js.map +1 -0
  20. package/build/__tests__/unit/executor.test.js +59 -7
  21. package/build/__tests__/unit/executor.test.js.map +1 -1
  22. package/build/__tests__/unit/fastRefreshRecorder.test.d.ts +2 -0
  23. package/build/__tests__/unit/fastRefreshRecorder.test.d.ts.map +1 -0
  24. package/build/__tests__/unit/fastRefreshRecorder.test.js +68 -0
  25. package/build/__tests__/unit/fastRefreshRecorder.test.js.map +1 -0
  26. package/build/__tests__/unit/fastRefreshTools.test.d.ts +2 -0
  27. package/build/__tests__/unit/fastRefreshTools.test.d.ts.map +1 -0
  28. package/build/__tests__/unit/fastRefreshTools.test.js +112 -0
  29. package/build/__tests__/unit/fastRefreshTools.test.js.map +1 -0
  30. package/build/__tests__/unit/fingerprint.test.js +10 -0
  31. package/build/__tests__/unit/fingerprint.test.js.map +1 -1
  32. package/build/__tests__/unit/measureComponent.test.d.ts +2 -0
  33. package/build/__tests__/unit/measureComponent.test.d.ts.map +1 -0
  34. package/build/__tests__/unit/measureComponent.test.js +66 -0
  35. package/build/__tests__/unit/measureComponent.test.js.map +1 -0
  36. package/build/__tests__/unit/measureComponentTools.test.d.ts +2 -0
  37. package/build/__tests__/unit/measureComponentTools.test.d.ts.map +1 -0
  38. package/build/__tests__/unit/measureComponentTools.test.js +94 -0
  39. package/build/__tests__/unit/measureComponentTools.test.js.map +1 -0
  40. package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts +2 -0
  41. package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts.map +1 -0
  42. package/build/__tests__/unit/rnGlobalsBootstrap.test.js +29 -0
  43. package/build/__tests__/unit/rnGlobalsBootstrap.test.js.map +1 -0
  44. package/build/__tests__/unit/timeoutClamp.test.d.ts +2 -0
  45. package/build/__tests__/unit/timeoutClamp.test.d.ts.map +1 -0
  46. package/build/__tests__/unit/timeoutClamp.test.js +17 -0
  47. package/build/__tests__/unit/timeoutClamp.test.js.map +1 -0
  48. package/build/__tests__/unit/transportClassify.test.d.ts +2 -0
  49. package/build/__tests__/unit/transportClassify.test.d.ts.map +1 -0
  50. package/build/__tests__/unit/transportClassify.test.js +60 -0
  51. package/build/__tests__/unit/transportClassify.test.js.map +1 -0
  52. package/build/core/appDetection.d.ts +7 -0
  53. package/build/core/appDetection.d.ts.map +1 -1
  54. package/build/core/appDetection.js +38 -17
  55. package/build/core/appDetection.js.map +1 -1
  56. package/build/core/bundle.d.ts +1 -1
  57. package/build/core/bundle.d.ts.map +1 -1
  58. package/build/core/bundle.js +6 -10
  59. package/build/core/bundle.js.map +1 -1
  60. package/build/core/componentSearch.d.ts +41 -0
  61. package/build/core/componentSearch.d.ts.map +1 -0
  62. package/build/core/componentSearch.js +484 -0
  63. package/build/core/componentSearch.js.map +1 -0
  64. package/build/core/componentTree.d.ts +13 -0
  65. package/build/core/componentTree.d.ts.map +1 -0
  66. package/build/core/componentTree.js +275 -0
  67. package/build/core/componentTree.js.map +1 -0
  68. package/build/core/connection.d.ts +1 -0
  69. package/build/core/connection.d.ts.map +1 -1
  70. package/build/core/connection.js +97 -88
  71. package/build/core/connection.js.map +1 -1
  72. package/build/core/debugGlobals.d.ts +6 -0
  73. package/build/core/debugGlobals.d.ts.map +1 -0
  74. package/build/core/debugGlobals.js +336 -0
  75. package/build/core/debugGlobals.js.map +1 -0
  76. package/build/core/escapeNonAscii.d.ts +18 -0
  77. package/build/core/escapeNonAscii.d.ts.map +1 -0
  78. package/build/core/escapeNonAscii.js +352 -0
  79. package/build/core/escapeNonAscii.js.map +1 -0
  80. package/build/core/executor.d.ts +7 -206
  81. package/build/core/executor.d.ts.map +1 -1
  82. package/build/core/executor.js +10 -4333
  83. package/build/core/executor.js.map +1 -1
  84. package/build/core/fastRefreshRecorder.d.ts +23 -0
  85. package/build/core/fastRefreshRecorder.d.ts.map +1 -0
  86. package/build/core/fastRefreshRecorder.js +83 -0
  87. package/build/core/fastRefreshRecorder.js.map +1 -0
  88. package/build/core/fastRefreshTools.d.ts +19 -0
  89. package/build/core/fastRefreshTools.d.ts.map +1 -0
  90. package/build/core/fastRefreshTools.js +71 -0
  91. package/build/core/fastRefreshTools.js.map +1 -0
  92. package/build/core/guides.d.ts.map +1 -1
  93. package/build/core/guides.js +2 -1
  94. package/build/core/guides.js.map +1 -1
  95. package/build/core/index.d.ts +5 -2
  96. package/build/core/index.d.ts.map +1 -1
  97. package/build/core/index.js +5 -2
  98. package/build/core/index.js.map +1 -1
  99. package/build/core/inspector.d.ts +50 -0
  100. package/build/core/inspector.d.ts.map +1 -0
  101. package/build/core/inspector.js +604 -0
  102. package/build/core/inspector.js.map +1 -0
  103. package/build/core/jsExecute.d.ts +61 -0
  104. package/build/core/jsExecute.d.ts.map +1 -0
  105. package/build/core/jsExecute.js +679 -0
  106. package/build/core/jsExecute.js.map +1 -0
  107. package/build/core/measureComponent.d.ts +19 -0
  108. package/build/core/measureComponent.d.ts.map +1 -0
  109. package/build/core/measureComponent.js +94 -0
  110. package/build/core/measureComponent.js.map +1 -0
  111. package/build/core/measureComponentTools.d.ts +6 -0
  112. package/build/core/measureComponentTools.d.ts.map +1 -0
  113. package/build/core/measureComponentTools.js +43 -0
  114. package/build/core/measureComponentTools.js.map +1 -0
  115. package/build/core/pressables.d.ts +44 -0
  116. package/build/core/pressables.d.ts.map +1 -0
  117. package/build/core/pressables.js +1480 -0
  118. package/build/core/pressables.js.map +1 -0
  119. package/build/core/register.d.ts +8 -0
  120. package/build/core/register.d.ts.map +1 -0
  121. package/build/core/register.js +216 -0
  122. package/build/core/register.js.map +1 -0
  123. package/build/core/rnGlobalsBootstrap.d.ts +16 -0
  124. package/build/core/rnGlobalsBootstrap.d.ts.map +1 -0
  125. package/build/core/rnGlobalsBootstrap.js +101 -0
  126. package/build/core/rnGlobalsBootstrap.js.map +1 -0
  127. package/build/core/screenLayout.d.ts +53 -0
  128. package/build/core/screenLayout.d.ts.map +1 -0
  129. package/build/core/screenLayout.js +828 -0
  130. package/build/core/screenLayout.js.map +1 -0
  131. package/build/core/state.d.ts +2 -2
  132. package/build/core/state.d.ts.map +1 -1
  133. package/build/core/state.js +10 -5
  134. package/build/core/state.js.map +1 -1
  135. package/build/core/telemetry.d.ts +23 -0
  136. package/build/core/telemetry.d.ts.map +1 -1
  137. package/build/core/telemetry.js +67 -0
  138. package/build/core/telemetry.js.map +1 -1
  139. package/build/core/toolHelpers.d.ts +24 -0
  140. package/build/core/toolHelpers.d.ts.map +1 -0
  141. package/build/core/toolHelpers.js +118 -0
  142. package/build/core/toolHelpers.js.map +1 -0
  143. package/build/core/types.d.ts +18 -6
  144. package/build/core/types.d.ts.map +1 -1
  145. package/build/index.d.ts +2 -4
  146. package/build/index.d.ts.map +1 -1
  147. package/build/index.js +37 -4440
  148. package/build/index.js.map +1 -1
  149. package/build/tools/accountTools.d.ts +2 -0
  150. package/build/tools/accountTools.d.ts.map +1 -1
  151. package/build/tools/accountTools.js +5 -0
  152. package/build/tools/accountTools.js.map +1 -1
  153. package/build/tools/bundleTools.d.ts +3 -0
  154. package/build/tools/bundleTools.d.ts.map +1 -0
  155. package/build/tools/bundleTools.js +368 -0
  156. package/build/tools/bundleTools.js.map +1 -0
  157. package/build/tools/componentTools.d.ts +3 -0
  158. package/build/tools/componentTools.d.ts.map +1 -0
  159. package/build/tools/componentTools.js +704 -0
  160. package/build/tools/componentTools.js.map +1 -0
  161. package/build/tools/connectionTools.d.ts +3 -0
  162. package/build/tools/connectionTools.d.ts.map +1 -0
  163. package/build/tools/connectionTools.js +564 -0
  164. package/build/tools/connectionTools.js.map +1 -0
  165. package/build/tools/deviceTools.d.ts +3 -0
  166. package/build/tools/deviceTools.d.ts.map +1 -0
  167. package/build/tools/deviceTools.js +296 -0
  168. package/build/tools/deviceTools.js.map +1 -0
  169. package/build/tools/executionTools.d.ts +3 -0
  170. package/build/tools/executionTools.d.ts.map +1 -0
  171. package/build/tools/executionTools.js +245 -0
  172. package/build/tools/executionTools.js.map +1 -0
  173. package/build/tools/interactionTools.d.ts +3 -0
  174. package/build/tools/interactionTools.d.ts.map +1 -0
  175. package/build/tools/interactionTools.js +558 -0
  176. package/build/tools/interactionTools.js.map +1 -0
  177. package/build/tools/logTools.d.ts +3 -0
  178. package/build/tools/logTools.d.ts.map +1 -0
  179. package/build/tools/logTools.js +371 -0
  180. package/build/tools/logTools.js.map +1 -0
  181. package/build/tools/metaTools.d.ts +7 -0
  182. package/build/tools/metaTools.d.ts.map +1 -0
  183. package/build/tools/metaTools.js +205 -0
  184. package/build/tools/metaTools.js.map +1 -0
  185. package/build/tools/networkTools.d.ts +3 -0
  186. package/build/tools/networkTools.d.ts.map +1 -0
  187. package/build/tools/networkTools.js +478 -0
  188. package/build/tools/networkTools.js.map +1 -0
  189. package/build/tools/reduxTools.d.ts +3 -0
  190. package/build/tools/reduxTools.d.ts.map +1 -0
  191. package/build/tools/reduxTools.js +83 -0
  192. package/build/tools/reduxTools.js.map +1 -0
  193. package/build/tools/screenshotTools.d.ts +3 -0
  194. package/build/tools/screenshotTools.d.ts.map +1 -0
  195. package/build/tools/screenshotTools.js +606 -0
  196. package/build/tools/screenshotTools.js.map +1 -0
  197. package/package.json +1 -1
@@ -0,0 +1,704 @@
1
+ import { z } from "zod";
2
+ import { registerToolWithTelemetry } from "../core/register.js";
3
+ import { getComponentTree, getScreenLayout, getPressableElements, inspectComponent, findComponents, toggleElementInspector, getInspectorSelection, getInspectorSelectionAtPoint, inspectAtPoint, measureComponent, iosScreenshot, androidScreenshot, androidGetDensity, getDevicePixelRatio, getIOSSafeAreaTop, inferIOSDevicePixelRatio, metroMissingHintIfAbsent, hasMetro, getConnectedAppByDevice, getFirstConnectedApp, } from "../core/index.js";
4
+ import { primaryInteractionBanner } from "../core/toolHelpers.js";
5
+ function collectMetaNotes(r) {
6
+ const out = [];
7
+ if (r._meta?.reconnected) {
8
+ out.push(`[reconnected: transport error "${r._meta.transportError ?? "unknown"}" was auto-recovered]`);
9
+ }
10
+ if (r._meta?.timeoutClampedFrom !== undefined) {
11
+ out.push(`[warning: timeoutMs ${r._meta.timeoutClampedFrom} clamped to 120000]`);
12
+ }
13
+ return out;
14
+ }
15
+ export function registerComponentTools(server) {
16
+ // Tool: Get full screen layout (all components with layout styles)
17
+ registerToolWithTelemetry(server, "get_screen_layout", {
18
+ 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.)." +
19
+ primaryInteractionBanner() + "\n" +
20
+ "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" +
21
+ "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" +
22
+ "WORKFLOW: get_screen_layout -> find_components(pattern=\"...\") or inspect_component(componentName=\"...\") -> tap(testID=...) -> get_screen_layout again to confirm.\n" +
23
+ "LIMITATIONS: Coordinates are points/dp, not screenshot pixels — pass them to tap() which handles conversion, do not multiply by devicePixelRatio yourself.\n" +
24
+ "GOOD: get_screen_layout({ extended: true })\n" +
25
+ "BAD: get_screen_layout({ summary: true }) when you actually need to pick a specific element — summary hides the tree.\n" +
26
+ "SEE ALSO: call get_usage_guide(topic=\"layout\") for the full layout-check playbook.",
27
+ inputSchema: {
28
+ extended: z
29
+ .boolean()
30
+ .optional()
31
+ .default(false)
32
+ .describe("Include layout styles (padding, margin, flex, backgroundColor, borderRadius, etc.) for each component. Default: false for compact output."),
33
+ summary: z
34
+ .boolean()
35
+ .optional()
36
+ .default(false)
37
+ .describe("Return only component counts by name instead of full tree (default: false)"),
38
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices."),
39
+ timeoutMs: z.coerce
40
+ .number()
41
+ .optional()
42
+ .describe("Per-call timeout in milliseconds. Default: 5000; bumped to 15000 when extended=true. Hard cap: 120000.")
43
+ }
44
+ }, async ({ extended, summary, device, timeoutMs }) => {
45
+ if (!hasMetro()) {
46
+ const hint = await metroMissingHintIfAbsent("get_screen_layout");
47
+ return {
48
+ content: [{ type: "text", text: `Screen Layout unavailable.${hint}` }],
49
+ isError: true
50
+ };
51
+ }
52
+ const effectiveTimeoutMs = timeoutMs ?? (extended ? 15000 : 5000);
53
+ const result = await getScreenLayout({ extended, summary, device, timeoutMs: effectiveTimeoutMs });
54
+ const metaNotes = collectMetaNotes(result);
55
+ if (!result.success) {
56
+ const errText = metaNotes.length > 0 ? `Error: ${result.error}\n\n${metaNotes.join("\n")}` : `Error: ${result.error}`;
57
+ return {
58
+ content: [{ type: "text", text: errText }],
59
+ isError: true
60
+ };
61
+ }
62
+ const body = metaNotes.length > 0
63
+ ? `Screen Layout:\n\n${result.result}\n\n${metaNotes.join("\n")}`
64
+ : `Screen Layout:\n\n${result.result}`;
65
+ return { content: [{ type: "text", text: body }] };
66
+ });
67
+ // ============================================================================
68
+ // React Component Inspection Tools
69
+ // ============================================================================
70
+ // Tool: Get the React component tree
71
+ registerToolWithTelemetry(server, "get_component_tree", {
72
+ 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" +
73
+ "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" +
74
+ "WHEN TO USE: Debugging context propagation, navigation wrappers, hidden modals, or when you need to understand the full React architecture.\n" +
75
+ "WORKFLOW: get_component_tree(structureOnly=true) for overview -> find_components for targeted lookup -> inspect_component for props/state.\n" +
76
+ "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" +
77
+ "GOOD: get_component_tree({ structureOnly: true })\n" +
78
+ "BAD: get_component_tree({ includeProps: true, includeStyles: true }) on a large app — likely hits response-size limits. Prefer inspect_component for specific nodes.\n" +
79
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
80
+ inputSchema: {
81
+ structureOnly: z
82
+ .boolean()
83
+ .optional()
84
+ .default(false)
85
+ .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."),
86
+ maxDepth: z
87
+ .number()
88
+ .optional()
89
+ .describe("Maximum tree depth (default: 5000)"),
90
+ includeProps: z
91
+ .boolean()
92
+ .optional()
93
+ .default(false)
94
+ .describe("Include component props (excluding children and style). Ignored if structureOnly=true."),
95
+ includeStyles: z
96
+ .boolean()
97
+ .optional()
98
+ .default(false)
99
+ .describe("Include layout styles (padding, margin, flex, etc.). Ignored if structureOnly=true."),
100
+ hideInternals: z
101
+ .boolean()
102
+ .optional()
103
+ .default(true)
104
+ .describe("Hide internal RN components (RCTView, RNS*, Animated, etc.) for cleaner output (default: true)"),
105
+ format: z
106
+ .enum(["json", "tonl"])
107
+ .optional()
108
+ .default("tonl")
109
+ .describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true."),
110
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices."),
111
+ timeoutMs: z.coerce
112
+ .number()
113
+ .optional()
114
+ .describe("Per-call timeout in milliseconds. Default: 5000. Hard cap: 120000.")
115
+ }
116
+ }, async ({ structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format, device, timeoutMs }) => {
117
+ const effectiveTimeoutMs = timeoutMs ?? 5000;
118
+ const result = await getComponentTree({
119
+ structureOnly,
120
+ maxDepth,
121
+ includeProps,
122
+ includeStyles,
123
+ hideInternals,
124
+ format,
125
+ device,
126
+ timeoutMs: effectiveTimeoutMs
127
+ });
128
+ const metaNotes = collectMetaNotes(result);
129
+ if (!result.success) {
130
+ const errText = metaNotes.length > 0 ? `Error: ${result.error}\n\n${metaNotes.join("\n")}` : `Error: ${result.error}`;
131
+ return {
132
+ content: [{ type: "text", text: errText }],
133
+ isError: true
134
+ };
135
+ }
136
+ const body = metaNotes.length > 0
137
+ ? `React Component Tree:\n\n${result.result}\n\n${metaNotes.join("\n")}`
138
+ : `React Component Tree:\n\n${result.result}`;
139
+ return { content: [{ type: "text", text: body }] };
140
+ });
141
+ // Tool: Get all pressable elements on screen
142
+ registerToolWithTelemetry(server, "get_pressable_elements", {
143
+ 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" +
144
+ "HELPER — call before `tap` when you need to enumerate candidate elements before committing to a target; not a replacement for tap itself.\n" +
145
+ "PURPOSE: Produce a ready-to-tap inventory of every interactive element on screen with screenshot-pixel coordinates that tap(x, y) accepts directly.\n" +
146
+ "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" +
147
+ "WORKFLOW: ios_screenshot / android_screenshot -> get_pressable_elements -> tap(testID=\"...\") or tap(x, y) using the center coordinates.\n" +
148
+ "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" +
149
+ "GOOD: get_pressable_elements()\n" +
150
+ "BAD: Calling when tap(testID=\"...\") already works — testID matching is faster and more stable.\n" +
151
+ "SEE ALSO: call get_usage_guide(topic=\"interact\") for the full interaction playbook.",
152
+ inputSchema: {
153
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
154
+ }
155
+ }, async ({ device }) => {
156
+ const result = await getPressableElements({ device });
157
+ if (!result.success) {
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `Error: ${result.error}`
163
+ }
164
+ ],
165
+ isError: true
166
+ };
167
+ }
168
+ const elements = result.parsedElements;
169
+ if (!elements || elements.length === 0) {
170
+ return {
171
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
172
+ };
173
+ }
174
+ // Resolve target app so we can pick the right simulator / android device
175
+ const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
176
+ if (!targetApp) {
177
+ // No connected app — return raw (points) output; better than nothing
178
+ return {
179
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
180
+ };
181
+ }
182
+ // Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
183
+ // Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
184
+ let lines = [];
185
+ try {
186
+ if (targetApp.platform === "ios") {
187
+ const udid = targetApp.simulatorUdid;
188
+ const shot = await iosScreenshot(undefined, udid);
189
+ const screenshotScale = shot.scaleFactor || 1;
190
+ const devicePixelRatio = (shot.originalWidth && shot.originalHeight
191
+ ? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
192
+ : null) ?? (await getDevicePixelRatio(udid)) ?? 3;
193
+ // Fallback to 59pt (iPhone typical) when the UI driver preflight can't
194
+ // resolve the true inset — matches ios_screenshot's default. Without this
195
+ // shift, react-native-screens modal-presented screens report y relative
196
+ // to content origin and taps land in the status bar instead of the button.
197
+ const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
198
+ // Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
199
+ // same scaleFactor when converting our pixel coords back to points.
200
+ if (shot.originalWidth && shot.originalHeight) {
201
+ targetApp.lastScreenshot = {
202
+ originalWidth: shot.originalWidth,
203
+ originalHeight: shot.originalHeight,
204
+ scaleFactor: screenshotScale
205
+ };
206
+ }
207
+ lines = formatPressablesInPixels(elements, {
208
+ platform: "ios",
209
+ devicePixelRatio,
210
+ screenshotScale,
211
+ safeAreaTop
212
+ });
213
+ }
214
+ else {
215
+ // Metro's `deviceName` is the device model (e.g. "sdk_gphone16k_arm64"),
216
+ // not the adb serial (e.g. "emulator-5554"), so passing it as -s makes
217
+ // adb miss the device and androidScreenshot/androidGetDensity silently
218
+ // return defaults (scale=1, density=160). That leaves coords in raw
219
+ // device pixels — off by ~1.2× from the screenshot/JPEG space the tool
220
+ // description promises. Pass undefined to let adb auto-pick (matches
221
+ // how android_screenshot works when called without deviceId). Multi-
222
+ // Android-device support tracks separately under the multi-device
223
+ // refactor; this path is a single-Android-device fix.
224
+ const shot = await androidScreenshot(undefined, undefined);
225
+ const screenshotScale = shot.scaleFactor || 1;
226
+ const density = await androidGetDensity(undefined).catch(() => ({ density: 160 }));
227
+ const devicePixelRatio = (density.density || 160) / 160;
228
+ if (shot.originalWidth && shot.originalHeight) {
229
+ targetApp.lastScreenshot = {
230
+ originalWidth: shot.originalWidth,
231
+ originalHeight: shot.originalHeight,
232
+ scaleFactor: screenshotScale
233
+ };
234
+ }
235
+ lines = formatPressablesInPixels(elements, {
236
+ platform: "android",
237
+ devicePixelRatio,
238
+ screenshotScale,
239
+ safeAreaTop: 0
240
+ });
241
+ }
242
+ }
243
+ catch {
244
+ // Fallback to points if screenshot/metadata unavailable
245
+ return {
246
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
247
+ };
248
+ }
249
+ const iconCount = elements.filter((e) => !e.hasLabel).length;
250
+ const labeledCount = elements.length - iconCount;
251
+ const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
252
+ const text = [summary, "", ...lines].join("\n");
253
+ return {
254
+ content: [{ type: "text", text }]
255
+ };
256
+ });
257
+ function formatPressablesInPixels(elements, opts) {
258
+ const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
259
+ const out = [];
260
+ for (let i = 0; i < elements.length; i++) {
261
+ const el = elements[i];
262
+ // react-native-screens modal presentations report y relative to content origin;
263
+ // shift into the window frame when the measurement falls inside the safe-area band.
264
+ let cy = el.center.y;
265
+ let fy = el.frame.y;
266
+ if (platform === "ios" && safeAreaTop > 0) {
267
+ if (cy < safeAreaTop)
268
+ cy += safeAreaTop;
269
+ if (fy < safeAreaTop)
270
+ fy += safeAreaTop;
271
+ }
272
+ // iOS: fiber returns points → convert points × DPR / screenshotScale = JPEG px.
273
+ // Android: getPressableElements reconciles fiber DP against uiautomator device-pixel
274
+ // bounds (executor.ts, 2026-05-17). After reconciliation, coords are already in
275
+ // device pixels — only the JPEG downscale needs to be applied. Multiplying by DPR
276
+ // here would re-inflate them by ~density/160 (~2.6× on a 420dpi device), reproducing
277
+ // the original out-of-bounds bug.
278
+ const toPx = (v) => platform === "android"
279
+ ? Math.round(v / screenshotScale)
280
+ : Math.round((v * devicePixelRatio) / screenshotScale);
281
+ const cx = toPx(el.center.x);
282
+ const cyPx = toPx(cy);
283
+ const fx = toPx(el.frame.x);
284
+ const fyPx = toPx(fy);
285
+ const fw = toPx(el.frame.width);
286
+ const fh = toPx(el.frame.height);
287
+ const num = i + 1;
288
+ const label = el.hasLabel
289
+ ? `"${el.text}"`
290
+ : el.intent
291
+ ? `(${el.intent} icon)`
292
+ : "(icon/image)";
293
+ const ids = [];
294
+ if (el.testID)
295
+ ids.push(`testID="${el.testID}"`);
296
+ if (el.accessibilityLabel)
297
+ ids.push(`a11y="${el.accessibilityLabel}"`);
298
+ const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
299
+ const inputStr = el.isInput ? " (input)" : "";
300
+ const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
301
+ const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
302
+ out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
303
+ if (el.path)
304
+ out.push(` path: ${el.path}`);
305
+ }
306
+ return out;
307
+ }
308
+ // Tool: Inspect a specific component by name
309
+ registerToolWithTelemetry(server, "inspect_component", {
310
+ 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." +
311
+ primaryInteractionBanner() + "\n" +
312
+ "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" +
313
+ "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" +
314
+ "WORKFLOW: get_screen_layout or find_components -> inspect_component(componentName=\"Foo\") -> tap or execute_in_app to change state -> inspect_component again.\n" +
315
+ "LIMITATIONS: Requires the component to be currently mounted in the fiber tree. Name matching is exact; use find_components for fuzzy/regex lookup.\n" +
316
+ "GOOD: inspect_component({ componentName: \"SneakerCard\", index: 0 })\n" +
317
+ "BAD: inspect_component({ componentName: \"Card\" }) when many Card instances exist — pass index or narrow via find_components.\n" +
318
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
319
+ inputSchema: {
320
+ componentName: z
321
+ .string()
322
+ .describe("Name of the component to inspect (e.g., 'Button', 'HomeScreen', 'FlatList')"),
323
+ index: z
324
+ .number()
325
+ .optional()
326
+ .default(0)
327
+ .describe("If multiple instances exist, which one to inspect (0-based index, default: 0)"),
328
+ includeState: z
329
+ .boolean()
330
+ .optional()
331
+ .default(true)
332
+ .describe("Include component state/hooks (default: true)"),
333
+ includeChildren: z.boolean().optional().default(false).describe("Include children component tree"),
334
+ childrenDepth: z
335
+ .number()
336
+ .optional()
337
+ .default(1)
338
+ .describe("How many levels deep to show children (default: 1 = direct children only, 2+ = nested tree)"),
339
+ includeStyle: z
340
+ .boolean()
341
+ .optional()
342
+ .default(false)
343
+ .describe("Include flattened style on each child entry (only with includeChildren). Use when debugging 'why isn't X style applying' or cascade-like inheritance on nested elements (e.g., textAlign on an inner Text, flex on a wrapper View). Resolves StyleSheet IDs and array/conditional styles to a single object. Default: false."),
344
+ shortPath: z.boolean().optional().default(true).describe("Show only last 3 path segments (default: true)"),
345
+ simplifyHooks: z
346
+ .boolean()
347
+ .optional()
348
+ .default(true)
349
+ .describe("Simplify hooks output by hiding effects and reducing depth (default: true)"),
350
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices."),
351
+ timeoutMs: z.coerce
352
+ .number()
353
+ .optional()
354
+ .describe("Per-call timeout in milliseconds. Default: 5000. Hard cap: 120000.")
355
+ }
356
+ }, async ({ componentName, index, includeState, includeChildren, childrenDepth, includeStyle, shortPath, simplifyHooks, device, timeoutMs }) => {
357
+ const effectiveTimeoutMs = timeoutMs ?? 5000;
358
+ const result = await inspectComponent(componentName, {
359
+ index,
360
+ includeState,
361
+ includeChildren,
362
+ childrenDepth,
363
+ includeStyle,
364
+ shortPath,
365
+ simplifyHooks,
366
+ device,
367
+ timeoutMs: effectiveTimeoutMs
368
+ });
369
+ const metaNotes = collectMetaNotes(result);
370
+ if (!result.success) {
371
+ const errText = metaNotes.length > 0 ? `Error: ${result.error}\n\n${metaNotes.join("\n")}` : `Error: ${result.error}`;
372
+ return {
373
+ content: [{ type: "text", text: errText }],
374
+ isError: true
375
+ };
376
+ }
377
+ const body = metaNotes.length > 0
378
+ ? `Component Inspection: ${componentName}\n\n${result.result}\n\n${metaNotes.join("\n")}`
379
+ : `Component Inspection: ${componentName}\n\n${result.result}`;
380
+ return { content: [{ type: "text", text: body }] };
381
+ });
382
+ // Tool: Find components matching a pattern
383
+ registerToolWithTelemetry(server, "find_components", {
384
+ 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." +
385
+ primaryInteractionBanner() + "\n" +
386
+ "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" +
387
+ "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" +
388
+ "WORKFLOW: get_screen_layout (orient) -> find_components(pattern=\"...\") -> inspect_component(componentName=\"...\", index=N).\n" +
389
+ "LIMITATIONS: Matches the React display name only; minified builds may return opaque names. Large result sets — use maxResults or a tighter pattern.\n" +
390
+ "GOOD: find_components({ pattern: \"Button\" }); find_components({ pattern: \"Screen$\" })\n" +
391
+ "BAD: find_components({ pattern: \".*\" }) — floods the response; narrow the regex.\n" +
392
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
393
+ inputSchema: {
394
+ pattern: z
395
+ .string()
396
+ .describe("Regex pattern to match component names (case-insensitive). Examples: 'Button', 'Screen$', 'List.*Item'"),
397
+ maxResults: z.number().optional().default(20).describe("Maximum number of results to return (default: 20)"),
398
+ includeLayout: z
399
+ .boolean()
400
+ .optional()
401
+ .default(false)
402
+ .describe("Include layout styles (padding, margin, flex) for each matched component"),
403
+ shortPath: z.boolean().optional().default(true).describe("Show only last 3 path segments (default: true)"),
404
+ summary: z
405
+ .boolean()
406
+ .optional()
407
+ .default(false)
408
+ .describe("Return only component counts by name instead of full list (default: false)"),
409
+ format: z
410
+ .enum(["json", "tonl"])
411
+ .optional()
412
+ .default("tonl")
413
+ .describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)"),
414
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices."),
415
+ timeoutMs: z.coerce
416
+ .number()
417
+ .optional()
418
+ .describe("Per-call timeout in milliseconds. Default: 5000. Hard cap: 120000.")
419
+ }
420
+ }, async ({ pattern, maxResults, includeLayout, shortPath, summary, format, device, timeoutMs }) => {
421
+ const effectiveTimeoutMs = timeoutMs ?? 5000;
422
+ const result = await findComponents(pattern, {
423
+ maxResults, includeLayout, shortPath, summary, format, device,
424
+ timeoutMs: effectiveTimeoutMs,
425
+ });
426
+ const metaNotes = collectMetaNotes(result);
427
+ if (!result.success) {
428
+ const errText = metaNotes.length > 0 ? `Error: ${result.error}\n\n${metaNotes.join("\n")}` : `Error: ${result.error}`;
429
+ return {
430
+ content: [{ type: "text", text: errText }],
431
+ isError: true
432
+ };
433
+ }
434
+ const body = metaNotes.length > 0
435
+ ? `Find Components (pattern: "${pattern}"):\n\n${result.result}\n\n${metaNotes.join("\n")}`
436
+ : `Find Components (pattern: "${pattern}"):\n\n${result.result}`;
437
+ return { content: [{ type: "text", text: body }] };
438
+ });
439
+ // Tool: Toggle Element Inspector programmatically
440
+ registerToolWithTelemetry(server, "toggle_element_inspector", {
441
+ 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" +
442
+ "PURPOSE: Manual control over the on-device inspector overlay.\n" +
443
+ "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" +
444
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
445
+ inputSchema: {
446
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
447
+ }
448
+ }, async ({ device }) => {
449
+ const result = await toggleElementInspector(device);
450
+ if (!result.success) {
451
+ return {
452
+ content: [
453
+ {
454
+ type: "text",
455
+ text: `Error: ${result.error}`
456
+ }
457
+ ],
458
+ isError: true
459
+ };
460
+ }
461
+ try {
462
+ const parsed = JSON.parse(result.result || "{}");
463
+ if (parsed.error) {
464
+ return {
465
+ content: [
466
+ {
467
+ type: "text",
468
+ text: `Failed to toggle Element Inspector: ${parsed.error}`
469
+ }
470
+ ],
471
+ isError: true
472
+ };
473
+ }
474
+ return {
475
+ content: [
476
+ {
477
+ type: "text",
478
+ text: parsed.message || "Element Inspector toggled successfully"
479
+ }
480
+ ]
481
+ };
482
+ }
483
+ catch {
484
+ return {
485
+ content: [
486
+ {
487
+ type: "text",
488
+ text: result.result || "Element Inspector toggled"
489
+ }
490
+ ]
491
+ };
492
+ }
493
+ });
494
+ // Tool: Get currently selected element from Element Inspector
495
+ registerToolWithTelemetry(server, "get_inspector_selection", {
496
+ 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" +
497
+ "PURPOSE: Identity + styling. Answers \"what is this and how is it styled?\" — the primary tool for visual/style debugging at a coordinate.\n" +
498
+ "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" +
499
+ "WORKFLOW: ios_screenshot / android_screenshot -> note the suspect pixel -> get_inspector_selection(x, y) -> edit the returned style values.\n" +
500
+ "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" +
501
+ "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" +
502
+ "GOOD: get_inspector_selection({ x: 180, y: 420 }) // \"what is this gradient card and what's its borderRadius?\"\n" +
503
+ "BAD: Calling it in a tight loop — prefer inspect_at_point (no overlay toggle, faster, no visual side effect).\n" +
504
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
505
+ inputSchema: {
506
+ x: z
507
+ .number()
508
+ .optional()
509
+ .describe("X coordinate (in points). If provided with y, auto-taps at this location."),
510
+ y: z
511
+ .number()
512
+ .optional()
513
+ .describe("Y coordinate (in points). If provided with x, auto-taps at this location."),
514
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
515
+ }
516
+ }, async ({ x, y, device }) => {
517
+ if (!hasMetro()) {
518
+ const hint = await metroMissingHintIfAbsent("get_inspector_selection");
519
+ return {
520
+ content: [{ type: "text", text: `Inspector selection unavailable.${hint}` }],
521
+ isError: true
522
+ };
523
+ }
524
+ // Coordinate path: use fiber-based hit testing (works on Bridgeless / new arch
525
+ // where RN's built-in inspector cannot populate hierarchy via UIManager.findSubviewIn).
526
+ // Avoids toggling the on-device overlay so screenshots stay clean.
527
+ const result = x !== undefined && y !== undefined
528
+ ? await getInspectorSelectionAtPoint(x, y, device)
529
+ : await getInspectorSelection(device);
530
+ if (!result.success) {
531
+ return {
532
+ content: [{ type: "text", text: `Error: ${result.error}` }],
533
+ isError: true
534
+ };
535
+ }
536
+ try {
537
+ const parsed = JSON.parse(result.result || "{}");
538
+ if (parsed.error) {
539
+ const hint = parsed.hint ? `\n\n${parsed.hint}` : "";
540
+ return {
541
+ content: [{ type: "text", text: `${parsed.error}${hint}` }],
542
+ isError: true
543
+ };
544
+ }
545
+ let output = `Element: ${parsed.element}\n`;
546
+ output += `Path: ${parsed.path}\n`;
547
+ if (parsed.frame) {
548
+ const f = parsed.frame;
549
+ output += `Frame: (${f.left?.toFixed(1)}, ${f.top?.toFixed(1)}) ${f.width?.toFixed?.(1) ?? f.width}x${f.height?.toFixed?.(1) ?? f.height}\n`;
550
+ }
551
+ if (parsed.style) {
552
+ output += `Style: ${JSON.stringify(parsed.style, null, 2)}\n`;
553
+ }
554
+ if (Array.isArray(parsed.hierarchy) && parsed.hierarchy.length > 0) {
555
+ output += `\nHierarchy:\n`;
556
+ for (const h of parsed.hierarchy) {
557
+ output += ` - ${h.name}`;
558
+ if (h.source)
559
+ output += ` (${h.source})`;
560
+ output += `\n`;
561
+ if (h.style && Object.keys(h.style).length > 0) {
562
+ const styleStr = JSON.stringify(h.style);
563
+ output += ` style: ${styleStr.length > 300 ? styleStr.slice(0, 300) + "…" : styleStr}\n`;
564
+ }
565
+ }
566
+ }
567
+ return {
568
+ content: [{ type: "text", text: output }]
569
+ };
570
+ }
571
+ catch {
572
+ return {
573
+ content: [{ type: "text", text: result.result || "No selection data" }]
574
+ };
575
+ }
576
+ });
577
+ // Tool: Inspect component at coordinates (like Element Inspector)
578
+ registerToolWithTelemetry(server, "inspect_at_point", {
579
+ 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" +
580
+ "PURPOSE: Layout/props diagnosis. Answers \"where is each ancestor positioned, and what props does the touched component expose?\"\n" +
581
+ "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" +
582
+ "WORKFLOW: ios_screenshot -> find suspect pixel -> convert to dp (pixel / pixelRatio) -> inspect_at_point(x, y).\n" +
583
+ "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" +
584
+ "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" +
585
+ "GOOD: inspect_at_point({ x: 205, y: 360 }) // \"why is this button's hit area too small?\"\n" +
586
+ "BAD: inspect_at_point({ x: 540, y: 960 }) // raw screenshot pixels — picks the wrong node.\n" +
587
+ "SEE ALSO: call get_usage_guide(topic=\"inspect\") for the full component-inspect playbook.",
588
+ inputSchema: {
589
+ x: z
590
+ .number()
591
+ .describe("X coordinate in dp (logical pixels). Convert from screenshot pixels by dividing by the device pixel ratio."),
592
+ y: z
593
+ .number()
594
+ .describe("Y coordinate in dp (logical pixels). Convert from screenshot pixels by dividing by the device pixel ratio."),
595
+ includeProps: z
596
+ .boolean()
597
+ .optional()
598
+ .default(true)
599
+ .describe("Include component props in the output (default: true)"),
600
+ includeFrame: z
601
+ .boolean()
602
+ .optional()
603
+ .default(true)
604
+ .describe("Include position/dimensions (frame) in the output (default: true)"),
605
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
606
+ }
607
+ }, async ({ x, y, includeProps, includeFrame, device }) => {
608
+ const result = await inspectAtPoint(x, y, { includeProps, includeFrame, device });
609
+ if (!result.success) {
610
+ return {
611
+ content: [
612
+ {
613
+ type: "text",
614
+ text: `Error: ${result.error}`
615
+ }
616
+ ],
617
+ isError: true
618
+ };
619
+ }
620
+ // Parse the result to check for errors in the response
621
+ try {
622
+ const parsed = JSON.parse(result.result || "{}");
623
+ if (parsed.error) {
624
+ const hint = parsed.hint ? `\n\n${parsed.hint}` : "";
625
+ const alternatives = parsed.alternatives
626
+ ? `\n\nAlternatives:\n${parsed.alternatives.map((a) => ` - ${a}`).join("\n")}`
627
+ : "";
628
+ return {
629
+ content: [
630
+ {
631
+ type: "text",
632
+ text: `Inspect at (${x}, ${y}): ${parsed.error}${hint}${alternatives}`
633
+ }
634
+ ],
635
+ isError: true
636
+ };
637
+ }
638
+ }
639
+ catch {
640
+ // If parsing fails, just return the raw result
641
+ }
642
+ return {
643
+ content: [
644
+ {
645
+ type: "text",
646
+ text: `Element at (${x}, ${y}):\n\n${result.result}`
647
+ }
648
+ ]
649
+ };
650
+ });
651
+ // Tool: Measure on-screen geometry of a named component
652
+ registerToolWithTelemetry(server, "measure", {
653
+ description: "Get on-screen geometry {x, y, width, height} for a named React component instance. Calls measureInWindow on the matched fiber (or its nearest host descendant for composite components). Coordinates are in points (iOS) / dp (Android), same space as get_screen_layout and inspect_at_point.\n" +
654
+ "PURPOSE: One-shot, name-based component measurement — avoids hand-rolling fiber walks and Promise-wrapping measureInWindow callbacks in execute_in_app.\n" +
655
+ "WHEN TO USE: You already know the component's display name (from get_screen_layout or find_components) and just need its current bounds — e.g. to verify a layout change, compute a tap target, or compare against design specs.\n" +
656
+ "WORKFLOW: find_components(pattern=\"...\") -> measure(componentName=\"...\", index=N) -> tap(x, y) at the center, or inspect_at_point at the center to verify identity.\n" +
657
+ "LIMITATIONS: Returns post-layout on-screen geometry only — for static style use find_components({ includeLayout: true }). For point-based lookup use inspect_at_point. Off-screen fibers may return zeros; that's the truth, not an error. Composites with multiple host descendants return the first host descendant's bounds.\n" +
658
+ "GOOD: measure({ componentName: \"SneakerCard\", index: 0 })\n" +
659
+ "BAD: measure({ componentName: \"View\" }) — too generic; narrow with find_components first.\n" +
660
+ "SEE ALSO: inspect_at_point for point-based variant; find_components({ includeLayout: true }) for static style; get_usage_guide(topic=\"inspect\") for the full playbook.",
661
+ inputSchema: {
662
+ componentName: z
663
+ .string()
664
+ .describe("Exact React display name to match (same matcher as inspect_component)."),
665
+ index: z
666
+ .number()
667
+ .optional()
668
+ .default(0)
669
+ .describe("0-based index when multiple instances match (default: 0)."),
670
+ device: z
671
+ .string()
672
+ .optional()
673
+ .describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
674
+ }
675
+ }, async ({ componentName, index, device }) => {
676
+ if (!hasMetro()) {
677
+ const hint = await metroMissingHintIfAbsent("measure");
678
+ return {
679
+ content: [{ type: "text", text: `measure unavailable.${hint}` }],
680
+ isError: true
681
+ };
682
+ }
683
+ const result = await measureComponent(componentName, index ?? 0, device);
684
+ if (!result.success) {
685
+ return {
686
+ content: [{ type: "text", text: `Error: ${result.error}` }],
687
+ isError: true,
688
+ _errorContext: result.outcome
689
+ };
690
+ }
691
+ const lines = [
692
+ `Component: ${result.name}`,
693
+ `Frame: (${result.x.toFixed(1)}, ${result.y.toFixed(1)}) ${result.width.toFixed(1)}x${result.height.toFixed(1)}`,
694
+ `Center: (${(result.x + result.width / 2).toFixed(1)}, ${(result.y + result.height / 2).toFixed(1)})`,
695
+ ];
696
+ if (typeof result.nativeTag === "number") {
697
+ lines.push(`nativeTag: ${result.nativeTag}`);
698
+ }
699
+ return {
700
+ content: [{ type: "text", text: lines.join("\n") }]
701
+ };
702
+ });
703
+ }
704
+ //# sourceMappingURL=componentTools.js.map