execbro 2.12.3 → 2.12.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 (266) hide show
  1. package/README.md +15 -1
  2. package/build/__tests__/unit/autoReconnectOptOut.test.js +3 -0
  3. package/build/__tests__/unit/autoReconnectOptOut.test.js.map +1 -1
  4. package/build/__tests__/unit/cdpCommand.test.d.ts +2 -0
  5. package/build/__tests__/unit/cdpCommand.test.d.ts.map +1 -0
  6. package/build/__tests__/unit/cdpCommand.test.js +84 -0
  7. package/build/__tests__/unit/cdpCommand.test.js.map +1 -0
  8. package/build/__tests__/unit/chromium.test.d.ts +2 -0
  9. package/build/__tests__/unit/chromium.test.d.ts.map +1 -0
  10. package/build/__tests__/unit/chromium.test.js +118 -0
  11. package/build/__tests__/unit/chromium.test.js.map +1 -0
  12. package/build/__tests__/unit/chromiumDevice.test.d.ts +2 -0
  13. package/build/__tests__/unit/chromiumDevice.test.d.ts.map +1 -0
  14. package/build/__tests__/unit/chromiumDevice.test.js +115 -0
  15. package/build/__tests__/unit/chromiumDevice.test.js.map +1 -0
  16. package/build/__tests__/unit/chromiumDialogAnswer.test.d.ts +2 -0
  17. package/build/__tests__/unit/chromiumDialogAnswer.test.d.ts.map +1 -0
  18. package/build/__tests__/unit/chromiumDialogAnswer.test.js +71 -0
  19. package/build/__tests__/unit/chromiumDialogAnswer.test.js.map +1 -0
  20. package/build/__tests__/unit/chromiumDialogRaces.test.d.ts +2 -0
  21. package/build/__tests__/unit/chromiumDialogRaces.test.d.ts.map +1 -0
  22. package/build/__tests__/unit/chromiumDialogRaces.test.js +100 -0
  23. package/build/__tests__/unit/chromiumDialogRaces.test.js.map +1 -0
  24. package/build/__tests__/unit/chromiumDialogs.test.d.ts +2 -0
  25. package/build/__tests__/unit/chromiumDialogs.test.d.ts.map +1 -0
  26. package/build/__tests__/unit/chromiumDialogs.test.js +167 -0
  27. package/build/__tests__/unit/chromiumDialogs.test.js.map +1 -0
  28. package/build/__tests__/unit/chromiumDiscovery.test.d.ts +2 -0
  29. package/build/__tests__/unit/chromiumDiscovery.test.d.ts.map +1 -0
  30. package/build/__tests__/unit/chromiumDiscovery.test.js +302 -0
  31. package/build/__tests__/unit/chromiumDiscovery.test.js.map +1 -0
  32. package/build/__tests__/unit/chromiumDom.test.d.ts +2 -0
  33. package/build/__tests__/unit/chromiumDom.test.d.ts.map +1 -0
  34. package/build/__tests__/unit/chromiumDom.test.js +119 -0
  35. package/build/__tests__/unit/chromiumDom.test.js.map +1 -0
  36. package/build/__tests__/unit/chromiumGate.test.d.ts +2 -0
  37. package/build/__tests__/unit/chromiumGate.test.d.ts.map +1 -0
  38. package/build/__tests__/unit/chromiumGate.test.js +149 -0
  39. package/build/__tests__/unit/chromiumGate.test.js.map +1 -0
  40. package/build/__tests__/unit/chromiumInput.test.d.ts +2 -0
  41. package/build/__tests__/unit/chromiumInput.test.d.ts.map +1 -0
  42. package/build/__tests__/unit/chromiumInput.test.js +113 -0
  43. package/build/__tests__/unit/chromiumInput.test.js.map +1 -0
  44. package/build/__tests__/unit/chromiumKeys.test.d.ts +2 -0
  45. package/build/__tests__/unit/chromiumKeys.test.d.ts.map +1 -0
  46. package/build/__tests__/unit/chromiumKeys.test.js +26 -0
  47. package/build/__tests__/unit/chromiumKeys.test.js.map +1 -0
  48. package/build/__tests__/unit/chromiumScreen.test.d.ts +2 -0
  49. package/build/__tests__/unit/chromiumScreen.test.d.ts.map +1 -0
  50. package/build/__tests__/unit/chromiumScreen.test.js +390 -0
  51. package/build/__tests__/unit/chromiumScreen.test.js.map +1 -0
  52. package/build/__tests__/unit/chromiumStaleTarget.test.d.ts +2 -0
  53. package/build/__tests__/unit/chromiumStaleTarget.test.d.ts.map +1 -0
  54. package/build/__tests__/unit/chromiumStaleTarget.test.js +26 -0
  55. package/build/__tests__/unit/chromiumStaleTarget.test.js.map +1 -0
  56. package/build/__tests__/unit/connectionGaps.test.d.ts +2 -0
  57. package/build/__tests__/unit/connectionGaps.test.d.ts.map +1 -0
  58. package/build/__tests__/unit/connectionGaps.test.js +14 -0
  59. package/build/__tests__/unit/connectionGaps.test.js.map +1 -0
  60. package/build/__tests__/unit/contextPreambleGlobals.test.d.ts +2 -0
  61. package/build/__tests__/unit/contextPreambleGlobals.test.d.ts.map +1 -0
  62. package/build/__tests__/unit/contextPreambleGlobals.test.js +25 -0
  63. package/build/__tests__/unit/contextPreambleGlobals.test.js.map +1 -0
  64. package/build/__tests__/unit/deviceResolver.test.js +9 -0
  65. package/build/__tests__/unit/deviceResolver.test.js.map +1 -1
  66. package/build/__tests__/unit/electronLaunch.test.d.ts +2 -0
  67. package/build/__tests__/unit/electronLaunch.test.d.ts.map +1 -0
  68. package/build/__tests__/unit/electronLaunch.test.js +165 -0
  69. package/build/__tests__/unit/electronLaunch.test.js.map +1 -0
  70. package/build/__tests__/unit/fiberRoots.test.d.ts +2 -0
  71. package/build/__tests__/unit/fiberRoots.test.d.ts.map +1 -0
  72. package/build/__tests__/unit/fiberRoots.test.js +95 -0
  73. package/build/__tests__/unit/fiberRoots.test.js.map +1 -0
  74. package/build/__tests__/unit/fiberRootsDrift.test.d.ts +2 -0
  75. package/build/__tests__/unit/fiberRootsDrift.test.d.ts.map +1 -0
  76. package/build/__tests__/unit/fiberRootsDrift.test.js +23 -0
  77. package/build/__tests__/unit/fiberRootsDrift.test.js.map +1 -0
  78. package/build/__tests__/unit/hostAddress.test.d.ts +2 -0
  79. package/build/__tests__/unit/hostAddress.test.d.ts.map +1 -0
  80. package/build/__tests__/unit/hostAddress.test.js +38 -0
  81. package/build/__tests__/unit/hostAddress.test.js.map +1 -0
  82. package/build/__tests__/unit/measureComponent.test.js +1 -1
  83. package/build/__tests__/unit/measureComponent.test.js.map +1 -1
  84. package/build/__tests__/unit/networkInterceptorFetchMock.test.d.ts +2 -0
  85. package/build/__tests__/unit/networkInterceptorFetchMock.test.d.ts.map +1 -0
  86. package/build/__tests__/unit/networkInterceptorFetchMock.test.js +90 -0
  87. package/build/__tests__/unit/networkInterceptorFetchMock.test.js.map +1 -0
  88. package/build/__tests__/unit/networkMockTool.test.js +12 -1
  89. package/build/__tests__/unit/networkMockTool.test.js.map +1 -1
  90. package/build/__tests__/unit/pressKeyMobile.test.d.ts +2 -0
  91. package/build/__tests__/unit/pressKeyMobile.test.d.ts.map +1 -0
  92. package/build/__tests__/unit/pressKeyMobile.test.js +63 -0
  93. package/build/__tests__/unit/pressKeyMobile.test.js.map +1 -0
  94. package/build/__tests__/unit/reloadAppAutoConnect.test.js +4 -1
  95. package/build/__tests__/unit/reloadAppAutoConnect.test.js.map +1 -1
  96. package/build/__tests__/unit/tapAndroidSerial.test.js +16 -0
  97. package/build/__tests__/unit/tapAndroidSerial.test.js.map +1 -1
  98. package/build/core/android.d.ts +10 -0
  99. package/build/core/android.d.ts.map +1 -1
  100. package/build/core/android.js +17 -0
  101. package/build/core/android.js.map +1 -1
  102. package/build/core/appContext.d.ts.map +1 -1
  103. package/build/core/appContext.js +11 -13
  104. package/build/core/appContext.js.map +1 -1
  105. package/build/core/appDetection.d.ts.map +1 -1
  106. package/build/core/appDetection.js +7 -4
  107. package/build/core/appDetection.js.map +1 -1
  108. package/build/core/cdpCommand.d.ts +4 -0
  109. package/build/core/cdpCommand.d.ts.map +1 -0
  110. package/build/core/cdpCommand.js +58 -0
  111. package/build/core/cdpCommand.js.map +1 -0
  112. package/build/core/chromium.d.ts +157 -0
  113. package/build/core/chromium.d.ts.map +1 -0
  114. package/build/core/chromium.js +480 -0
  115. package/build/core/chromium.js.map +1 -0
  116. package/build/core/chromiumCapabilities.d.ts +11 -0
  117. package/build/core/chromiumCapabilities.d.ts.map +1 -0
  118. package/build/core/chromiumCapabilities.js +35 -0
  119. package/build/core/chromiumCapabilities.js.map +1 -0
  120. package/build/core/chromiumDialogAnswer.d.ts +11 -0
  121. package/build/core/chromiumDialogAnswer.d.ts.map +1 -0
  122. package/build/core/chromiumDialogAnswer.js +61 -0
  123. package/build/core/chromiumDialogAnswer.js.map +1 -0
  124. package/build/core/chromiumDialogs.d.ts +46 -0
  125. package/build/core/chromiumDialogs.d.ts.map +1 -0
  126. package/build/core/chromiumDialogs.js +88 -0
  127. package/build/core/chromiumDialogs.js.map +1 -0
  128. package/build/core/chromiumScreen.d.ts +97 -0
  129. package/build/core/chromiumScreen.d.ts.map +1 -0
  130. package/build/core/chromiumScreen.js +498 -0
  131. package/build/core/chromiumScreen.js.map +1 -0
  132. package/build/core/componentSearch.d.ts.map +1 -1
  133. package/build/core/componentSearch.js +7 -26
  134. package/build/core/componentSearch.js.map +1 -1
  135. package/build/core/componentSource.d.ts.map +1 -1
  136. package/build/core/componentSource.js +3 -11
  137. package/build/core/componentSource.js.map +1 -1
  138. package/build/core/componentTree.d.ts.map +1 -1
  139. package/build/core/componentTree.js +4 -18
  140. package/build/core/componentTree.js.map +1 -1
  141. package/build/core/connection.d.ts +13 -0
  142. package/build/core/connection.d.ts.map +1 -1
  143. package/build/core/connection.js +99 -18
  144. package/build/core/connection.js.map +1 -1
  145. package/build/core/connectionState.d.ts +2 -1
  146. package/build/core/connectionState.d.ts.map +1 -1
  147. package/build/core/connectionState.js +3 -0
  148. package/build/core/connectionState.js.map +1 -1
  149. package/build/core/deviceResolver.d.ts.map +1 -1
  150. package/build/core/deviceResolver.js +8 -5
  151. package/build/core/deviceResolver.js.map +1 -1
  152. package/build/core/electronLaunch.d.ts +25 -0
  153. package/build/core/electronLaunch.d.ts.map +1 -0
  154. package/build/core/electronLaunch.js +113 -0
  155. package/build/core/electronLaunch.js.map +1 -0
  156. package/build/core/errors.d.ts +1 -1
  157. package/build/core/errors.d.ts.map +1 -1
  158. package/build/core/errors.js.map +1 -1
  159. package/build/core/guides.d.ts.map +1 -1
  160. package/build/core/guides.js +9 -6
  161. package/build/core/guides.js.map +1 -1
  162. package/build/core/hostAddress.d.ts +6 -0
  163. package/build/core/hostAddress.d.ts.map +1 -0
  164. package/build/core/hostAddress.js +32 -0
  165. package/build/core/hostAddress.js.map +1 -0
  166. package/build/core/injected/fiberRoots.d.ts +2 -0
  167. package/build/core/injected/fiberRoots.d.ts.map +1 -0
  168. package/build/core/injected/fiberRoots.js +60 -0
  169. package/build/core/injected/fiberRoots.js.map +1 -0
  170. package/build/core/inputTarget.d.ts.map +1 -1
  171. package/build/core/inputTarget.js +4 -9
  172. package/build/core/inputTarget.js.map +1 -1
  173. package/build/core/inspector.d.ts.map +1 -1
  174. package/build/core/inspector.js +6 -22
  175. package/build/core/inspector.js.map +1 -1
  176. package/build/core/ios.d.ts +12 -0
  177. package/build/core/ios.d.ts.map +1 -1
  178. package/build/core/ios.js +32 -0
  179. package/build/core/ios.js.map +1 -1
  180. package/build/core/jsExecute.d.ts.map +1 -1
  181. package/build/core/jsExecute.js +20 -1
  182. package/build/core/jsExecute.js.map +1 -1
  183. package/build/core/measureComponent.d.ts.map +1 -1
  184. package/build/core/measureComponent.js +4 -8
  185. package/build/core/measureComponent.js.map +1 -1
  186. package/build/core/metro.d.ts +20 -1
  187. package/build/core/metro.d.ts.map +1 -1
  188. package/build/core/metro.js +146 -9
  189. package/build/core/metro.js.map +1 -1
  190. package/build/core/mockRules.d.ts +1 -0
  191. package/build/core/mockRules.d.ts.map +1 -1
  192. package/build/core/mockRules.js +8 -0
  193. package/build/core/mockRules.js.map +1 -1
  194. package/build/core/nativeLogs.js +1 -1
  195. package/build/core/nativeLogs.js.map +1 -1
  196. package/build/core/navigation.d.ts.map +1 -1
  197. package/build/core/navigation.js +4 -18
  198. package/build/core/navigation.js.map +1 -1
  199. package/build/core/networkInterceptor.d.ts +2 -2
  200. package/build/core/networkInterceptor.d.ts.map +1 -1
  201. package/build/core/networkInterceptor.js +90 -3
  202. package/build/core/networkInterceptor.js.map +1 -1
  203. package/build/core/pressables.d.ts.map +1 -1
  204. package/build/core/pressables.js +6 -13
  205. package/build/core/pressables.js.map +1 -1
  206. package/build/core/redux.d.ts.map +1 -1
  207. package/build/core/redux.js +6 -10
  208. package/build/core/redux.js.map +1 -1
  209. package/build/core/register.d.ts.map +1 -1
  210. package/build/core/register.js +5 -2
  211. package/build/core/register.js.map +1 -1
  212. package/build/core/rnGlobalsBootstrap.d.ts.map +1 -1
  213. package/build/core/rnGlobalsBootstrap.js +3 -1
  214. package/build/core/rnGlobalsBootstrap.js.map +1 -1
  215. package/build/core/screenLayout.d.ts.map +1 -1
  216. package/build/core/screenLayout.js +4 -13
  217. package/build/core/screenLayout.js.map +1 -1
  218. package/build/core/screenStaleness.d.ts.map +1 -1
  219. package/build/core/screenStaleness.js +2 -0
  220. package/build/core/screenStaleness.js.map +1 -1
  221. package/build/core/screenState.d.ts.map +1 -1
  222. package/build/core/screenState.js +4 -11
  223. package/build/core/screenState.js.map +1 -1
  224. package/build/core/swipeDiagnosis.d.ts.map +1 -1
  225. package/build/core/swipeDiagnosis.js +3 -10
  226. package/build/core/swipeDiagnosis.js.map +1 -1
  227. package/build/core/textEntry.d.ts +1 -1
  228. package/build/core/textEntry.d.ts.map +1 -1
  229. package/build/core/types.d.ts +4 -2
  230. package/build/core/types.d.ts.map +1 -1
  231. package/build/pro/chromiumKeys.d.ts +19 -0
  232. package/build/pro/chromiumKeys.d.ts.map +1 -0
  233. package/build/pro/chromiumKeys.js +68 -0
  234. package/build/pro/chromiumKeys.js.map +1 -0
  235. package/build/pro/chromiumSwipe.d.ts +35 -0
  236. package/build/pro/chromiumSwipe.d.ts.map +1 -0
  237. package/build/pro/chromiumSwipe.js +105 -0
  238. package/build/pro/chromiumSwipe.js.map +1 -0
  239. package/build/pro/chromiumTap.d.ts +12 -0
  240. package/build/pro/chromiumTap.d.ts.map +1 -0
  241. package/build/pro/chromiumTap.js +155 -0
  242. package/build/pro/chromiumTap.js.map +1 -0
  243. package/build/pro/tap.d.ts +1 -0
  244. package/build/pro/tap.d.ts.map +1 -1
  245. package/build/pro/tap.js +7 -1
  246. package/build/pro/tap.js.map +1 -1
  247. package/build/tools/componentTools.d.ts.map +1 -1
  248. package/build/tools/componentTools.js +31 -3
  249. package/build/tools/componentTools.js.map +1 -1
  250. package/build/tools/connectionTools.d.ts.map +1 -1
  251. package/build/tools/connectionTools.js +66 -28
  252. package/build/tools/connectionTools.js.map +1 -1
  253. package/build/tools/deviceTools.d.ts.map +1 -1
  254. package/build/tools/deviceTools.js +42 -1
  255. package/build/tools/deviceTools.js.map +1 -1
  256. package/build/tools/interactionTools.d.ts.map +1 -1
  257. package/build/tools/interactionTools.js +132 -7
  258. package/build/tools/interactionTools.js.map +1 -1
  259. package/build/tools/networkTools.d.ts.map +1 -1
  260. package/build/tools/networkTools.js +20 -5
  261. package/build/tools/networkTools.js.map +1 -1
  262. package/build/tools/screenshotTools.d.ts.map +1 -1
  263. package/build/tools/screenshotTools.js +452 -373
  264. package/build/tools/screenshotTools.js.map +1 -1
  265. package/package.json +1 -1
  266. package/tools.json +27 -1
@@ -5,6 +5,11 @@ import { recordScreenMetrics } from "../core/projectMemory.js";
5
5
  import { iosScreenshot, androidScreenshot, detectLogBox, formatLogBoxWarning, getScreenState, formatScreenStateSummary, imageBuffer, getActiveOrBootedSimulatorUdid, connectedApps, getConnectedAppBySimulatorUdid, getConnectedAppByAndroidDeviceId, iosDescribeAll, detectIOSSystemOverlay, formatIOSSystemOverlayWarning, androidGetStatusBarHeight, androidGetDensity, } from "../core/index.js";
6
6
  import { screenStateToScreenSpace } from "../core/screenSpace.js";
7
7
  import { resolvePhysicalIosDevice, physicalIosScreenshot } from "../core/iosPhysical.js";
8
+ import { writeFileSync } from "node:fs";
9
+ import { chromiumAppFor } from "../core/connection.js";
10
+ import { chromiumScreenState } from "../core/chromiumScreen.js";
11
+ import { resolveDeviceTarget, formatResolverError } from "../core/deviceResolver.js";
12
+ import { chromiumCapture } from "../core/chromium.js";
8
13
  async function capturePhysicalDevice(phys, outputPath) {
9
14
  const result = await physicalIosScreenshot(phys.udid, outputPath);
10
15
  if (!result.success || !result.data) {
@@ -43,243 +48,461 @@ async function capturePhysicalDevice(phys, outputPath) {
43
48
  ]
44
49
  };
45
50
  }
46
- export function registerScreenshotTools(server) {
47
- registerToolWithTelemetry(server, "ios_screenshot", {
48
- description: "Take a screenshot from an iOS simulator. Returns the image plus a screen-state summary: active route (name + navigation stack), overlay-grouped tappable elements (pressables behind an open sheet/modal are excluded), component names as JSX tags, labels, testIDs, and frames — all in ready-to-tap pixel coordinates. Prefer tap(text=\"...\") when text is exact and unique; otherwise use tap(x, y) with coordinates from the list — this is the most reliable way to tap icons or visually-identified elements. Use component names for inspect_component/find_components.\n" +
49
- "PURPOSE: Snapshot what the user sees on iOS AND get tap-ready pressables + a structured component map in one call.\n" +
50
- "WHEN TO USE: Any visual verification, before/after comparison, or as the starting point for tapping UI by coordinates.\n" +
51
- "WORKFLOW: ios_screenshot -> pick element from pressables -> tap(x, y) or tap(testID=...) -> ios_screenshot to verify.\n" +
52
- "PHYSICAL DEVICES: a USB-attached iPhone/iPad works too (pass its UDID or name from list_devices) — capture only, via pymobiledevice3. No pressables, no tap/swipe/input_text: iOS has no touch injection below 17.\n" +
53
- "LIMITATIONS: Otherwise requires a booted iOS simulator (simctl). For system dialogs without RN, combine with tap(..., native=true).\n" +
54
- "GOOD: ios_screenshot()\n" +
55
- "BAD: ios_screenshot({ udid: \"guess\" }) with a made-up UDID — run list_devices first.\n" +
56
- "SOURCE: to jump from a pixel to the code that renders it, call inspect_at_point(x, y) — it returns the absolute file and line.\n",
57
- inputSchema: {
58
- outputPath: z
59
- .string()
60
- .optional()
61
- .describe("Optional path to save the screenshot. If not provided, saves to temp directory."),
62
- udid: z
63
- .string()
64
- .optional()
65
- .describe(IOS_ARG_DESC),
66
- device: z
67
- .string()
68
- .optional()
69
- .describe("Alias for `udid` — same accepted values. Provided for consistency with tap/get_screen_layout/get_screen_state, which all use `device`. If both are given, `udid` wins.")
70
- }
71
- }, async ({ outputPath, udid, device }) => {
72
- const resolved = await resolveIosUdid(udid ?? device);
73
- if (!resolved.ok) {
74
- const hint = udid ?? device;
75
- const phys = hint ? await resolvePhysicalIosDevice(hint) : null;
76
- if (phys)
77
- return await capturePhysicalDevice(phys, outputPath);
78
- return resolved.response;
79
- }
80
- const targetUdid = resolved.udid ?? (await getActiveOrBootedSimulatorUdid());
81
- const capturePromise = iosScreenshot(outputPath, targetUdid ?? undefined);
82
- const describePromise = iosDescribeAll(targetUdid ?? undefined).catch(() => null);
83
- const earlyTargetApp = targetUdid ? getConnectedAppBySimulatorUdid(targetUdid) : null;
84
- const screenStatePromise = earlyTargetApp
85
- ? getScreenState({ device: earlyTargetApp.deviceInfo.deviceName }).catch(() => null)
86
- : Promise.resolve(null);
87
- const result = await capturePromise;
88
- if (!result.success) {
89
- return {
90
- content: [
91
- {
92
- type: "text",
93
- text: `Error: ${result.error}`
94
- }
95
- ],
96
- isError: true
51
+ async function iosScreenshotHandler({ outputPath, udid, device }) {
52
+ const resolved = await resolveIosUdid(udid ?? device);
53
+ if (!resolved.ok) {
54
+ const hint = udid ?? device;
55
+ const phys = hint ? await resolvePhysicalIosDevice(hint) : null;
56
+ if (phys)
57
+ return await capturePhysicalDevice(phys, outputPath);
58
+ return resolved.response;
59
+ }
60
+ const targetUdid = resolved.udid ?? (await getActiveOrBootedSimulatorUdid());
61
+ const capturePromise = iosScreenshot(outputPath, targetUdid ?? undefined);
62
+ const describePromise = iosDescribeAll(targetUdid ?? undefined).catch(() => null);
63
+ const earlyTargetApp = targetUdid ? getConnectedAppBySimulatorUdid(targetUdid) : null;
64
+ const screenStatePromise = earlyTargetApp
65
+ ? getScreenState({ device: earlyTargetApp.deviceInfo.deviceName }).catch(() => null)
66
+ : Promise.resolve(null);
67
+ const result = await capturePromise;
68
+ if (!result.success) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: `Error: ${result.error}`
74
+ }
75
+ ],
76
+ isError: true
77
+ };
78
+ }
79
+ if (result.data) {
80
+ const pixelWidth = result.originalWidth || 0;
81
+ const pixelHeight = result.originalHeight || 0;
82
+ const resolvedUdid = targetUdid;
83
+ const targetApp = resolvedUdid ? getConnectedAppBySimulatorUdid(resolvedUdid) : null;
84
+ const targetDeviceName = targetApp?.deviceInfo.deviceName;
85
+ if (targetApp) {
86
+ targetApp.lastScreenshot = {
87
+ originalWidth: pixelWidth,
88
+ originalHeight: pixelHeight,
89
+ scaleFactor: result.scaleFactor || 1,
97
90
  };
98
91
  }
99
- if (result.data) {
100
- const pixelWidth = result.originalWidth || 0;
101
- const pixelHeight = result.originalHeight || 0;
102
- const resolvedUdid = targetUdid;
103
- const targetApp = resolvedUdid ? getConnectedAppBySimulatorUdid(resolvedUdid) : null;
104
- const targetDeviceName = targetApp?.deviceInfo.deviceName;
105
- if (targetApp) {
106
- targetApp.lastScreenshot = {
107
- originalWidth: pixelWidth,
108
- originalHeight: pixelHeight,
109
- scaleFactor: result.scaleFactor || 1,
110
- };
111
- }
112
- let pointWidth = 0;
113
- let pointHeight = 0;
114
- let scaleFactor = 3;
115
- let safeAreaTop = 59;
116
- try {
117
- const describeResult = await describePromise;
118
- if (describeResult && describeResult.success && describeResult.elements && describeResult.elements.length > 0) {
119
- const rootElement = describeResult.elements[0];
120
- if (rootElement.frame) {
121
- pointWidth = Math.round(rootElement.frame.width);
122
- pointHeight = Math.round(rootElement.frame.height);
123
- if (rootElement.frame.y > 0) {
124
- safeAreaTop = Math.round(rootElement.frame.y);
125
- }
92
+ let pointWidth = 0;
93
+ let pointHeight = 0;
94
+ let scaleFactor = 3;
95
+ let safeAreaTop = 59;
96
+ try {
97
+ const describeResult = await describePromise;
98
+ if (describeResult && describeResult.success && describeResult.elements && describeResult.elements.length > 0) {
99
+ const rootElement = describeResult.elements[0];
100
+ if (rootElement.frame) {
101
+ pointWidth = Math.round(rootElement.frame.width);
102
+ pointHeight = Math.round(rootElement.frame.height);
103
+ if (rootElement.frame.y > 0) {
104
+ safeAreaTop = Math.round(rootElement.frame.y);
126
105
  }
127
- else if (rootElement.AXFrame) {
128
- const match = rootElement.AXFrame.match(/\{\{([\d.]+),\s*([\d.]+)\},\s*\{([\d.]+),\s*([\d.]+)\}\}/);
129
- if (match) {
130
- const frameY = parseFloat(match[2]);
131
- pointWidth = Math.round(parseFloat(match[3]));
132
- pointHeight = Math.round(parseFloat(match[4]));
133
- if (frameY > 0) {
134
- safeAreaTop = Math.round(frameY);
135
- }
106
+ }
107
+ else if (rootElement.AXFrame) {
108
+ const match = rootElement.AXFrame.match(/\{\{([\d.]+),\s*([\d.]+)\},\s*\{([\d.]+),\s*([\d.]+)\}\}/);
109
+ if (match) {
110
+ const frameY = parseFloat(match[2]);
111
+ pointWidth = Math.round(parseFloat(match[3]));
112
+ pointHeight = Math.round(parseFloat(match[4]));
113
+ if (frameY > 0) {
114
+ safeAreaTop = Math.round(frameY);
136
115
  }
137
116
  }
138
- if (pointWidth > 0) {
139
- scaleFactor = Math.round(pixelWidth / pointWidth);
140
- }
117
+ }
118
+ if (pointWidth > 0) {
119
+ scaleFactor = Math.round(pixelWidth / pointWidth);
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ }
125
+ if (pointWidth === 0) {
126
+ pointWidth = Math.round(pixelWidth / scaleFactor);
127
+ pointHeight = Math.round(pixelHeight / scaleFactor);
128
+ }
129
+ const safeAreaOffsetPixels = safeAreaTop * scaleFactor;
130
+ let pressablesText = null;
131
+ let pressablesIsScreenState = false;
132
+ if (targetApp) {
133
+ try {
134
+ const ssResult = await screenStatePromise;
135
+ if (ssResult && ssResult.success && ssResult.screenState) {
136
+ const screenshotScale = result.scaleFactor || 1;
137
+ const toPx = (v) => Math.round((v * scaleFactor) / screenshotScale);
138
+ const screenSpaceSs = screenStateToScreenSpace(ssResult.screenState, {
139
+ platform: "ios",
140
+ topInset: safeAreaTop
141
+ });
142
+ pressablesText = formatScreenStateSummary(screenSpaceSs, (p) => ({
143
+ center: { x: toPx(p.center.x), y: toPx(p.center.y) },
144
+ frame: {
145
+ x: toPx(p.bounds.x),
146
+ y: toPx(p.bounds.y),
147
+ width: toPx(p.bounds.width),
148
+ height: toPx(p.bounds.height),
149
+ },
150
+ }));
151
+ pressablesIsScreenState = true;
141
152
  }
142
153
  }
143
154
  catch {
144
155
  }
145
- if (pointWidth === 0) {
146
- pointWidth = Math.round(pixelWidth / scaleFactor);
147
- pointHeight = Math.round(pixelHeight / scaleFactor);
156
+ }
157
+ const deliveredWidth = result.scaleFactor && result.scaleFactor > 1
158
+ ? Math.round(pixelWidth / result.scaleFactor)
159
+ : pixelWidth;
160
+ const deliveredHeight = result.scaleFactor && result.scaleFactor > 1
161
+ ? Math.round(pixelHeight / result.scaleFactor)
162
+ : pixelHeight;
163
+ let infoText;
164
+ if (result.scaleFactor && result.scaleFactor > 1) {
165
+ infoText = `Screenshot: raw ${pixelWidth}x${pixelHeight} px → delivered ${deliveredWidth}x${deliveredHeight} px (downscaled ${(1 / result.scaleFactor).toFixed(3)}Ɨ to fit API limits). Pressable coordinates below are in delivered-image pixels.`;
166
+ }
167
+ else {
168
+ infoText = `Screenshot captured (${pixelWidth}x${pixelHeight} pixels)`;
169
+ }
170
+ if (resolved.udid) {
171
+ recordScreenMetrics(resolved.udid, {
172
+ rawWidth: pixelWidth,
173
+ rawHeight: pixelHeight,
174
+ deliveredWidth,
175
+ deliveredHeight,
176
+ downscale: result.scaleFactor && result.scaleFactor > 1 ? 1 / result.scaleFactor : 1,
177
+ pointWidth,
178
+ pointHeight,
179
+ scale: scaleFactor,
180
+ capturedAt: Date.now(),
181
+ });
182
+ }
183
+ if (targetUdid) {
184
+ infoText += `\nšŸ“ø Captured from: ${targetDeviceName ? `${targetDeviceName} ` : ""}(${targetUdid})`;
185
+ }
186
+ infoText += `\nšŸ“± iOS screen: ${pointWidth}x${pointHeight} points (${scaleFactor}x scale)`;
187
+ infoText += `\nšŸ“ tap() handles pixel-to-point conversion automatically — pass pixel coords from this image directly`;
188
+ infoText += `\nāš ļø Status bar + safe area: ${safeAreaTop} points (${safeAreaOffsetPixels} pixels) from top`;
189
+ if (pressablesText) {
190
+ infoText += pressablesIsScreenState
191
+ ? `\n\n🧭 Screen state (route + tappable elements, coordinates in screenshot pixels):\n`
192
+ : `\n\nšŸŽÆ Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
193
+ infoText += `\n${pressablesText}`;
194
+ infoText += `\n\nšŸ’” Next steps:`;
195
+ infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
196
+ infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
197
+ infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
198
+ infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
199
+ }
200
+ else {
201
+ if (!targetApp && connectedApps.size > 0) {
202
+ infoText += `\n\nā„¹ļø Pressable enrichment skipped: no RN app is connected to simulator ${resolvedUdid}.`;
203
+ infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
148
204
  }
149
- const safeAreaOffsetPixels = safeAreaTop * scaleFactor;
150
- let pressablesText = null;
151
- let pressablesIsScreenState = false;
152
- if (targetApp) {
153
- try {
154
- const ssResult = await screenStatePromise;
155
- if (ssResult && ssResult.success && ssResult.screenState) {
156
- const screenshotScale = result.scaleFactor || 1;
157
- const toPx = (v) => Math.round((v * scaleFactor) / screenshotScale);
158
- const screenSpaceSs = screenStateToScreenSpace(ssResult.screenState, {
159
- platform: "ios",
160
- topInset: safeAreaTop
161
- });
162
- pressablesText = formatScreenStateSummary(screenSpaceSs, (p) => ({
163
- center: { x: toPx(p.center.x), y: toPx(p.center.y) },
164
- frame: {
165
- x: toPx(p.bounds.x),
166
- y: toPx(p.bounds.y),
167
- width: toPx(p.bounds.width),
168
- height: toPx(p.bounds.height),
169
- },
170
- }));
171
- pressablesIsScreenState = true;
172
- }
173
- }
174
- catch {
205
+ infoText += `\n\nšŸ’” Next steps:`;
206
+ infoText += `\n • tap(text="Button Label") — tap element by visible text`;
207
+ infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
208
+ infoText += `\n • get_screen_layout — get full UI tree with real on-screen positions`;
209
+ }
210
+ if (targetApp) {
211
+ try {
212
+ const logBoxState = await detectLogBox(targetDeviceName);
213
+ if (logBoxState && logBoxState.total > 0) {
214
+ infoText += formatLogBoxWarning(logBoxState);
175
215
  }
176
216
  }
177
- const deliveredWidth = result.scaleFactor && result.scaleFactor > 1
178
- ? Math.round(pixelWidth / result.scaleFactor)
179
- : pixelWidth;
180
- const deliveredHeight = result.scaleFactor && result.scaleFactor > 1
181
- ? Math.round(pixelHeight / result.scaleFactor)
182
- : pixelHeight;
183
- let infoText;
184
- if (result.scaleFactor && result.scaleFactor > 1) {
185
- infoText = `Screenshot: raw ${pixelWidth}x${pixelHeight} px → delivered ${deliveredWidth}x${deliveredHeight} px (downscaled ${(1 / result.scaleFactor).toFixed(3)}Ɨ to fit API limits). Pressable coordinates below are in delivered-image pixels.`;
217
+ catch {
186
218
  }
187
- else {
188
- infoText = `Screenshot captured (${pixelWidth}x${pixelHeight} pixels)`;
219
+ }
220
+ try {
221
+ const overlay = await detectIOSSystemOverlay(resolvedUdid ?? undefined);
222
+ if (overlay) {
223
+ infoText += formatIOSSystemOverlayWarning(overlay);
189
224
  }
190
- if (resolved.udid) {
191
- recordScreenMetrics(resolved.udid, {
192
- rawWidth: pixelWidth,
193
- rawHeight: pixelHeight,
194
- deliveredWidth,
195
- deliveredHeight,
196
- downscale: result.scaleFactor && result.scaleFactor > 1 ? 1 / result.scaleFactor : 1,
197
- pointWidth,
198
- pointHeight,
199
- scale: scaleFactor,
200
- capturedAt: Date.now(),
201
- });
225
+ }
226
+ catch {
227
+ }
228
+ imageBuffer.add({
229
+ id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
230
+ image: result.data,
231
+ timestamp: Date.now(),
232
+ source: "ios_screenshot",
233
+ metadata: {
234
+ width: result.originalWidth || 0,
235
+ height: result.originalHeight || 0,
236
+ scaleFactor: result.scaleFactor || 1,
237
+ platform: "ios",
238
+ },
239
+ });
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: infoText
245
+ },
246
+ {
247
+ type: "image",
248
+ data: result.data.toString("base64"),
249
+ mimeType: "image/jpeg"
250
+ }
251
+ ]
252
+ };
253
+ }
254
+ return {
255
+ content: [
256
+ {
257
+ type: "text",
258
+ text: `Screenshot saved to: ${result.result}`
202
259
  }
203
- if (targetUdid) {
204
- infoText += `\nšŸ“ø Captured from: ${targetDeviceName ? `${targetDeviceName} ` : ""}(${targetUdid})`;
260
+ ]
261
+ };
262
+ }
263
+ async function androidScreenshotHandler({ outputPath, deviceId: deviceIdArg, device }) {
264
+ const deviceId = deviceIdArg ?? device;
265
+ const resolved = await resolveAndroidDeviceId(deviceId);
266
+ if (!resolved.ok)
267
+ return resolved.response;
268
+ const androidTargetApp = getConnectedAppByAndroidDeviceId(resolved.serial);
269
+ const capturePromise = androidScreenshot(outputPath, resolved.serial);
270
+ const statusBarPromise = androidGetStatusBarHeight(resolved.serial).catch(() => null);
271
+ const densityPromise = androidGetDensity(resolved.serial).catch(() => null);
272
+ const screenStatePromise = androidTargetApp
273
+ ? getScreenState({ device: androidTargetApp.deviceInfo.deviceName }).catch(() => null)
274
+ : Promise.resolve(null);
275
+ const result = await capturePromise;
276
+ if (!result.success) {
277
+ return {
278
+ content: [
279
+ {
280
+ type: "text",
281
+ text: `Error: ${result.error}`
282
+ }
283
+ ],
284
+ isError: true
285
+ };
286
+ }
287
+ if (result.data) {
288
+ const pixelWidth = result.originalWidth || 0;
289
+ const pixelHeight = result.originalHeight || 0;
290
+ const targetApp = getConnectedAppByAndroidDeviceId(resolved.serial);
291
+ const targetDeviceName = targetApp?.deviceInfo.deviceName;
292
+ if (targetApp) {
293
+ targetApp.lastScreenshot = {
294
+ originalWidth: pixelWidth,
295
+ originalHeight: pixelHeight,
296
+ scaleFactor: result.scaleFactor || 1,
297
+ };
298
+ }
299
+ const androidDeliveredW = result.scaleFactor && result.scaleFactor > 1
300
+ ? Math.round(pixelWidth / result.scaleFactor)
301
+ : pixelWidth;
302
+ const androidDeliveredH = result.scaleFactor && result.scaleFactor > 1
303
+ ? Math.round(pixelHeight / result.scaleFactor)
304
+ : pixelHeight;
305
+ let infoText = result.scaleFactor && result.scaleFactor > 1
306
+ ? `Screenshot: raw ${pixelWidth}x${pixelHeight} px → delivered ${androidDeliveredW}x${androidDeliveredH} px (downscaled ${(1 / result.scaleFactor).toFixed(3)}Ɨ to fit API limits). Pressable coordinates below are in delivered-image pixels.`
307
+ : `Screenshot captured (${pixelWidth}x${pixelHeight} pixels)`;
308
+ if (resolved.serial) {
309
+ recordScreenMetrics(resolved.serial, {
310
+ rawWidth: pixelWidth,
311
+ rawHeight: pixelHeight,
312
+ deliveredWidth: androidDeliveredW,
313
+ deliveredHeight: androidDeliveredH,
314
+ downscale: result.scaleFactor && result.scaleFactor > 1 ? 1 / result.scaleFactor : 1,
315
+ capturedAt: Date.now(),
316
+ });
317
+ }
318
+ let statusBarPixels = 63;
319
+ let statusBarDp = 24;
320
+ let densityDpi = 440;
321
+ try {
322
+ const statusBarResult = await statusBarPromise;
323
+ if (statusBarResult && statusBarResult.success && statusBarResult.heightPixels) {
324
+ statusBarPixels = statusBarResult.heightPixels;
325
+ statusBarDp = statusBarResult.heightDp || 24;
205
326
  }
206
- infoText += `\nšŸ“± iOS screen: ${pointWidth}x${pointHeight} points (${scaleFactor}x scale)`;
207
- infoText += `\nšŸ“ tap() handles pixel-to-point conversion automatically — pass pixel coords from this image directly`;
208
- infoText += `\nāš ļø Status bar + safe area: ${safeAreaTop} points (${safeAreaOffsetPixels} pixels) from top`;
209
- if (pressablesText) {
210
- infoText += pressablesIsScreenState
211
- ? `\n\n🧭 Screen state (route + tappable elements, coordinates in screenshot pixels):\n`
212
- : `\n\nšŸŽÆ Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
213
- infoText += `\n${pressablesText}`;
214
- infoText += `\n\nšŸ’” Next steps:`;
215
- infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
216
- infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
217
- infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
218
- infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
327
+ const densityResult = await densityPromise;
328
+ if (densityResult && densityResult.success && densityResult.density) {
329
+ densityDpi = densityResult.density;
219
330
  }
220
- else {
221
- if (!targetApp && connectedApps.size > 0) {
222
- infoText += `\n\nā„¹ļø Pressable enrichment skipped: no RN app is connected to simulator ${resolvedUdid}.`;
223
- infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
331
+ }
332
+ catch {
333
+ }
334
+ let pressablesText = null;
335
+ let pressablesIsScreenState = false;
336
+ if (targetApp) {
337
+ try {
338
+ const ssResult = await screenStatePromise;
339
+ if (ssResult && ssResult.success && ssResult.screenState) {
340
+ const screenshotScale = result.scaleFactor || 1;
341
+ const densityScale = densityDpi / 160;
342
+ const toPx = (v) => Math.round((v * densityScale) / screenshotScale);
343
+ const screenSpaceSs = screenStateToScreenSpace(ssResult.screenState, {
344
+ platform: "android",
345
+ topInset: statusBarDp
346
+ });
347
+ pressablesText = formatScreenStateSummary(screenSpaceSs, (p) => ({
348
+ center: { x: toPx(p.center.x), y: toPx(p.center.y) },
349
+ frame: {
350
+ x: toPx(p.bounds.x),
351
+ y: toPx(p.bounds.y),
352
+ width: toPx(p.bounds.width),
353
+ height: toPx(p.bounds.height),
354
+ },
355
+ }));
356
+ pressablesIsScreenState = true;
224
357
  }
225
- infoText += `\n\nšŸ’” Next steps:`;
226
- infoText += `\n • tap(text="Button Label") — tap element by visible text`;
227
- infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
228
- infoText += `\n • get_screen_layout — get full UI tree with real on-screen positions`;
229
358
  }
230
- if (targetApp) {
231
- try {
232
- const logBoxState = await detectLogBox(targetDeviceName);
233
- if (logBoxState && logBoxState.total > 0) {
234
- infoText += formatLogBoxWarning(logBoxState);
235
- }
236
- }
237
- catch {
238
- }
359
+ catch {
239
360
  }
361
+ }
362
+ infoText += `\nšŸ“± Android uses PIXELS for all coordinates`;
363
+ if (result.scaleFactor && result.scaleFactor > 1) {
364
+ infoText += `\nšŸ“ tap() handles coordinate conversion automatically — pass pixel coords from this image directly`;
365
+ }
366
+ else {
367
+ infoText += `\nšŸ“ Screenshot coords = tap coords (no conversion needed)`;
368
+ }
369
+ infoText += `\nāš ļø Status bar: ${statusBarPixels}px (${statusBarDp}dp) from top - app content starts below this`;
370
+ infoText += `\nšŸ“Š Display density: ${densityDpi}dpi`;
371
+ if (pressablesText) {
372
+ infoText += pressablesIsScreenState
373
+ ? `\n\n🧭 Screen state (route + tappable elements, coordinates in screenshot pixels):\n`
374
+ : `\n\nšŸŽÆ Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
375
+ infoText += `\n${pressablesText}`;
376
+ infoText += `\n\nšŸ’” Next steps:`;
377
+ infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
378
+ infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
379
+ infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
380
+ infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
381
+ }
382
+ else {
383
+ if (!targetApp && connectedApps.size > 0) {
384
+ infoText += `\n\nā„¹ļø Pressable enrichment skipped: no RN app is connected to device ${deviceId ?? "(default)"}.`;
385
+ infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
386
+ }
387
+ infoText += `\n\nšŸ’” Next steps:`;
388
+ infoText += `\n • tap(text="Button Label") — tap element by visible text`;
389
+ infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
390
+ infoText += `\n • get_screen_layout — get full UI tree with real on-screen positions`;
391
+ }
392
+ if (targetApp) {
240
393
  try {
241
- const overlay = await detectIOSSystemOverlay(resolvedUdid ?? undefined);
242
- if (overlay) {
243
- infoText += formatIOSSystemOverlayWarning(overlay);
394
+ const logBoxState = await detectLogBox(targetDeviceName);
395
+ if (logBoxState && logBoxState.total > 0) {
396
+ infoText += formatLogBoxWarning(logBoxState);
244
397
  }
245
398
  }
246
399
  catch {
247
400
  }
248
- imageBuffer.add({
249
- id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
250
- image: result.data,
251
- timestamp: Date.now(),
252
- source: "ios_screenshot",
253
- metadata: {
254
- width: result.originalWidth || 0,
255
- height: result.originalHeight || 0,
256
- scaleFactor: result.scaleFactor || 1,
257
- platform: "ios",
258
- },
259
- });
260
- return {
261
- content: [
262
- {
263
- type: "text",
264
- text: infoText
265
- },
266
- {
267
- type: "image",
268
- data: result.data.toString("base64"),
269
- mimeType: "image/jpeg"
270
- }
271
- ]
272
- };
273
401
  }
402
+ imageBuffer.add({
403
+ id: `android-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
404
+ image: result.data,
405
+ timestamp: Date.now(),
406
+ source: "android_screenshot",
407
+ metadata: {
408
+ width: result.originalWidth || 0,
409
+ height: result.originalHeight || 0,
410
+ scaleFactor: result.scaleFactor || 1,
411
+ platform: "android",
412
+ },
413
+ });
274
414
  return {
275
415
  content: [
276
416
  {
277
417
  type: "text",
278
- text: `Screenshot saved to: ${result.result}`
418
+ text: infoText
419
+ },
420
+ {
421
+ type: "image",
422
+ data: result.data.toString("base64"),
423
+ mimeType: "image/jpeg"
279
424
  }
280
425
  ]
281
426
  };
427
+ }
428
+ return {
429
+ content: [
430
+ {
431
+ type: "text",
432
+ text: `Screenshot saved to: ${result.result}`
433
+ }
434
+ ]
435
+ };
436
+ }
437
+ async function chromiumScreenshotResponse(app, outputPath) {
438
+ let shot;
439
+ try {
440
+ shot = await chromiumCapture(app);
441
+ }
442
+ catch (err) {
443
+ return {
444
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
445
+ isError: true,
446
+ };
447
+ }
448
+ let saveNote = "";
449
+ if (outputPath) {
450
+ try {
451
+ writeFileSync(outputPath, shot.buffer);
452
+ }
453
+ catch (err) {
454
+ saveNote = `\nCould not save to ${outputPath}: ${err instanceof Error ? err.message : String(err)}`;
455
+ }
456
+ }
457
+ imageBuffer.add({
458
+ id: `chromium-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
459
+ image: shot.buffer,
460
+ timestamp: Date.now(),
461
+ source: "screenshot",
462
+ metadata: { width: shot.width, height: shot.height, scaleFactor: shot.scaleFactor, platform: "chromium" },
282
463
  });
464
+ const summary = await chromiumScreenState(app).then((ss) => formatScreenStateSummary(ss)).catch(() => null);
465
+ const { w, h, dpr } = shot.viewport;
466
+ const downscaled = shot.scaleFactor !== 1
467
+ ? `, downscaled from ${Math.round(w * dpr)}x${Math.round(h * dpr)} to fit API limits`
468
+ : "";
469
+ const text = `Screenshot ${shot.width}x${shot.height} px of ${app.deviceInfo.deviceName} ` +
470
+ `(chromium viewport ${w}x${h} CSS px at devicePixelRatio ${dpr}${downscaled}).\n` +
471
+ `Pass pixel coordinates from this image straight to tap(x, y, device).` + saveNote +
472
+ (summary ? `\n\n${summary}` : "");
473
+ return {
474
+ content: [
475
+ { type: "text", text },
476
+ { type: "image", data: shot.buffer.toString("base64"), mimeType: "image/jpeg" },
477
+ ],
478
+ };
479
+ }
480
+ export function registerScreenshotTools(server) {
481
+ registerToolWithTelemetry(server, "ios_screenshot", {
482
+ description: "Take a screenshot from an iOS simulator. Returns the image plus a screen-state summary: active route (name + navigation stack), overlay-grouped tappable elements (pressables behind an open sheet/modal are excluded), component names as JSX tags, labels, testIDs, and frames — all in ready-to-tap pixel coordinates. Prefer tap(text=\"...\") when text is exact and unique; otherwise use tap(x, y) with coordinates from the list — this is the most reliable way to tap icons or visually-identified elements. Use component names for inspect_component/find_components.\n" +
483
+ "PURPOSE: Snapshot what the user sees on iOS AND get tap-ready pressables + a structured component map in one call.\n" +
484
+ "WHEN TO USE: Any visual verification, before/after comparison, or as the starting point for tapping UI by coordinates.\n" +
485
+ "WORKFLOW: ios_screenshot -> pick element from pressables -> tap(x, y) or tap(testID=...) -> ios_screenshot to verify.\n" +
486
+ "PHYSICAL DEVICES: a USB-attached iPhone/iPad works too (pass its UDID or name from list_devices) — capture only, via pymobiledevice3. No pressables, no tap/swipe/input_text: iOS has no touch injection below 17.\n" +
487
+ "LIMITATIONS: Otherwise requires a booted iOS simulator (simctl). For system dialogs without RN, combine with tap(..., native=true).\n" +
488
+ "GOOD: ios_screenshot()\n" +
489
+ "BAD: ios_screenshot({ udid: \"guess\" }) with a made-up UDID — run list_devices first.\n" +
490
+ "SOURCE: to jump from a pixel to the code that renders it, call inspect_at_point(x, y) — it returns the absolute file and line.\n",
491
+ inputSchema: {
492
+ outputPath: z
493
+ .string()
494
+ .optional()
495
+ .describe("Optional path to save the screenshot. If not provided, saves to temp directory."),
496
+ udid: z
497
+ .string()
498
+ .optional()
499
+ .describe(IOS_ARG_DESC),
500
+ device: z
501
+ .string()
502
+ .optional()
503
+ .describe("Alias for `udid` — same accepted values. Provided for consistency with tap/get_screen_layout/get_screen_state, which all use `device`. If both are given, `udid` wins.")
504
+ }
505
+ }, iosScreenshotHandler);
283
506
  registerToolWithTelemetry(server, "android_screenshot", {
284
507
  description: "Take a screenshot from an Android device/emulator. Returns the image plus a screen-state summary: active route (name + navigation stack), overlay-grouped tappable elements (pressables behind an open sheet/modal are excluded), component names as JSX tags, labels, testIDs, and frames — all in ready-to-tap pixel coordinates. Prefer tap(text=\"...\") when text is exact and unique; otherwise use tap(x, y) with coordinates from the list — this is the most reliable way to tap icons or visually-identified elements. Use component names for inspect_component/find_components.\n" +
285
508
  "PURPOSE: Snapshot what the user sees on Android AND get tap-ready pressables + a structured component map in one call.\n" +
@@ -303,179 +526,35 @@ export function registerScreenshotTools(server) {
303
526
  .optional()
304
527
  .describe("Alias for `deviceId` — same accepted values. Provided for consistency with tap/get_screen_state/ios_screenshot, which all accept `device`. If both are given, `deviceId` wins.")
305
528
  }
306
- }, async ({ outputPath, deviceId: deviceIdArg, device }) => {
307
- const deviceId = deviceIdArg ?? device;
308
- const resolved = await resolveAndroidDeviceId(deviceId);
309
- if (!resolved.ok)
310
- return resolved.response;
311
- const androidTargetApp = getConnectedAppByAndroidDeviceId(resolved.serial);
312
- const capturePromise = androidScreenshot(outputPath, resolved.serial);
313
- const statusBarPromise = androidGetStatusBarHeight(resolved.serial).catch(() => null);
314
- const densityPromise = androidGetDensity(resolved.serial).catch(() => null);
315
- const screenStatePromise = androidTargetApp
316
- ? getScreenState({ device: androidTargetApp.deviceInfo.deviceName }).catch(() => null)
317
- : Promise.resolve(null);
318
- const result = await capturePromise;
319
- if (!result.success) {
320
- return {
321
- content: [
322
- {
323
- type: "text",
324
- text: `Error: ${result.error}`
325
- }
326
- ],
327
- isError: true
328
- };
329
- }
330
- if (result.data) {
331
- const pixelWidth = result.originalWidth || 0;
332
- const pixelHeight = result.originalHeight || 0;
333
- const targetApp = getConnectedAppByAndroidDeviceId(resolved.serial);
334
- const targetDeviceName = targetApp?.deviceInfo.deviceName;
335
- if (targetApp) {
336
- targetApp.lastScreenshot = {
337
- originalWidth: pixelWidth,
338
- originalHeight: pixelHeight,
339
- scaleFactor: result.scaleFactor || 1,
340
- };
341
- }
342
- const androidDeliveredW = result.scaleFactor && result.scaleFactor > 1
343
- ? Math.round(pixelWidth / result.scaleFactor)
344
- : pixelWidth;
345
- const androidDeliveredH = result.scaleFactor && result.scaleFactor > 1
346
- ? Math.round(pixelHeight / result.scaleFactor)
347
- : pixelHeight;
348
- let infoText = result.scaleFactor && result.scaleFactor > 1
349
- ? `Screenshot: raw ${pixelWidth}x${pixelHeight} px → delivered ${androidDeliveredW}x${androidDeliveredH} px (downscaled ${(1 / result.scaleFactor).toFixed(3)}Ɨ to fit API limits). Pressable coordinates below are in delivered-image pixels.`
350
- : `Screenshot captured (${pixelWidth}x${pixelHeight} pixels)`;
351
- if (resolved.serial) {
352
- recordScreenMetrics(resolved.serial, {
353
- rawWidth: pixelWidth,
354
- rawHeight: pixelHeight,
355
- deliveredWidth: androidDeliveredW,
356
- deliveredHeight: androidDeliveredH,
357
- downscale: result.scaleFactor && result.scaleFactor > 1 ? 1 / result.scaleFactor : 1,
358
- capturedAt: Date.now(),
359
- });
360
- }
361
- let statusBarPixels = 63;
362
- let statusBarDp = 24;
363
- let densityDpi = 440;
364
- try {
365
- const statusBarResult = await statusBarPromise;
366
- if (statusBarResult && statusBarResult.success && statusBarResult.heightPixels) {
367
- statusBarPixels = statusBarResult.heightPixels;
368
- statusBarDp = statusBarResult.heightDp || 24;
369
- }
370
- const densityResult = await densityPromise;
371
- if (densityResult && densityResult.success && densityResult.density) {
372
- densityDpi = densityResult.density;
373
- }
374
- }
375
- catch {
376
- }
377
- let pressablesText = null;
378
- let pressablesIsScreenState = false;
379
- if (targetApp) {
380
- try {
381
- const ssResult = await screenStatePromise;
382
- if (ssResult && ssResult.success && ssResult.screenState) {
383
- const screenshotScale = result.scaleFactor || 1;
384
- const densityScale = densityDpi / 160;
385
- const toPx = (v) => Math.round((v * densityScale) / screenshotScale);
386
- const screenSpaceSs = screenStateToScreenSpace(ssResult.screenState, {
387
- platform: "android",
388
- topInset: statusBarDp
389
- });
390
- pressablesText = formatScreenStateSummary(screenSpaceSs, (p) => ({
391
- center: { x: toPx(p.center.x), y: toPx(p.center.y) },
392
- frame: {
393
- x: toPx(p.bounds.x),
394
- y: toPx(p.bounds.y),
395
- width: toPx(p.bounds.width),
396
- height: toPx(p.bounds.height),
397
- },
398
- }));
399
- pressablesIsScreenState = true;
400
- }
401
- }
402
- catch {
403
- }
404
- }
405
- infoText += `\nšŸ“± Android uses PIXELS for all coordinates`;
406
- if (result.scaleFactor && result.scaleFactor > 1) {
407
- infoText += `\nšŸ“ tap() handles coordinate conversion automatically — pass pixel coords from this image directly`;
408
- }
409
- else {
410
- infoText += `\nšŸ“ Screenshot coords = tap coords (no conversion needed)`;
411
- }
412
- infoText += `\nāš ļø Status bar: ${statusBarPixels}px (${statusBarDp}dp) from top - app content starts below this`;
413
- infoText += `\nšŸ“Š Display density: ${densityDpi}dpi`;
414
- if (pressablesText) {
415
- infoText += pressablesIsScreenState
416
- ? `\n\n🧭 Screen state (route + tappable elements, coordinates in screenshot pixels):\n`
417
- : `\n\nšŸŽÆ Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
418
- infoText += `\n${pressablesText}`;
419
- infoText += `\n\nšŸ’” Next steps:`;
420
- infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
421
- infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
422
- infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
423
- infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
424
- }
425
- else {
426
- if (!targetApp && connectedApps.size > 0) {
427
- infoText += `\n\nā„¹ļø Pressable enrichment skipped: no RN app is connected to device ${deviceId ?? "(default)"}.`;
428
- infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
429
- }
430
- infoText += `\n\nšŸ’” Next steps:`;
431
- infoText += `\n • tap(text="Button Label") — tap element by visible text`;
432
- infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
433
- infoText += `\n • get_screen_layout — get full UI tree with real on-screen positions`;
434
- }
435
- if (targetApp) {
436
- try {
437
- const logBoxState = await detectLogBox(targetDeviceName);
438
- if (logBoxState && logBoxState.total > 0) {
439
- infoText += formatLogBoxWarning(logBoxState);
440
- }
441
- }
442
- catch {
443
- }
444
- }
445
- imageBuffer.add({
446
- id: `android-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
447
- image: result.data,
448
- timestamp: Date.now(),
449
- source: "android_screenshot",
450
- metadata: {
451
- width: result.originalWidth || 0,
452
- height: result.originalHeight || 0,
453
- scaleFactor: result.scaleFactor || 1,
454
- platform: "android",
455
- },
456
- });
529
+ }, androidScreenshotHandler);
530
+ registerToolWithTelemetry(server, "screenshot", {
531
+ description: "Take a screenshot of whichever target `device` resolves to: an iOS simulator, an Android device, or a chromium (Electron / Chrome) window.\n" +
532
+ "PURPOSE: One capture tool for every platform. On iOS and Android it is exactly ios_screenshot / android_screenshot, pressables summary included. On chromium it captures the page viewport (no window chrome) over CDP, with the same element summary as get_screen_state.\n" +
533
+ "COORDINATES: pixels in the returned image are the coordinates tap(x, y) takes, on every platform. Never scale them yourself.\n" +
534
+ "GOOD: screenshot(); screenshot({ device: \"FluentTalk\" })\n" +
535
+ "LIMITATIONS: a hidden or minimised chromium window does not paint and cannot be captured: the call fails at once and says so.",
536
+ inputSchema: {
537
+ device: z
538
+ .string()
539
+ .optional()
540
+ .describe("Target: a connected app name (get_apps), or a simulator/emulator name, UDID or adb serial. Omit for the default target."),
541
+ outputPath: z.string().optional().describe("Optional path to also save the image to."),
542
+ },
543
+ }, async ({ device, outputPath }) => {
544
+ const chromeApp = chromiumAppFor("screenshot", device);
545
+ if (chromeApp)
546
+ return await chromiumScreenshotResponse(chromeApp, outputPath);
547
+ const resolved = await resolveDeviceTarget(device);
548
+ if (!resolved.ok) {
457
549
  return {
458
- content: [
459
- {
460
- type: "text",
461
- text: infoText
462
- },
463
- {
464
- type: "image",
465
- data: result.data.toString("base64"),
466
- mimeType: "image/jpeg"
467
- }
468
- ]
550
+ content: [{ type: "text", text: `Error: ${formatResolverError(resolved.error)}` }],
551
+ isError: true,
469
552
  };
470
553
  }
471
- return {
472
- content: [
473
- {
474
- type: "text",
475
- text: `Screenshot saved to: ${result.result}`
476
- }
477
- ]
478
- };
554
+ const t = resolved.target;
555
+ return t.platform === "ios"
556
+ ? await iosScreenshotHandler({ outputPath, udid: t.iosUdid ?? device })
557
+ : await androidScreenshotHandler({ outputPath, deviceId: t.androidSerial ?? device });
479
558
  });
480
559
  registerToolWithTelemetry(server, "get_images", {
481
560
  description: "Access the shared image buffer containing screenshots from all tools (ios_screenshot, android_screenshot, tap verification). Returns metadata only by default — use id or groupId+frameIndex to retrieve actual image data. Tap burst verification stores frame groups here when burst=true is used.\n" +