execbro 2.1.2 → 2.3.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 (138) hide show
  1. package/README.md +4 -3
  2. package/build/__tests__/integration/text-input.test.d.ts +2 -0
  3. package/build/__tests__/integration/text-input.test.d.ts.map +1 -0
  4. package/build/__tests__/integration/text-input.test.js +181 -0
  5. package/build/__tests__/integration/text-input.test.js.map +1 -0
  6. package/build/__tests__/unit/focusedInputTools.test.js +19 -6
  7. package/build/__tests__/unit/focusedInputTools.test.js.map +1 -1
  8. package/build/__tests__/unit/inputTarget.test.d.ts +2 -0
  9. package/build/__tests__/unit/inputTarget.test.d.ts.map +1 -0
  10. package/build/__tests__/unit/inputTarget.test.js +144 -0
  11. package/build/__tests__/unit/inputTarget.test.js.map +1 -0
  12. package/build/__tests__/unit/inputTargetTools.test.d.ts +2 -0
  13. package/build/__tests__/unit/inputTargetTools.test.d.ts.map +1 -0
  14. package/build/__tests__/unit/inputTargetTools.test.js +46 -0
  15. package/build/__tests__/unit/inputTargetTools.test.js.map +1 -0
  16. package/build/__tests__/unit/inputTextTool.test.d.ts +2 -0
  17. package/build/__tests__/unit/inputTextTool.test.d.ts.map +1 -0
  18. package/build/__tests__/unit/inputTextTool.test.js +120 -0
  19. package/build/__tests__/unit/inputTextTool.test.js.map +1 -0
  20. package/build/__tests__/unit/keyboardMetrics.test.d.ts +2 -0
  21. package/build/__tests__/unit/keyboardMetrics.test.d.ts.map +1 -0
  22. package/build/__tests__/unit/keyboardMetrics.test.js +58 -0
  23. package/build/__tests__/unit/keyboardMetrics.test.js.map +1 -0
  24. package/build/__tests__/unit/keyboardRaise.test.d.ts +2 -0
  25. package/build/__tests__/unit/keyboardRaise.test.d.ts.map +1 -0
  26. package/build/__tests__/unit/keyboardRaise.test.js +74 -0
  27. package/build/__tests__/unit/keyboardRaise.test.js.map +1 -0
  28. package/build/__tests__/unit/nativeInputValue.test.d.ts +2 -0
  29. package/build/__tests__/unit/nativeInputValue.test.d.ts.map +1 -0
  30. package/build/__tests__/unit/nativeInputValue.test.js +67 -0
  31. package/build/__tests__/unit/nativeInputValue.test.js.map +1 -0
  32. package/build/__tests__/unit/screenSpace.test.d.ts +2 -0
  33. package/build/__tests__/unit/screenSpace.test.d.ts.map +1 -0
  34. package/build/__tests__/unit/screenSpace.test.js +58 -0
  35. package/build/__tests__/unit/screenSpace.test.js.map +1 -0
  36. package/build/__tests__/unit/screenStateInputValue.test.d.ts +2 -0
  37. package/build/__tests__/unit/screenStateInputValue.test.d.ts.map +1 -0
  38. package/build/__tests__/unit/screenStateInputValue.test.js +49 -0
  39. package/build/__tests__/unit/screenStateInputValue.test.js.map +1 -0
  40. package/build/__tests__/unit/screenStateKeyboard.test.d.ts +2 -0
  41. package/build/__tests__/unit/screenStateKeyboard.test.d.ts.map +1 -0
  42. package/build/__tests__/unit/screenStateKeyboard.test.js +99 -0
  43. package/build/__tests__/unit/screenStateKeyboard.test.js.map +1 -0
  44. package/build/__tests__/unit/textEntry.test.d.ts +2 -0
  45. package/build/__tests__/unit/textEntry.test.d.ts.map +1 -0
  46. package/build/__tests__/unit/textEntry.test.js +291 -0
  47. package/build/__tests__/unit/textEntry.test.js.map +1 -0
  48. package/build/__tests__/unit/visibility.test.js +21 -0
  49. package/build/__tests__/unit/visibility.test.js.map +1 -1
  50. package/build/core/focusedInputTools.d.ts.map +1 -1
  51. package/build/core/focusedInputTools.js +13 -35
  52. package/build/core/focusedInputTools.js.map +1 -1
  53. package/build/core/guides.d.ts.map +1 -1
  54. package/build/core/guides.js +18 -4
  55. package/build/core/guides.js.map +1 -1
  56. package/build/core/index.d.ts +1 -1
  57. package/build/core/index.d.ts.map +1 -1
  58. package/build/core/index.js +1 -1
  59. package/build/core/index.js.map +1 -1
  60. package/build/core/injected/visibility.d.ts +14 -1
  61. package/build/core/injected/visibility.d.ts.map +1 -1
  62. package/build/core/injected/visibility.js +28 -4
  63. package/build/core/injected/visibility.js.map +1 -1
  64. package/build/core/injectedFilters.d.ts +31 -0
  65. package/build/core/injectedFilters.d.ts.map +1 -0
  66. package/build/core/injectedFilters.js +31 -0
  67. package/build/core/injectedFilters.js.map +1 -0
  68. package/build/core/inputTarget.d.ts +85 -0
  69. package/build/core/inputTarget.d.ts.map +1 -0
  70. package/build/core/inputTarget.js +364 -0
  71. package/build/core/inputTarget.js.map +1 -0
  72. package/build/core/inputTargetTools.d.ts +12 -0
  73. package/build/core/inputTargetTools.d.ts.map +1 -0
  74. package/build/core/inputTargetTools.js +26 -0
  75. package/build/core/inputTargetTools.js.map +1 -0
  76. package/build/core/inspector.d.ts +6 -0
  77. package/build/core/inspector.d.ts.map +1 -1
  78. package/build/core/inspector.js +200 -36
  79. package/build/core/inspector.js.map +1 -1
  80. package/build/core/ios.d.ts.map +1 -1
  81. package/build/core/ios.js +7 -1
  82. package/build/core/ios.js.map +1 -1
  83. package/build/core/keyboardMetrics.d.ts +11 -0
  84. package/build/core/keyboardMetrics.d.ts.map +1 -0
  85. package/build/core/keyboardMetrics.js +40 -0
  86. package/build/core/keyboardMetrics.js.map +1 -0
  87. package/build/core/keyboardRaise.d.ts +22 -0
  88. package/build/core/keyboardRaise.d.ts.map +1 -0
  89. package/build/core/keyboardRaise.js +74 -0
  90. package/build/core/keyboardRaise.js.map +1 -0
  91. package/build/core/nativeInputValue.d.ts +48 -0
  92. package/build/core/nativeInputValue.d.ts.map +1 -0
  93. package/build/core/nativeInputValue.js +113 -0
  94. package/build/core/nativeInputValue.js.map +1 -0
  95. package/build/core/pressables.d.ts +0 -32
  96. package/build/core/pressables.d.ts.map +1 -1
  97. package/build/core/pressables.js +5 -1061
  98. package/build/core/pressables.js.map +1 -1
  99. package/build/core/screenLayout.d.ts +3 -0
  100. package/build/core/screenLayout.d.ts.map +1 -1
  101. package/build/core/screenLayout.js +32 -0
  102. package/build/core/screenLayout.js.map +1 -1
  103. package/build/core/screenSpace.d.ts +75 -0
  104. package/build/core/screenSpace.d.ts.map +1 -0
  105. package/build/core/screenSpace.js +84 -0
  106. package/build/core/screenSpace.js.map +1 -0
  107. package/build/core/screenSpaceDevice.d.ts +15 -0
  108. package/build/core/screenSpaceDevice.d.ts.map +1 -0
  109. package/build/core/screenSpaceDevice.js +24 -0
  110. package/build/core/screenSpaceDevice.js.map +1 -0
  111. package/build/core/screenState.d.ts +39 -0
  112. package/build/core/screenState.d.ts.map +1 -1
  113. package/build/core/screenState.js +200 -16
  114. package/build/core/screenState.js.map +1 -1
  115. package/build/core/textEntry.d.ts +75 -0
  116. package/build/core/textEntry.d.ts.map +1 -0
  117. package/build/core/textEntry.js +252 -0
  118. package/build/core/textEntry.js.map +1 -0
  119. package/build/tools/componentTools.d.ts.map +1 -1
  120. package/build/tools/componentTools.js +63 -188
  121. package/build/tools/componentTools.js.map +1 -1
  122. package/build/tools/interactionTools.d.ts +13 -0
  123. package/build/tools/interactionTools.d.ts.map +1 -1
  124. package/build/tools/interactionTools.js +112 -2
  125. package/build/tools/interactionTools.js.map +1 -1
  126. package/build/tools/screenshotTools.d.ts.map +1 -1
  127. package/build/tools/screenshotTools.js +37 -72
  128. package/build/tools/screenshotTools.js.map +1 -1
  129. package/package.json +1 -1
  130. package/tools.json +10 -4
  131. package/build/__tests__/unit/focusedInput.test.d.ts +0 -2
  132. package/build/__tests__/unit/focusedInput.test.d.ts.map +0 -1
  133. package/build/__tests__/unit/focusedInput.test.js +0 -58
  134. package/build/__tests__/unit/focusedInput.test.js.map +0 -1
  135. package/build/core/focusedInput.d.ts +0 -27
  136. package/build/core/focusedInput.d.ts.map +0 -1
  137. package/build/core/focusedInput.js +0 -110
  138. package/build/core/focusedInput.js.map +0 -1
@@ -0,0 +1,252 @@
1
+ import { resolveWrittenField } from "./nativeInputValue.js";
2
+ /**
3
+ * Names the likely cause of a mismatch. The field's own keyboard settings
4
+ * transform text on the way in, and "landed differently" alone sends the
5
+ * reader hunting for a bug in the write when the write was fine.
6
+ */
7
+ export function diagnoseMismatch(sent, landed) {
8
+ if (landed === null)
9
+ return "";
10
+ if (landed !== sent && landed.toLowerCase() === sent.toLowerCase()) {
11
+ return " — only the capitalisation differs, which is the field's keyboard transforming input" +
12
+ " (RN TextInput defaults autoCapitalize to 'sentences'); set autoCapitalize=\"none\" to type verbatim";
13
+ }
14
+ if (landed.replace(/\s+/g, "") === sent.replace(/\s+/g, "")) {
15
+ return " — only whitespace differs, which is usually iOS smart punctuation or autocorrect spacing";
16
+ }
17
+ if (landed.length === sent.length && [...landed].sort().join("") === [...sent].sort().join("")) {
18
+ return " — the same characters arrived in a different order, which is the HID keystroke race;" +
19
+ " the retry already ran, so this one did not settle";
20
+ }
21
+ return "";
22
+ }
23
+ /**
24
+ * Whether the HID driver can express this text at all.
25
+ *
26
+ * AXe types US-keyboard HID keycodes: A-Z, a-z, 0-9, space and the ASCII
27
+ * symbols. Nothing outside that map has a keycode to send — which rules out
28
+ * Cyrillic and CJK, and equally Spanish accents (á, ñ, ü), a case that is easy
29
+ * to miss because the text otherwise looks Latin. `adb shell input text` is no
30
+ * better: non-ASCII throws a NullPointerException inside InputShellCommand.
31
+ */
32
+ export function isHidTypeable(text) {
33
+ // Printable ASCII plus tab/newline. Deliberately excludes the C1 range and
34
+ // anything above 0x7E.
35
+ return /^[\x20-\x7E\t\n]*$/.test(text);
36
+ }
37
+ function queryOf(a) {
38
+ const q = {};
39
+ if (a.testID != null)
40
+ q.testID = a.testID;
41
+ if (a.component != null)
42
+ q.component = a.component;
43
+ if (a.textMatch != null)
44
+ q.textMatch = a.textMatch;
45
+ if (a.index != null)
46
+ q.index = a.index;
47
+ return Object.keys(q).length > 0 ? q : undefined;
48
+ }
49
+ /**
50
+ * Resolve -> focus -> write -> read back -> retry once -> raise keyboard.
51
+ *
52
+ * The read-back comparison is EXACT. The corruption reproduced on device is a
53
+ * reorder (CASEB -> CSEBA), which every loose check — length, non-empty,
54
+ * prefix — passes. A success from this function means the field holds the
55
+ * requested string, or it says it could not confirm that.
56
+ */
57
+ export async function enterText(args, deps) {
58
+ const q = queryOf(args);
59
+ // 1. Resolve. A miss is a hard failure — this is precisely where the old
60
+ // tools typed into the void and reported success.
61
+ const target = await deps.runOp({ kind: "find" }, q, args.device);
62
+ if (!target.found) {
63
+ return {
64
+ success: false,
65
+ error: target.reason,
66
+ ...(target.ambiguous && { ambiguous: true }),
67
+ ...(target.candidates && { candidates: target.candidates }),
68
+ ...(target.totalInputs !== undefined && { totalInputs: target.totalInputs })
69
+ };
70
+ }
71
+ // 2. Focus it ourselves if needed. A tap reporting success does not
72
+ // guarantee React focus — reproduced on device.
73
+ if (!target.focused) {
74
+ const focused = await deps.runOp({ kind: "focus" }, q, args.device);
75
+ if (!focused.found)
76
+ return { success: false, error: focused.reason };
77
+ if (!focused.ok)
78
+ return { success: false, error: focused.via ?? "could not focus the input" };
79
+ }
80
+ // An uncontrolled field's current text is not in props, so `target.value` is
81
+ // always null for one. Taking that as "" made append silently behave as
82
+ // replace, and the retry then CLEARED the field to make the wrong answer
83
+ // verify clean. The accessibility tree is the only place the real previous
84
+ // text exists, so it has to be read before `desired` is computed.
85
+ let nativeBefore = [];
86
+ const usesNativeReadBack = !target.controlled && deps.readNativeFields !== undefined;
87
+ let previous = target.value ?? "";
88
+ let previousKnown = target.controlled;
89
+ if (usesNativeReadBack) {
90
+ nativeBefore = (await deps.readNativeFields()).fields;
91
+ const current = resolveWrittenField(nativeBefore, nativeBefore, target.testID);
92
+ if (current && current.text !== null) {
93
+ previous = current.text;
94
+ previousKnown = true;
95
+ }
96
+ }
97
+ const desired = args.replace ? args.text : previous + args.text;
98
+ // Three paths, chosen by what the field can actually honour:
99
+ //
100
+ // controlled -> onChangeText. The value prop mirrors the text,
101
+ // so this both writes and stays verifiable.
102
+ // uncontrolled + handler, ASCII -> real keystrokes. The most faithful
103
+ // simulation, and the retry now catches its
104
+ // keystroke race, so the racy path is only taken
105
+ // where it can actually succeed.
106
+ // uncontrolled + handler, non-ASCII -> setNativeProps AND a direct
107
+ // onChangeText call. HID cannot express these
108
+ // characters at all, so faithfulness is not on
109
+ // offer; firing the handler is what keeps the
110
+ // app from missing text the field displays.
111
+ // uncontrolled, no handler -> setNativeProps. Nothing to fire.
112
+ // `fromEmpty` says whether the field was just cleared. It matters only for
113
+ // HID, which appends at the caret rather than setting a value: typing the
114
+ // delta into a populated field is right, but typing it into a cleared one
115
+ // would lose whatever came before.
116
+ const write = async (fromEmpty) => {
117
+ if (target.controlled) {
118
+ const r = await deps.runOp({ kind: "setValue", value: desired }, q, args.device);
119
+ return { path: "react", ok: r.found && r.ok, error: r.found ? r.via : r.reason };
120
+ }
121
+ if (target.hasOnChangeText && isHidTypeable(args.text)) {
122
+ const r = await deps.typeHid(fromEmpty ? desired : args.text);
123
+ return { path: "hid", ok: r.success, error: r.error };
124
+ }
125
+ const r = await deps.runOp({ kind: "setNative", value: desired }, q, args.device);
126
+ return { path: "native", ok: r.found && r.ok, error: r.found ? r.via : r.reason };
127
+ };
128
+ // `undefined` from readBack means "could not determine" and must surface as
129
+ // unverified — never as a mismatch, which is what comparing against an
130
+ // always-null read produced before.
131
+ const readBack = async () => {
132
+ if (target.controlled) {
133
+ const r = await deps.runOp({ kind: "read" }, q, args.device);
134
+ return r.found ? r.value : null;
135
+ }
136
+ if (!usesNativeReadBack)
137
+ return undefined;
138
+ // A native write updates the view asynchronously, and the accessibility
139
+ // dump is a separate process — reading immediately captures the state
140
+ // from before the write. Poll until the text appears, then stop. Going
141
+ // through MCP hid this because the call latency happened to cover it.
142
+ const wait = deps.delay ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
143
+ let last;
144
+ for (const ms of [0, 250, 500]) {
145
+ if (ms > 0)
146
+ await wait(ms);
147
+ const after = await deps.readNativeFields();
148
+ if (after.error) {
149
+ last = undefined;
150
+ continue;
151
+ }
152
+ const hit = resolveWrittenField(nativeBefore, after.fields, target.testID);
153
+ last = hit ? hit.text : undefined;
154
+ if (last === desired)
155
+ return last;
156
+ }
157
+ return last;
158
+ };
159
+ // HID appends at the caret, so a replace must clear first or it concatenates.
160
+ const needsClearFirst = args.replace === true && !target.controlled && target.hasOnChangeText &&
161
+ isHidTypeable(args.text) && previous.length > 0;
162
+ if (needsClearFirst)
163
+ await deps.runOp({ kind: "clear" }, q, args.device);
164
+ const first = await write(needsClearFirst);
165
+ if (!first.ok) {
166
+ // A failed replace already cleared the field. Put it back rather than
167
+ // leaving the user's data destroyed with no mention of it.
168
+ if (args.replace && target.hasOnChangeText && previous.length > 0) {
169
+ const restored = await deps.runOp({ kind: "setValue", value: previous }, q, args.device);
170
+ return {
171
+ success: false,
172
+ path: first.path,
173
+ error: `${first.error ?? "write failed"} (previous value ${restored.found && restored.ok ? "restored" : "COULD NOT be restored"})`
174
+ };
175
+ }
176
+ return { success: false, path: first.path, error: first.error ?? "write failed" };
177
+ }
178
+ let landed = await readBack();
179
+ // Could not read the field at all — say so instead of guessing either way.
180
+ if (landed === undefined) {
181
+ const keyboard = await deps.raise();
182
+ return {
183
+ success: true,
184
+ path: first.path,
185
+ verified: false,
186
+ keyboard,
187
+ error: target.controlled
188
+ ? "the value could not be read back"
189
+ : "this field is uncontrolled and could not be located in the accessibility tree" +
190
+ (target.testID ? "" : " (it has no testID, and no single field's text changed)")
191
+ };
192
+ }
193
+ let retried = false;
194
+ if (landed !== desired) {
195
+ retried = true;
196
+ // Clear only for HID, which appends at the caret. The other paths set
197
+ // the whole value, so a clear is redundant — and actively harmful:
198
+ // publicInstance.clear() after HID typing races the setNativeProps that
199
+ // follows and wipes it, which made every non-ASCII retry land empty.
200
+ const clearedForRetry = first.path === "hid";
201
+ if (clearedForRetry) {
202
+ await deps.runOp({ kind: "clear" }, q, args.device);
203
+ }
204
+ else {
205
+ // A native write immediately after another input operation is
206
+ // sometimes swallowed — the value simply does not change. Give the
207
+ // previous operation a beat to settle before rewriting, rather than
208
+ // repeating the same call into the same contention.
209
+ const wait = deps.delay ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
210
+ await wait(300);
211
+ }
212
+ const second = await write(clearedForRetry);
213
+ if (!second.ok) {
214
+ return { success: false, path: second.path, retried, error: second.error ?? "rewrite failed" };
215
+ }
216
+ landed = await readBack();
217
+ }
218
+ if (landed !== desired) {
219
+ // A read that failed only on the retry is still "unconfirmed", not a
220
+ // mismatch — we have no evidence about what the field holds.
221
+ if (landed === undefined) {
222
+ const keyboard = await deps.raise();
223
+ return {
224
+ success: true, path: first.path, verified: false, retried, keyboard,
225
+ error: "the rewrite could not be read back, so the result is unconfirmed"
226
+ };
227
+ }
228
+ return {
229
+ success: false,
230
+ path: first.path,
231
+ verified: false,
232
+ retried,
233
+ sent: desired,
234
+ landed,
235
+ error: `text landed differently than it was sent${diagnoseMismatch(desired, landed)}` +
236
+ (!previousKnown && !args.replace
237
+ ? " — note the field's prior text could not be read, so the appended result was predicted from an empty starting point; pass replace:true to write an exact value"
238
+ : "")
239
+ };
240
+ }
241
+ // Last, and never fatal.
242
+ const keyboard = await deps.raise();
243
+ return {
244
+ success: true,
245
+ value: landed,
246
+ path: first.path,
247
+ verified: true,
248
+ ...(retried && { retried }),
249
+ keyboard
250
+ };
251
+ }
252
+ //# sourceMappingURL=textEntry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textEntry.js","sourceRoot":"","sources":["../../src/core/textEntry.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"componentTools.d.ts","sourceRoot":"","sources":["../../src/tools/componentTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA0CzE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0uB9D"}
1
+ {"version":3,"file":"componentTools.d.ts","sourceRoot":"","sources":["../../src/tools/componentTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsDzE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAqkB9D"}
@@ -1,9 +1,28 @@
1
1
  import { z } from "zod";
2
2
  import { registerToolWithTelemetry } from "../core/register.js";
3
- import { iconLabel } from "../core/iconSemantics.js";
4
- import { getComponentTree, getScreenLayout, getPressableElements, getScreenState, formatScreenStateSummary, inspectComponent, findComponents, harvestStacksAtPoint, enrichWithSource, inspectAtPoint, measureComponent, iosScreenshot, androidScreenshot, androidGetDensity, getDevicePixelRatio, getIOSSafeAreaTop, inferIOSDevicePixelRatio, metroMissingHintIfAbsent, hasMetro, getConnectedAppByDevice, getFirstConnectedApp, } from "../core/index.js";
3
+ import { getComponentTree, getScreenLayout, getScreenState, formatScreenStateSummary, inspectComponent, findComponents, harvestStacksAtPoint, enrichWithSource, inspectAtPoint, measureComponent, metroMissingHintIfAbsent, hasMetro, getConnectedAppByDevice, getFirstConnectedApp, } from "../core/index.js";
4
+ import { screenStateToScreenSpace, toScreenSpaceY } from "../core/screenSpace.js";
5
+ import { resolveScreenSpaceMetrics } from "../core/screenSpaceDevice.js";
6
+ import { readKeyboardState } from "../core/keyboardMetrics.js";
5
7
  import { primaryInteractionBanner } from "../core/toolHelpers.js";
6
8
  import { DEVICE_ARG_DESC } from "./_deviceArg.js";
9
+ /**
10
+ * Top inset for whichever device a layout tool is about to answer for.
11
+ *
12
+ * A zero inset is the safe default: {@link toScreenSpaceY} treats it as "leave coordinates
13
+ * alone", so an unresolvable device degrades to the previous raw-fiber behaviour rather
14
+ * than to a wrongly shifted one.
15
+ */
16
+ async function resolveScreenSpaceMetricsFor(device) {
17
+ const app = (device ? getConnectedAppByDevice(device) : getFirstConnectedApp()) ?? getFirstConnectedApp();
18
+ if (!app)
19
+ return { platform: "ios", topInset: 0 };
20
+ return resolveScreenSpaceMetrics({
21
+ platform: app.platform,
22
+ udid: app.simulatorUdid,
23
+ deviceId: app.adbSerial
24
+ });
25
+ }
7
26
  function collectMetaNotes(r) {
8
27
  const out = [];
9
28
  if (r._meta?.reconnected) {
@@ -17,7 +36,7 @@ function collectMetaNotes(r) {
17
36
  export function registerComponentTools(server) {
18
37
  // Tool: Get full screen layout (all components with layout styles)
19
38
  registerToolWithTelemetry(server, "get_screen_layout", {
20
- 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 extended=true to include layout styles (padding, margin, flex, backgroundColor, etc.)." +
39
+ 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 screen space — the same space screenshots, get_screen_state and tap() use, so pass them through unchanged. Use extended=true to include layout styles (padding, margin, flex, backgroundColor, etc.)." +
21
40
  primaryInteractionBanner() + "\n" +
22
41
  "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" +
23
42
  "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" +
@@ -52,7 +71,13 @@ export function registerComponentTools(server) {
52
71
  };
53
72
  }
54
73
  const effectiveTimeoutMs = timeoutMs ?? (extended ? 15000 : 5000);
55
- const result = await getScreenLayout({ extended, summary, device, timeoutMs: effectiveTimeoutMs });
74
+ const result = await getScreenLayout({
75
+ extended,
76
+ summary,
77
+ device,
78
+ timeoutMs: effectiveTimeoutMs,
79
+ screenSpace: await resolveScreenSpaceMetricsFor(device)
80
+ });
56
81
  const metaNotes = collectMetaNotes(result);
57
82
  if (!result.success) {
58
83
  const errText = metaNotes.length > 0 ? `Error: ${result.error}\n\n${metaNotes.join("\n")}` : `Error: ${result.error}`;
@@ -139,129 +164,13 @@ export function registerComponentTools(server) {
139
164
  : `React Component Tree:\n\n${result.result}`;
140
165
  return { content: [{ type: "text", text: body }] };
141
166
  });
142
- // Tool: Get all pressable elements on screen
143
- registerToolWithTelemetry(server, "get_pressable_elements", {
144
- description: "Prefer get_screen_state after navigation (route + overlays + pressables in one call).\n\n" +
145
- "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" +
146
- "HELPER — call before `tap` when you need to enumerate candidate elements before committing to a target; not a replacement for tap itself.\n" +
147
- "PURPOSE: Produce a ready-to-tap inventory of every interactive element on screen with screenshot-pixel coordinates that tap(x, y) accepts directly.\n" +
148
- "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" +
149
- "WORKFLOW: ios_screenshot / android_screenshot -> get_pressable_elements -> tap(testID=\"...\") or tap(x, y) using the center coordinates.\n" +
150
- "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" +
151
- "GOOD: get_pressable_elements()\n" +
152
- "BAD: Calling when tap(testID=\"...\") already works — testID matching is faster and more stable.\n",
153
- inputSchema: {
154
- device: z.string().optional().describe(DEVICE_ARG_DESC)
155
- }
156
- }, async ({ device }) => {
157
- const result = await getPressableElements({ device });
158
- if (!result.success) {
159
- return {
160
- content: [
161
- {
162
- type: "text",
163
- text: `Error: ${result.error}`
164
- }
165
- ],
166
- isError: true
167
- };
168
- }
169
- const elements = result.parsedElements;
170
- if (!elements || elements.length === 0) {
171
- return {
172
- content: [{ type: "text", text: result.result || "No pressable elements found." }]
173
- };
174
- }
175
- // Resolve target app so we can pick the right simulator / android device
176
- const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
177
- if (!targetApp) {
178
- // No connected app — return raw (points) output; better than nothing
179
- return {
180
- content: [{ type: "text", text: result.result || "No pressable elements found." }]
181
- };
182
- }
183
- // Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
184
- // Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
185
- let lines = [];
186
- try {
187
- if (targetApp.platform === "ios") {
188
- const udid = targetApp.simulatorUdid;
189
- const shot = await iosScreenshot(undefined, udid);
190
- const screenshotScale = shot.scaleFactor || 1;
191
- const devicePixelRatio = (shot.originalWidth && shot.originalHeight
192
- ? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
193
- : null) ?? (await getDevicePixelRatio(udid)) ?? 3;
194
- // Fallback to 59pt (iPhone typical) when the UI driver preflight can't
195
- // resolve the true inset — matches ios_screenshot's default. Without this
196
- // shift, react-native-screens modal-presented screens report y relative
197
- // to content origin and taps land in the status bar instead of the button.
198
- const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
199
- // Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
200
- // same scaleFactor when converting our pixel coords back to points.
201
- if (shot.originalWidth && shot.originalHeight) {
202
- targetApp.lastScreenshot = {
203
- originalWidth: shot.originalWidth,
204
- originalHeight: shot.originalHeight,
205
- scaleFactor: screenshotScale
206
- };
207
- }
208
- lines = formatPressablesInPixels(elements, {
209
- platform: "ios",
210
- devicePixelRatio,
211
- screenshotScale,
212
- safeAreaTop
213
- });
214
- }
215
- else {
216
- // Metro's `deviceName` is the device model (e.g. "sdk_gphone16k_arm64"),
217
- // not the adb serial (e.g. "emulator-5554"), so passing it as -s makes
218
- // adb miss the device and androidScreenshot/androidGetDensity silently
219
- // return defaults (scale=1, density=160). That leaves coords in raw
220
- // device pixels — off by ~1.2× from the screenshot/JPEG space the tool
221
- // description promises. Pass undefined to let adb auto-pick (matches
222
- // how android_screenshot works when called without deviceId). Multi-
223
- // Android-device support tracks separately under the multi-device
224
- // refactor; this path is a single-Android-device fix.
225
- const shot = await androidScreenshot(undefined, undefined);
226
- const screenshotScale = shot.scaleFactor || 1;
227
- const density = await androidGetDensity(undefined).catch(() => ({ density: 160 }));
228
- const devicePixelRatio = (density.density || 160) / 160;
229
- if (shot.originalWidth && shot.originalHeight) {
230
- targetApp.lastScreenshot = {
231
- originalWidth: shot.originalWidth,
232
- originalHeight: shot.originalHeight,
233
- scaleFactor: screenshotScale
234
- };
235
- }
236
- lines = formatPressablesInPixels(elements, {
237
- platform: "android",
238
- devicePixelRatio,
239
- screenshotScale,
240
- safeAreaTop: 0
241
- });
242
- }
243
- }
244
- catch {
245
- // Fallback to points if screenshot/metadata unavailable
246
- return {
247
- content: [{ type: "text", text: result.result || "No pressable elements found." }]
248
- };
249
- }
250
- const iconCount = elements.filter((e) => !e.hasLabel).length;
251
- const labeledCount = elements.length - iconCount;
252
- const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
253
- const text = [summary, "", ...lines].join("\n");
254
- return {
255
- content: [{ type: "text", text }]
256
- };
257
- });
258
167
  // Tool: Get current screen state — route, overlays, pressables (post-navigation checkpoint)
259
168
  registerToolWithTelemetry(server, "get_screen_state", {
260
169
  description: "Screenshot-free snapshot of the current screen: active route + params, blocking overlays (sheets, modals, alerts), and every on-screen element merged top-to-bottom within reachability groups. Call after any tap or navigation to orient before the next action. " +
261
170
  "Each line carries an (x, y) center + frame bounds (so anything is a tap(x, y) target), typed by a leading marker: 🔘 pressable (with component JSX tag, label, testID, onPress hint), 📝 text, 🖼 image (with src/alt). " +
262
171
  "Elements covered by an open overlay are grouped under 🚫 Blocked — visible for context, but taps will NOT reach them until the overlay closes. Long text truncates to 80 chars (fullText=true for full strings); pressablesOnly=true returns just the lean tappable list.\n\n" +
263
172
  "WHEN TO USE: After every tap/swipe that may navigate, and to read screen content (prices, labels, which image loaded) without a screenshot+OCR round-trip.\n" +
264
- "LIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection. Coordinates in points (iOS) / dp (Android); text frames are container-level (climb to nearest measurable host).\n" +
173
+ "LIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection.\n" +
265
174
  "SOURCE: this lists what is on screen, not where it lives in code — for the file:line that renders an element, call inspect_at_point(x, y).\n" +
266
175
  "SEE ALSO: get_screen_layout for the full hierarchical component tree (deep inspection) — this gives a flat, tap-ready content list instead.",
267
176
  inputSchema: {
@@ -277,7 +186,13 @@ export function registerComponentTools(server) {
277
186
  isError: true
278
187
  };
279
188
  }
280
- const result = await getScreenState({ device });
189
+ // In parallel: the keyboard read is a second CDP round trip and has
190
+ // no reason to queue behind the fiber walk. It degrades to
191
+ // { visible: false, error } on failure, so it cannot fail this call.
192
+ const [result, keyboard] = await Promise.all([
193
+ getScreenState({ device }),
194
+ readKeyboardState(device)
195
+ ]);
281
196
  const metaNotes = collectMetaNotes(result);
282
197
  if (!result.success) {
283
198
  const errText = metaNotes.length > 0
@@ -288,69 +203,18 @@ export function registerComponentTools(server) {
288
203
  isError: true
289
204
  };
290
205
  }
291
- const ss = result.screenState;
292
- const summary = ss ? formatScreenStateSummary(ss, undefined, { pressablesOnly, fullText }) : (result.result ?? "{}");
206
+ // Normalise to screen space so a coordinate read here can be handed straight to
207
+ // tap()/inspect_at_point/a screenshot. Raw measureInWindow is not screen-relative
208
+ // (Android excludes the status bar; iOS modals measure from their own container),
209
+ // which is why point-space output used to disagree with screenshot pixels.
210
+ const rawSs = result.screenState;
211
+ const ss = rawSs ? screenStateToScreenSpace(rawSs, await resolveScreenSpaceMetricsFor(device)) : rawSs;
212
+ const summary = ss ? formatScreenStateSummary(ss, undefined, { pressablesOnly, fullText, keyboard }) : (result.result ?? "{}");
293
213
  const body = metaNotes.length > 0
294
214
  ? `${summary}\n\n${metaNotes.join("\n")}`
295
215
  : summary;
296
216
  return { content: [{ type: "text", text: body }] };
297
217
  });
298
- function formatPressablesInPixels(elements, opts) {
299
- const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
300
- const out = [];
301
- for (let i = 0; i < elements.length; i++) {
302
- const el = elements[i];
303
- // react-native-screens modal presentations report y relative to content origin;
304
- // shift into the window frame when the element's center sits inside the safe-area
305
- // band. The shift MUST be applied to center and frame atomically — independent
306
- // checks (cy<inset, fy<inset) drift apart on iPad-style insets where a header's
307
- // center is just below the inset but its frame top is just above, producing
308
- // center/frame.y values that don't satisfy center = frame.y + frame.h/2.
309
- let cy = el.center.y;
310
- let fy = el.frame.y;
311
- if (platform === "ios" && safeAreaTop > 0 && cy < safeAreaTop) {
312
- cy += safeAreaTop;
313
- fy += safeAreaTop;
314
- }
315
- // iOS: fiber returns points → convert points × DPR / screenshotScale = JPEG px.
316
- // Android: getPressableElements reconciles fiber DP against uiautomator device-pixel
317
- // bounds (executor.ts, 2026-05-17). After reconciliation, coords are already in
318
- // device pixels — only the JPEG downscale needs to be applied. Multiplying by DPR
319
- // here would re-inflate them by ~density/160 (~2.6× on a 420dpi device), reproducing
320
- // the original out-of-bounds bug.
321
- const toPx = (v) => platform === "android"
322
- ? Math.round(v / screenshotScale)
323
- : Math.round((v * devicePixelRatio) / screenshotScale);
324
- const cx = toPx(el.center.x);
325
- const cyPx = toPx(cy);
326
- const fx = toPx(el.frame.x);
327
- const fyPx = toPx(fy);
328
- const fw = toPx(el.frame.width);
329
- const fh = toPx(el.frame.height);
330
- const num = i + 1;
331
- const iconHint = !el.hasLabel ? iconLabel(el.component, el.icon) : null;
332
- const label = el.hasLabel
333
- ? `"${el.text}"`
334
- : iconHint
335
- ? `(${iconHint})`
336
- : el.intent
337
- ? `(${el.intent} icon)`
338
- : "(icon/image)";
339
- const ids = [];
340
- if (el.testID)
341
- ids.push(`testID="${el.testID}"`);
342
- if (el.accessibilityLabel)
343
- ids.push(`a11y="${el.accessibilityLabel}"`);
344
- const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
345
- const inputStr = el.isInput ? " (input)" : "";
346
- const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
347
- const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
348
- out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
349
- if (el.path)
350
- out.push(` path: ${el.path}`);
351
- }
352
- return out;
353
- }
354
218
  // Tool: Inspect a specific component by name
355
219
  registerToolWithTelemetry(server, "inspect_component", {
356
220
  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." +
@@ -492,16 +356,16 @@ export function registerComponentTools(server) {
492
356
  description: "Inspect layout AND props at (x, y). Returns FRAME PER ANCESTOR (position/size in dp for every ancestor that hit-tested the point) + the innermost component's PROPS (handlers as [Function], refs, custom props like onPress/data/testID). Pure JS hit-test via fiber + measureInWindow — no overlay toggled, zero visual side effect. Works on Paper and Fabric.\n" +
493
357
  "PURPOSE: Layout/props diagnosis — \"where is each ancestor positioned, and what props does the touched component expose?\"\n" +
494
358
  "WHEN TO USE: A button is clipped, hit area is wrong, animated frame is unexpected — or you need handler/ref/non-style props. Also preferred for tight loops (no overlay flicker).\n" +
495
- "WORKFLOW: screenshot → suspect pixel → divide by pixel ratio → inspect_at_point(x, y).\n" +
496
- "LIMITATIONS: Coordinates MUST be in dp, not screenshot pixels — wrong unit = wrong node. Style is the node's own style object, not the merged cascade.\n" +
359
+ "WORKFLOW: screenshot or get_screen_state → take the coordinate as-is → inspect_at_point(x, y).\n" +
360
+ "LIMITATIONS: Style is the node's own style object, not the merged cascade. `frame` is the element's own box; `hitFrame` (when present) is the innermost node actually under the point.\n" +
497
361
  "SOURCE: also returns `source: {file, line, column}` for the component at the point, plus the owner chain as `Source ancestors` (set source=false to skip in tight loops).\n",
498
362
  inputSchema: {
499
363
  x: z
500
364
  .number()
501
- .describe("X coordinate in dp (logical pixels). Convert from screenshot pixels by dividing by the device pixel ratio."),
365
+ .describe("X coordinate in screen space — take it straight from a screenshot, get_screen_state or get_screen_layout. No conversion."),
502
366
  y: z
503
367
  .number()
504
- .describe("Y coordinate in dp (logical pixels). Convert from screenshot pixels by dividing by the device pixel ratio."),
368
+ .describe("Y coordinate in screen space — take it straight from a screenshot, get_screen_state or get_screen_layout. No conversion."),
505
369
  includeProps: z
506
370
  .boolean()
507
371
  .optional()
@@ -520,7 +384,12 @@ export function registerComponentTools(server) {
520
384
  .describe("Resolve the component's source file and line via Metro symbolication. Default true. Set false to skip in tight loops.")
521
385
  }
522
386
  }, async ({ x, y, includeProps, includeFrame, device, source = true }) => {
523
- const result = await inspectAtPoint(x, y, { includeProps, includeFrame, device });
387
+ const result = await inspectAtPoint(x, y, {
388
+ includeProps,
389
+ includeFrame,
390
+ device,
391
+ screenSpace: await resolveScreenSpaceMetricsFor(device)
392
+ });
524
393
  if (!result.success) {
525
394
  return {
526
395
  content: [
@@ -588,7 +457,7 @@ export function registerComponentTools(server) {
588
457
  });
589
458
  // Tool: Measure on-screen geometry of a named component
590
459
  registerToolWithTelemetry(server, "measure", {
591
- 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" +
460
+ 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 screen space, the same space as get_screen_layout, get_screen_state, inspect_at_point and tap().\n" +
592
461
  "PURPOSE: One-shot, name-based component measurement — avoids hand-rolling fiber walks and Promise-wrapping measureInWindow callbacks in execute_in_app.\n" +
593
462
  "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" +
594
463
  "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" +
@@ -626,10 +495,16 @@ export function registerComponentTools(server) {
626
495
  _errorContext: result.outcome
627
496
  };
628
497
  }
498
+ // measure returns a single leaf box, so the band rule applies directly — there is
499
+ // no root container here to exempt. Without this the tool's own promise of "the
500
+ // same space as get_screen_layout and inspect_at_point" would be false: off by the
501
+ // status bar on every Android screen, and by the safe area inside an iOS modal.
502
+ const measureMetrics = await resolveScreenSpaceMetricsFor(device);
503
+ const measuredY = toScreenSpaceY(result.y, measureMetrics);
629
504
  const lines = [
630
505
  `Component: ${result.name}`,
631
- `Frame: (${result.x.toFixed(1)}, ${result.y.toFixed(1)}) ${result.width.toFixed(1)}x${result.height.toFixed(1)}`,
632
- `Center: (${(result.x + result.width / 2).toFixed(1)}, ${(result.y + result.height / 2).toFixed(1)})`,
506
+ `Frame: (${result.x.toFixed(1)}, ${measuredY.toFixed(1)}) ${result.width.toFixed(1)}x${result.height.toFixed(1)}`,
507
+ `Center: (${(result.x + result.width / 2).toFixed(1)}, ${(measuredY + result.height / 2).toFixed(1)})`,
633
508
  ];
634
509
  if (typeof result.nativeTag === "number") {
635
510
  lines.push(`nativeTag: ${result.nativeTag}`);