execbro 1.10.23 → 1.10.25

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 (141) hide show
  1. package/README.md +2 -1
  2. package/build/__tests__/helpers/fake-cdp-server.d.ts +9 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -1
  4. package/build/__tests__/helpers/fake-cdp-server.js +20 -2
  5. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -1
  6. package/build/__tests__/integration/connection-health.test.js +8 -1
  7. package/build/__tests__/integration/connection-health.test.js.map +1 -1
  8. package/build/__tests__/integration/socket-close-midflight.test.d.ts +2 -0
  9. package/build/__tests__/integration/socket-close-midflight.test.d.ts.map +1 -0
  10. package/build/__tests__/integration/socket-close-midflight.test.js +106 -0
  11. package/build/__tests__/integration/socket-close-midflight.test.js.map +1 -0
  12. package/build/__tests__/integration/stale-target-probe.test.d.ts +2 -0
  13. package/build/__tests__/integration/stale-target-probe.test.d.ts.map +1 -0
  14. package/build/__tests__/integration/stale-target-probe.test.js +85 -0
  15. package/build/__tests__/integration/stale-target-probe.test.js.map +1 -0
  16. package/build/__tests__/unit/componentSource.test.d.ts +2 -0
  17. package/build/__tests__/unit/componentSource.test.d.ts.map +1 -0
  18. package/build/__tests__/unit/componentSource.test.js +67 -0
  19. package/build/__tests__/unit/componentSource.test.js.map +1 -0
  20. package/build/__tests__/unit/componentSourceResolve.test.d.ts +2 -0
  21. package/build/__tests__/unit/componentSourceResolve.test.d.ts.map +1 -0
  22. package/build/__tests__/unit/componentSourceResolve.test.js +91 -0
  23. package/build/__tests__/unit/componentSourceResolve.test.js.map +1 -0
  24. package/build/__tests__/unit/deviceResolution.test.d.ts +2 -0
  25. package/build/__tests__/unit/deviceResolution.test.d.ts.map +1 -0
  26. package/build/__tests__/unit/deviceResolution.test.js +108 -0
  27. package/build/__tests__/unit/deviceResolution.test.js.map +1 -0
  28. package/build/__tests__/unit/executor.test.js +48 -12
  29. package/build/__tests__/unit/executor.test.js.map +1 -1
  30. package/build/__tests__/unit/getLogsEmptyContract.test.d.ts +2 -0
  31. package/build/__tests__/unit/getLogsEmptyContract.test.d.ts.map +1 -0
  32. package/build/__tests__/unit/getLogsEmptyContract.test.js +99 -0
  33. package/build/__tests__/unit/getLogsEmptyContract.test.js.map +1 -0
  34. package/build/__tests__/unit/inspectorHarvest.test.d.ts +2 -0
  35. package/build/__tests__/unit/inspectorHarvest.test.d.ts.map +1 -0
  36. package/build/__tests__/unit/inspectorHarvest.test.js +61 -0
  37. package/build/__tests__/unit/inspectorHarvest.test.js.map +1 -0
  38. package/build/__tests__/unit/inspectorSource.test.d.ts +2 -0
  39. package/build/__tests__/unit/inspectorSource.test.d.ts.map +1 -0
  40. package/build/__tests__/unit/inspectorSource.test.js +74 -0
  41. package/build/__tests__/unit/inspectorSource.test.js.map +1 -0
  42. package/build/__tests__/unit/logDiagnosis.test.d.ts +2 -0
  43. package/build/__tests__/unit/logDiagnosis.test.d.ts.map +1 -0
  44. package/build/__tests__/unit/logDiagnosis.test.js +81 -0
  45. package/build/__tests__/unit/logDiagnosis.test.js.map +1 -0
  46. package/build/__tests__/unit/pendingExecutions.test.d.ts +2 -0
  47. package/build/__tests__/unit/pendingExecutions.test.d.ts.map +1 -0
  48. package/build/__tests__/unit/pendingExecutions.test.js +70 -0
  49. package/build/__tests__/unit/pendingExecutions.test.js.map +1 -0
  50. package/build/__tests__/unit/reloadAppAutoConnect.test.d.ts +2 -0
  51. package/build/__tests__/unit/reloadAppAutoConnect.test.d.ts.map +1 -0
  52. package/build/__tests__/unit/reloadAppAutoConnect.test.js +63 -0
  53. package/build/__tests__/unit/reloadAppAutoConnect.test.js.map +1 -0
  54. package/build/__tests__/unit/selectionBuffer.test.d.ts +2 -0
  55. package/build/__tests__/unit/selectionBuffer.test.d.ts.map +1 -0
  56. package/build/__tests__/unit/selectionBuffer.test.js +86 -0
  57. package/build/__tests__/unit/selectionBuffer.test.js.map +1 -0
  58. package/build/__tests__/unit/selectionPoller.test.d.ts +2 -0
  59. package/build/__tests__/unit/selectionPoller.test.d.ts.map +1 -0
  60. package/build/__tests__/unit/selectionPoller.test.js +109 -0
  61. package/build/__tests__/unit/selectionPoller.test.js.map +1 -0
  62. package/build/__tests__/unit/symbolicate.test.d.ts +2 -0
  63. package/build/__tests__/unit/symbolicate.test.d.ts.map +1 -0
  64. package/build/__tests__/unit/symbolicate.test.js +154 -0
  65. package/build/__tests__/unit/symbolicate.test.js.map +1 -0
  66. package/build/__tests__/unit/transportClassify.test.js +108 -7
  67. package/build/__tests__/unit/transportClassify.test.js.map +1 -1
  68. package/build/core/componentSource.d.ts +62 -0
  69. package/build/core/componentSource.d.ts.map +1 -0
  70. package/build/core/componentSource.js +268 -0
  71. package/build/core/componentSource.js.map +1 -0
  72. package/build/core/connection.d.ts +30 -0
  73. package/build/core/connection.d.ts.map +1 -1
  74. package/build/core/connection.js +101 -28
  75. package/build/core/connection.js.map +1 -1
  76. package/build/core/debugGlobals.d.ts.map +1 -1
  77. package/build/core/debugGlobals.js +89 -32
  78. package/build/core/debugGlobals.js.map +1 -1
  79. package/build/core/errors.d.ts +7 -1
  80. package/build/core/errors.d.ts.map +1 -1
  81. package/build/core/errors.js +8 -1
  82. package/build/core/errors.js.map +1 -1
  83. package/build/core/guides.js +1 -1
  84. package/build/core/index.d.ts +4 -1
  85. package/build/core/index.d.ts.map +1 -1
  86. package/build/core/index.js +4 -1
  87. package/build/core/index.js.map +1 -1
  88. package/build/core/inspector.d.ts +24 -0
  89. package/build/core/inspector.d.ts.map +1 -1
  90. package/build/core/inspector.js +58 -0
  91. package/build/core/inspector.js.map +1 -1
  92. package/build/core/jsExecute.d.ts +76 -1
  93. package/build/core/jsExecute.d.ts.map +1 -1
  94. package/build/core/jsExecute.js +193 -32
  95. package/build/core/jsExecute.js.map +1 -1
  96. package/build/core/logDiagnosis.d.ts +65 -0
  97. package/build/core/logDiagnosis.d.ts.map +1 -0
  98. package/build/core/logDiagnosis.js +51 -0
  99. package/build/core/logDiagnosis.js.map +1 -0
  100. package/build/core/register.d.ts.map +1 -1
  101. package/build/core/register.js +22 -6
  102. package/build/core/register.js.map +1 -1
  103. package/build/core/selectionBuffer.d.ts +42 -0
  104. package/build/core/selectionBuffer.d.ts.map +1 -0
  105. package/build/core/selectionBuffer.js +51 -0
  106. package/build/core/selectionBuffer.js.map +1 -0
  107. package/build/core/selectionPoller.d.ts +13 -0
  108. package/build/core/selectionPoller.d.ts.map +1 -0
  109. package/build/core/selectionPoller.js +102 -0
  110. package/build/core/selectionPoller.js.map +1 -0
  111. package/build/core/state.d.ts +10 -0
  112. package/build/core/state.d.ts.map +1 -1
  113. package/build/core/state.js +21 -0
  114. package/build/core/state.js.map +1 -1
  115. package/build/core/symbolicate.d.ts +18 -0
  116. package/build/core/symbolicate.d.ts.map +1 -0
  117. package/build/core/symbolicate.js +119 -0
  118. package/build/core/symbolicate.js.map +1 -0
  119. package/build/core/types.d.ts +8 -0
  120. package/build/core/types.d.ts.map +1 -1
  121. package/build/tools/componentTools.d.ts.map +1 -1
  122. package/build/tools/componentTools.js +78 -9
  123. package/build/tools/componentTools.js.map +1 -1
  124. package/build/tools/connectionTools.d.ts.map +1 -1
  125. package/build/tools/connectionTools.js +10 -4
  126. package/build/tools/connectionTools.js.map +1 -1
  127. package/build/tools/executionTools.js +7 -7
  128. package/build/tools/executionTools.js.map +1 -1
  129. package/build/tools/interactionTools.d.ts.map +1 -1
  130. package/build/tools/interactionTools.js +1 -0
  131. package/build/tools/interactionTools.js.map +1 -1
  132. package/build/tools/logTools.d.ts.map +1 -1
  133. package/build/tools/logTools.js +77 -69
  134. package/build/tools/logTools.js.map +1 -1
  135. package/build/tools/networkTools.d.ts.map +1 -1
  136. package/build/tools/networkTools.js +63 -13
  137. package/build/tools/networkTools.js.map +1 -1
  138. package/build/tools/screenshotTools.d.ts.map +1 -1
  139. package/build/tools/screenshotTools.js +3 -0
  140. package/build/tools/screenshotTools.js.map +1 -1
  141. package/package.json +1 -1
@@ -0,0 +1,268 @@
1
+ import { parseStackString, symbolicateFrames } from "./symbolicate.js";
2
+ /** Shared fiber-root lookup. Hermes-safe: var + function only. */
3
+ const ROOTS_SNIPPET = `
4
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
5
+ var roots = [];
6
+ if (hook && hook.getFiberRoots) {
7
+ if (hook.renderers) {
8
+ for (var entry of hook.renderers) {
9
+ try { roots = roots.concat(Array.from(hook.getFiberRoots(entry[0]) || [])); } catch (e) {}
10
+ }
11
+ }
12
+ if (roots.length === 0) {
13
+ try { roots = Array.from(hook.getFiberRoots(1) || []); } catch (e) {}
14
+ }
15
+ }
16
+ `;
17
+ /**
18
+ * Cheap per-tick probe. Deliberately does NO measurement — it runs every
19
+ * 500-2000ms and must stay inexpensive. Reports whether RN's inspector is
20
+ * mounted and whether its selection changed since the previous tick.
21
+ */
22
+ export function buildSelectionProbeExpression() {
23
+ return `
24
+ (function() {
25
+ ${ROOTS_SNIPPET}
26
+ if (roots.length === 0) return { active: false, selected: false, changed: false };
27
+
28
+ var panels = [];
29
+ function walk(fiber, depth) {
30
+ if (!fiber || depth > 3000) return;
31
+ if (fiber.type && typeof fiber.type !== 'string' &&
32
+ (fiber.type.displayName || fiber.type.name) === 'InspectorPanel') {
33
+ panels.push(fiber);
34
+ }
35
+ var child = fiber.child;
36
+ while (child) { walk(child, depth + 1); child = child.sibling; }
37
+ }
38
+ for (var r = 0; r < roots.length; r++) { walk(roots[r].current, 0); }
39
+
40
+ if (panels.length === 0) {
41
+ globalThis.__execbro_lastInspected = undefined;
42
+ return { active: false, selected: false, changed: false };
43
+ }
44
+
45
+ var panel = null;
46
+ for (var p = 0; p < panels.length; p++) {
47
+ var h = panels[p].memoizedProps && panels[p].memoizedProps.hierarchy;
48
+ if (h && h.length > 0) { panel = panels[p]; break; }
49
+ }
50
+ if (!panel) return { active: true, selected: false, changed: false };
51
+
52
+ var props = panel.memoizedProps;
53
+ var inspected = props.inspected;
54
+
55
+ // RN allocates a fresh object per tap, so identity - not value -
56
+ // distinguishes two consecutive taps on the same element.
57
+ if (inspected === globalThis.__execbro_lastInspected) {
58
+ return { active: true, selected: true, changed: false };
59
+ }
60
+ globalThis.__execbro_lastInspected = inspected;
61
+
62
+ var names = [];
63
+ for (var i = 0; i < props.hierarchy.length; i++) {
64
+ names.push(props.hierarchy[i].name);
65
+ }
66
+
67
+ var style = {};
68
+ if (inspected && inspected.style) {
69
+ var list = Array.isArray(inspected.style) ? inspected.style : [inspected.style];
70
+ for (var s = 0; s < list.length; s++) {
71
+ if (list[s] && typeof list[s] === 'object') {
72
+ for (var k in list[s]) { style[k] = list[s][k]; }
73
+ }
74
+ }
75
+ }
76
+
77
+ return {
78
+ active: true,
79
+ selected: true,
80
+ changed: true,
81
+ element: names.length > 0 ? names[names.length - 1] : 'Unknown',
82
+ path: names.join(' > '),
83
+ hierarchy: names,
84
+ frame: inspected && inspected.frame ? inspected.frame : null,
85
+ style: Object.keys(style).length > 0 ? style : null
86
+ };
87
+ })()
88
+ `;
89
+ }
90
+ /**
91
+ * Hit-tests at (x, y) and harvests raw _debugStack strings up the return chain.
92
+ * Measures host fibers, so this runs only when the probe reports a change -
93
+ * once per tap, never per tick.
94
+ *
95
+ * Prefers _debugSource when present (React < 19, already source coordinates);
96
+ * otherwise emits the raw _debugStack string for host-side symbolication.
97
+ *
98
+ * The cap is deliberately generous. Real trees bury the nearest user component
99
+ * deep behind library wrappers - measured on a production app, a tapped SearchBar
100
+ * sat 12 ancestors above the hit fiber (SVG icon, Pressable and View wrappers in
101
+ * between) and HomeScreen sat at 22. Collapsed library frames are dropped
102
+ * host-side, so a larger walk improves recall without bloating the output; the
103
+ * extra frames cost nothing beyond one batched, cached symbolicate request.
104
+ */
105
+ export function buildDebugStackHarvestExpression(x, y, maxAncestors = 30) {
106
+ return `
107
+ new Promise(function(resolve) {
108
+ var maxAncestors = ${maxAncestors};
109
+ ${ROOTS_SNIPPET}
110
+ if (roots.length === 0) return resolve({ stacks: [], error: 'no-fiber-roots' });
111
+
112
+ function getMeasurable(fiber) {
113
+ var sn = fiber.stateNode;
114
+ if (!sn) return null;
115
+ if (typeof sn.measureInWindow === 'function') return sn;
116
+ if (sn.canonical && sn.canonical.publicInstance &&
117
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
118
+ return sn.canonical.publicInstance;
119
+ }
120
+ if (sn.node && globalThis.nativeFabricUIManager &&
121
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
122
+ var node = sn.node;
123
+ return {
124
+ measureInWindow: function(cb) {
125
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch (e) {}
126
+ }
127
+ };
128
+ }
129
+ return null;
130
+ }
131
+
132
+ var hostFibers = [];
133
+ function walkFibers(fiber, depth) {
134
+ var cur = fiber;
135
+ while (cur) {
136
+ if (hostFibers.length >= 500) return;
137
+ if (typeof cur.type === 'string' && getMeasurable(cur)) hostFibers.push(cur);
138
+ if (cur.child && depth < 250) walkFibers(cur.child, depth + 1);
139
+ cur = cur.sibling;
140
+ }
141
+ }
142
+ for (var r = 0; r < roots.length; r++) { walkFibers(roots[r].current, 0); }
143
+ if (hostFibers.length === 0) return resolve({ stacks: [], error: 'no-host-fibers' });
144
+
145
+ var measurements = new Array(hostFibers.length).fill(null);
146
+ var pending = hostFibers.length;
147
+ var settled = false;
148
+
149
+ function collect() {
150
+ if (settled) return;
151
+ settled = true;
152
+
153
+ var targetX = ${x};
154
+ var targetY = ${y};
155
+ var hits = [];
156
+ for (var i = 0; i < measurements.length; i++) {
157
+ var m = measurements[i];
158
+ if (m && m.width > 0 && m.height > 0 &&
159
+ targetX >= m.x && targetX <= m.x + m.width &&
160
+ targetY >= m.y && targetY <= m.y + m.height) {
161
+ hits.push({ fiber: hostFibers[i], area: m.width * m.height });
162
+ }
163
+ }
164
+ if (hits.length === 0) return resolve({ stacks: [], error: 'no-hit' });
165
+ hits.sort(function(a, b) { return a.area - b.area; });
166
+
167
+ var stacks = [];
168
+ var node = hits[0].fiber;
169
+ while (node && stacks.length < maxAncestors) {
170
+ var name = null;
171
+ if (node.type) {
172
+ name = typeof node.type === 'string'
173
+ ? node.type
174
+ : (node.type.displayName || node.type.name);
175
+ }
176
+ if (name) {
177
+ var src = node._debugSource;
178
+ if (src && src.fileName) {
179
+ stacks.push({
180
+ component: name,
181
+ stack: '',
182
+ file: src.fileName,
183
+ lineNumber: src.lineNumber,
184
+ column: src.columnNumber
185
+ });
186
+ } else if (node._debugStack && node._debugStack.stack) {
187
+ stacks.push({ component: name, stack: String(node._debugStack.stack) });
188
+ }
189
+ }
190
+ node = node.return;
191
+ }
192
+ resolve({ stacks: stacks });
193
+ }
194
+
195
+ hostFibers.forEach(function(fiber, i) {
196
+ try {
197
+ getMeasurable(fiber).measureInWindow(function(fx, fy, fw, fh) {
198
+ measurements[i] = { x: fx, y: fy, width: fw, height: fh };
199
+ if (--pending === 0) collect();
200
+ });
201
+ } catch (e) {
202
+ if (--pending === 0) collect();
203
+ }
204
+ });
205
+
206
+ setTimeout(collect, 300);
207
+ })
208
+ `;
209
+ }
210
+ /**
211
+ * Frame index 1 of a parsed _debugStack is the component's render site - the
212
+ * JSX call site in its parent, which is the line a developer edits. Index 0 is
213
+ * the Error construction inside React's jsx runtime.
214
+ */
215
+ const RENDER_SITE_FRAME_INDEX = 1;
216
+ export async function resolveStacksToSource(stacks) {
217
+ if (stacks.length === 0) {
218
+ return { source: null, ancestors: [], sourceUnavailable: "no-debug-stack" };
219
+ }
220
+ // React < 19 path: _debugSource already carries source coordinates.
221
+ const direct = [];
222
+ const needsSymbolication = [];
223
+ for (const entry of stacks) {
224
+ if (entry.file && entry.lineNumber !== undefined) {
225
+ direct.push({ component: entry.component, file: entry.file, line: entry.lineNumber });
226
+ continue;
227
+ }
228
+ const frames = parseStackString(entry.stack, RENDER_SITE_FRAME_INDEX + 1);
229
+ const renderSite = frames[RENDER_SITE_FRAME_INDEX];
230
+ if (renderSite) {
231
+ needsSymbolication.push({ component: entry.component, frame: renderSite });
232
+ }
233
+ }
234
+ if (needsSymbolication.length === 0) {
235
+ if (direct.length === 0) {
236
+ return { source: null, ancestors: [], sourceUnavailable: "no-debug-stack" };
237
+ }
238
+ const first = direct[0];
239
+ const firstEntry = stacks.find((s) => s.component === first.component);
240
+ return {
241
+ source: { file: first.file, line: first.line, column: firstEntry?.column ?? 0 },
242
+ ancestors: direct,
243
+ };
244
+ }
245
+ const resolved = await symbolicateFrames(needsSymbolication.map((n) => n.frame));
246
+ if (resolved === null) {
247
+ return { source: null, ancestors: [], sourceUnavailable: "symbolicate-unreachable" };
248
+ }
249
+ const ancestors = [...direct];
250
+ let best = null;
251
+ resolved.forEach((frame, i) => {
252
+ const component = needsSymbolication[i]?.component;
253
+ if (!component || frame.collapse)
254
+ return;
255
+ ancestors.push({ component, file: frame.file, line: frame.lineNumber });
256
+ if (!best) {
257
+ best = { file: frame.file, line: frame.lineNumber, column: frame.column };
258
+ }
259
+ });
260
+ if (!best && direct.length > 0) {
261
+ best = { file: direct[0].file, line: direct[0].line, column: 0 };
262
+ }
263
+ if (!best) {
264
+ return { source: null, ancestors: [], sourceUnavailable: "library-only" };
265
+ }
266
+ return { source: best, ancestors };
267
+ }
268
+ //# sourceMappingURL=componentSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"componentSource.js","sourceRoot":"","sources":["../../src/core/componentSource.ts"],"names":[],"mappings":"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"}
@@ -55,6 +55,36 @@ export declare function getConnectedAppBySimulatorUdid(udid: string): ConnectedA
55
55
  * multiple Android apps are connected.
56
56
  */
57
57
  export declare function getConnectedAppByAndroidDeviceId(deviceId?: string): ConnectedApp | null;
58
+ /**
59
+ * Outcome of matching a caller-supplied `device` string against the currently
60
+ * connected apps. Split out of getConnectedAppByDevice so callers that can
61
+ * *recover* from a miss (reload_app auto-connects, then re-resolves) can
62
+ * inspect the failure instead of catching a thrown UserInputError — a throw
63
+ * on the first resolve is what made reload_app skip its own auto-connect path
64
+ * whenever a device argument was supplied.
65
+ */
66
+ export type DeviceResolution = {
67
+ kind: "ok";
68
+ app: ConnectedApp;
69
+ } | {
70
+ kind: "ambiguous";
71
+ device: string;
72
+ matches: ConnectedApp[];
73
+ } | {
74
+ kind: "none";
75
+ device?: string;
76
+ connected: ConnectedApp[];
77
+ };
78
+ /**
79
+ * Human-facing explanation for a `kind: "none"` / `kind: "ambiguous"` resolution.
80
+ * Kept next to the matcher so the message always reflects the matching rules.
81
+ */
82
+ export declare function describeDeviceResolution(resolution: DeviceResolution): string;
83
+ /**
84
+ * Non-throwing device matcher. Prefer this in call sites that can retry
85
+ * (auto-connect, rescan); use getConnectedAppByDevice when a miss is terminal.
86
+ */
87
+ export declare function resolveConnectedAppByDevice(device?: string): DeviceResolution;
58
88
  export declare function getConnectedAppByDevice(device?: string): ConnectedApp | null;
59
89
  export declare function hasConnectedApp(): boolean;
60
90
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAoB,YAAY,EAAkB,cAAc,EAAsB,sBAAsB,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAkH1M;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED;;GAEG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;AAYD;;;;;;;GAOG;AACH,wBAAgB,iCAAiC,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CA6DnG;AAaD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CACzC,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,GAC9C,MAAM,EAAE,CAoCV;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA4B/D;AA2RD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,SAAS,GAAG,IAAI,CAoW3G;AAGD,wBAAsB,eAAe,CACjC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC7B,OAAO,CAAC,MAAM,CAAC,CAoTjB;AA8FD,wBAAgB,gBAAgB,IAAI,KAAK,CAAC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,YAAY,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACxB,CAAC,CAMD;AAGD,wBAAgB,oBAAoB,IAAI,YAAY,GAAG,IAAI,CAW1D;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAWhF;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuBvF;AAYD,wBAAgB,uBAAuB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAmD5E;AAGD,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CA4C7E;AAqCD,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsErF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5D;AAUD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAmLvC;AAED,MAAM,WAAW,uBAAuB;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,IAAI,GAAG,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,gBAAgB,CAAC;CACvF;AAED,wBAAgB,0BAA0B,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,uBAAuB,CAmDzF;AAED,wBAAsB,wBAAwB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAgD9F"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import WebSocket from "ws";
2
- import { connectedApps, pendingExecutions, getNextMessageId, getLogBuffer, getNetworkBuffer, setActiveSimulatorUdid, clearActiveSimulatorIfSource, updateLastCDPMessageTime, getLastCDPMessageTime, clearLastCDPMessageTime } from "./state.js";
2
+ import { connectedApps, pendingExecutions, failPendingExecutionsForSocket, getNextMessageId, getLogBuffer, getNetworkBuffer, setActiveSimulatorUdid, clearActiveSimulatorIfSource, updateLastCDPMessageTime, getLastCDPMessageTime, clearLastCDPMessageTime } from "./state.js";
3
3
  import { mapConsoleType } from "./logs.js";
4
4
  import { injectNetworkInterceptor, sendNetworkEnable, isInterceptorEvent, applyInterceptedEvent } from "./networkInterceptor.js";
5
5
  import { findSimulatorByName } from "./ios.js";
@@ -9,6 +9,7 @@ import { probeCdpAlive } from "./probe.js";
9
9
  import { UserInputError } from "./errors.js";
10
10
  import { scheduleAppDetection } from "./appDetection.js";
11
11
  import { markConnectionEstablished } from "./jsExecute.js";
12
+ import { startSelectionPoller } from "./selectionPoller.js";
12
13
  import { DEFAULT_RECONNECTION_CONFIG, MIN_STABLE_CONNECTION_MS, initConnectionState, updateConnectionState, getConnectionState, recordConnectionGap, closeConnectionGap, saveConnectionMetadata, clearConnectionMetadata, getConnectionMetadata, saveReconnectionTimer, cancelReconnectionTimer, calculateBackoffDelay, initContextHealth, markContextHealthy, markContextStale, getContextHealth, updateContextHealth, formatDuration, recordConnectionEvent, } from "./connectionState.js";
13
14
  // Connection locks to prevent concurrent connection attempts to the same device
14
15
  const connectionLocks = new Set();
@@ -877,6 +878,14 @@ export async function connectToDevice(device, port, options = {}) {
877
878
  clearInterval(pingInterval);
878
879
  pingInterval = null;
879
880
  }
881
+ // Fail anything still in flight on this socket. Otherwise each
882
+ // call parks until its own timeoutMs and then reports a generic
883
+ // "Expression took too long", which classifies as logical and
884
+ // never triggers auto-reconnect.
885
+ const failedCount = failPendingExecutionsForSocket(ws, "WebSocket connection is not open.");
886
+ if (failedCount > 0) {
887
+ console.error(`[execbro] Failed ${failedCount} in-flight call(s) on closed socket for ${device.title}`);
888
+ }
880
889
  // Release connection lock if still held
881
890
  connectionLocks.delete(appKey);
882
891
  connectedApps.delete(appKey);
@@ -954,6 +963,9 @@ export async function connectToDevice(device, port, options = {}) {
954
963
  connectionLocks.delete(appKey);
955
964
  connectedApps.set(appKey, { ws, deviceInfo: device, port, platform: "android" });
956
965
  markConnectionEstablished();
966
+ // Watch RN's Element Inspector for selections made outside an agent
967
+ // request. Idempotent, and a no-op when EXECBRO_DISABLE_SELECTION_POLL=1.
968
+ startSelectionPoller(device.deviceName);
957
969
  // Initialize or update connection state
958
970
  // Note: We do NOT reset reconnectionAttempts here - that happens
959
971
  // only when connection has been stable for MIN_STABLE_CONNECTION_MS
@@ -1227,27 +1239,85 @@ function normalizeDeviceId(value) {
1227
1239
  return "";
1228
1240
  return value.toLowerCase().replace(/[\s_\-]+/g, "");
1229
1241
  }
1230
- export function getConnectedAppByDevice(device) {
1231
- if (!device) {
1232
- return getFirstConnectedApp();
1242
+ /** Guess which platform a caller-supplied device string refers to. */
1243
+ function guessRequestedPlatform(device) {
1244
+ const lower = device.toLowerCase();
1245
+ // Simulator UDIDs are uppercase UUIDs; adb serials are not.
1246
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(lower))
1247
+ return "ios";
1248
+ if (/iphone|ipad|ipod|\bios\b|simulator/.test(lower))
1249
+ return "ios";
1250
+ if (/emulator-|sdk_gphone|pixel|galaxy|nexus|\bsm-|moto|xiaomi|redmi|oneplus|android|api \d+/.test(lower))
1251
+ return "android";
1252
+ return null;
1253
+ }
1254
+ function deviceLabel(app) {
1255
+ return app.deviceInfo.deviceName || app.deviceInfo.title || "";
1256
+ }
1257
+ /**
1258
+ * Human-facing explanation for a `kind: "none"` / `kind: "ambiguous"` resolution.
1259
+ * Kept next to the matcher so the message always reflects the matching rules.
1260
+ */
1261
+ export function describeDeviceResolution(resolution) {
1262
+ if (resolution.kind === "ok")
1263
+ return "";
1264
+ if (resolution.kind === "ambiguous") {
1265
+ const names = resolution.matches.map(deviceLabel).join(", ");
1266
+ return `Multiple devices match "${resolution.device}": ${names}. Be more specific (use the full device name for an exact match).`;
1233
1267
  }
1234
- const lowerDevice = device.toLowerCase();
1235
- const normDevice = normalizeDeviceId(device);
1236
- const matches = [];
1237
- const allOpenNames = [];
1268
+ const { device, connected } = resolution;
1269
+ if (!device) {
1270
+ return "No apps connected. Run scan_metro to discover and connect to Metro servers.";
1271
+ }
1272
+ if (connected.length === 0) {
1273
+ return `No connected device matches "${device}". No devices are currently connected — run scan_metro to discover and connect to Metro servers.`;
1274
+ }
1275
+ const quoted = connected.map(a => `"${deviceLabel(a)}"`).join(", ");
1276
+ // Cross-platform mismatch is the dominant shape of this failure: the agent
1277
+ // passes a name from list_ios_simulators / adb devices while only the other
1278
+ // platform is attached to Metro. Saying so beats "retry with one of these".
1279
+ const requested = guessRequestedPlatform(device);
1280
+ const attachedPlatforms = new Set(connected.map(a => a.platform));
1281
+ if (requested && !attachedPlatforms.has(requested)) {
1282
+ const other = requested === "ios" ? "Android" : "iOS";
1283
+ const launchHint = requested === "ios"
1284
+ ? "ios_launch_app (boot the simulator first if needed)"
1285
+ : "android_launch_app";
1286
+ return [
1287
+ `No connected device matches "${device}". Only ${other} device(s) are attached to Metro: ${quoted}.`,
1288
+ `The ${requested === "ios" ? "iOS" : "Android"} app is not connected — start it with ${launchHint}, then run scan_metro.`,
1289
+ `Or omit the device argument / use get_apps to target an already-connected device.`
1290
+ ].join(" ");
1291
+ }
1292
+ return `No connected device matches "${device}". Connected devices: ${quoted}. Retry with one of these names (substring match, case-insensitive), or run get_apps to list them.`;
1293
+ }
1294
+ /**
1295
+ * Non-throwing device matcher. Prefer this in call sites that can retry
1296
+ * (auto-connect, rescan); use getConnectedAppByDevice when a miss is terminal.
1297
+ */
1298
+ export function resolveConnectedAppByDevice(device) {
1299
+ const openApps = [];
1238
1300
  for (const [key, app] of connectedApps.entries()) {
1239
1301
  if (app.ws.readyState !== WebSocket.OPEN) {
1240
1302
  connectedApps.delete(key);
1241
1303
  continue;
1242
1304
  }
1243
- const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "";
1244
- allOpenNames.push(deviceName);
1305
+ openApps.push(app);
1306
+ }
1307
+ if (!device) {
1308
+ const app = getFirstConnectedApp();
1309
+ return app ? { kind: "ok", app } : { kind: "none", connected: openApps };
1310
+ }
1311
+ const lowerDevice = device.toLowerCase();
1312
+ const normDevice = normalizeDeviceId(device);
1313
+ const matches = [];
1314
+ for (const app of openApps) {
1245
1315
  // Match against deviceName + the underlying hardware identifiers, with
1246
1316
  // separator-insensitive normalization so "SM_A356N" finds
1247
1317
  // "SM-A356N - 15 - API 35" and "emulator-5554" finds the Android app
1248
1318
  // attached to that serial.
1249
1319
  const haystacks = [
1250
- normalizeDeviceId(deviceName),
1320
+ normalizeDeviceId(deviceLabel(app)),
1251
1321
  normalizeDeviceId(app.simulatorUdid),
1252
1322
  normalizeDeviceId(app.adbSerial)
1253
1323
  ].filter((s) => s.length > 0);
@@ -1257,24 +1327,27 @@ export function getConnectedAppByDevice(device) {
1257
1327
  }
1258
1328
  // Prefer exact (case-insensitive) match when present — disambiguates
1259
1329
  // "iPhone 17 Pro" from "iPhone 17 Pro Max" when both are connected.
1260
- const exact = matches.find(a => {
1261
- const name = a.deviceInfo.deviceName || a.deviceInfo.title || "";
1262
- return name.toLowerCase() === lowerDevice;
1263
- });
1330
+ const exact = matches.find(a => deviceLabel(a).toLowerCase() === lowerDevice);
1264
1331
  if (exact)
1265
- return exact;
1266
- if (matches.length === 1) {
1267
- return matches[0];
1268
- }
1269
- if (matches.length > 1) {
1270
- const names = matches.map(a => a.deviceInfo.deviceName || a.deviceInfo.title).join(", ");
1271
- throw new UserInputError(`Multiple devices match "${device}": ${names}. Be more specific (use the full device name for an exact match).`);
1272
- }
1273
- if (allOpenNames.length === 0) {
1274
- throw new UserInputError(`No connected device matches "${device}". No devices are currently connected — run scan_metro to discover and connect to Metro servers.`);
1275
- }
1276
- const quoted = allOpenNames.map(n => `"${n}"`).join(", ");
1277
- throw new UserInputError(`No connected device matches "${device}". Connected devices: ${quoted}. Retry with one of these names (substring match, case-insensitive).`);
1332
+ return { kind: "ok", app: exact };
1333
+ if (matches.length === 1)
1334
+ return { kind: "ok", app: matches[0] };
1335
+ if (matches.length > 1)
1336
+ return { kind: "ambiguous", device, matches };
1337
+ return { kind: "none", device, connected: openApps };
1338
+ }
1339
+ export function getConnectedAppByDevice(device) {
1340
+ const resolution = resolveConnectedAppByDevice(device);
1341
+ if (resolution.kind === "ok")
1342
+ return resolution.app;
1343
+ // No device argument: a miss just means "nothing connected" — callers
1344
+ // handle null themselves (historically getFirstConnectedApp's contract).
1345
+ if (!device)
1346
+ return null;
1347
+ const context = resolution.kind === "ambiguous"
1348
+ ? "ambiguous_device"
1349
+ : resolution.connected.length > 0 ? "device_mismatch" : "no_devices_connected";
1350
+ throw new UserInputError(describeDeviceResolution(resolution), context);
1278
1351
  }
1279
1352
  // Check if any app is connected with an OPEN WebSocket
1280
1353
  export function hasConnectedApp() {