sootsim 0.1.111 → 0.1.113

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 (146) hide show
  1. package/dist-cli/bin.js +3 -3
  2. package/dist-cli/chunks/{agent-ZXHGIPSY.js → agent-3JTU2UL4.js} +2 -2
  3. package/dist-cli/chunks/{agent-wrapper-5EACYKWZ.js → agent-wrapper-CR7GJ7FP.js} +2 -2
  4. package/dist-cli/chunks/{assert-LDEGLMEF.js → assert-N2OG5JQ3.js} +2 -2
  5. package/dist-cli/chunks/auto-bootstrap-RUMSH2SU.js +2 -0
  6. package/dist-cli/chunks/beta-WAJQAPBI.js +2 -0
  7. package/dist-cli/chunks/{chunk-53MJ3O5G.js → chunk-2OGV4GCY.js} +1 -1
  8. package/dist-cli/chunks/{chunk-SKLP2DGB.js → chunk-2SXHMBEC.js} +1 -1
  9. package/dist-cli/chunks/{chunk-JMD73BNN.js → chunk-3EQEJK4E.js} +2 -2
  10. package/dist-cli/chunks/chunk-3J3AQWIW.js +1 -0
  11. package/dist-cli/chunks/{chunk-67OJIOOH.js → chunk-3MBITUPW.js} +1 -1
  12. package/dist-cli/chunks/{chunk-ENWSR3JU.js → chunk-3OUCFCUV.js} +2 -2
  13. package/dist-cli/chunks/{chunk-4PBNEPVZ.js → chunk-4IA3F7NN.js} +2 -2
  14. package/dist-cli/chunks/{chunk-UVFLQ6P3.js → chunk-4K22TYNZ.js} +2 -2
  15. package/dist-cli/chunks/{chunk-GSIYERHY.js → chunk-5ALMZP2C.js} +7 -7
  16. package/dist-cli/chunks/{chunk-P2CHBVZW.js → chunk-5QY6UIVH.js} +2 -2
  17. package/dist-cli/chunks/{chunk-DR65C56B.js → chunk-73HWAXE2.js} +2 -2
  18. package/dist-cli/chunks/{chunk-XXHWZFQP.js → chunk-7HFQ2NWW.js} +2 -2
  19. package/dist-cli/chunks/{chunk-A25NMNNP.js → chunk-AC7WDB6V.js} +1 -1
  20. package/dist-cli/chunks/{chunk-QW3UTSSW.js → chunk-BKUVXSRQ.js} +1 -1
  21. package/dist-cli/chunks/{chunk-TVV3D6FW.js → chunk-D43UWL27.js} +2 -2
  22. package/dist-cli/chunks/{chunk-S4HNANRQ.js → chunk-D5KQSLTO.js} +2 -2
  23. package/dist-cli/chunks/{chunk-JYPK3NGB.js → chunk-E3H5ZULL.js} +1 -1
  24. package/dist-cli/chunks/{chunk-3SAP2ISO.js → chunk-EBWNCJET.js} +2 -2
  25. package/dist-cli/chunks/{chunk-SIB5KIJ4.js → chunk-FXSPK6L3.js} +1 -1
  26. package/dist-cli/chunks/{chunk-YU6GZ5NB.js → chunk-H7MKPGGV.js} +3 -3
  27. package/dist-cli/chunks/{chunk-AQQWYSLA.js → chunk-IZYZPIRZ.js} +2 -2
  28. package/dist-cli/chunks/chunk-K24H4XOV.js +1 -0
  29. package/dist-cli/chunks/{chunk-S7L3E2YD.js → chunk-K5LENBS5.js} +1 -1
  30. package/dist-cli/chunks/{chunk-G2SLRKMB.js → chunk-L76VLJIZ.js} +2 -2
  31. package/dist-cli/chunks/{chunk-BG6OWTOI.js → chunk-LQTTCT5C.js} +1 -1
  32. package/dist-cli/chunks/{chunk-EDSFZIXQ.js → chunk-LW3KJUVL.js} +1 -1
  33. package/dist-cli/chunks/{chunk-CMEVVRAR.js → chunk-MWQUK6QO.js} +3 -3
  34. package/dist-cli/chunks/{chunk-PQZFDY2A.js → chunk-MYNBI2ND.js} +1 -1
  35. package/dist-cli/chunks/{chunk-2D66SUNA.js → chunk-NIUQAJCI.js} +2 -2
  36. package/dist-cli/chunks/{chunk-N4JCA64R.js → chunk-OIWS3EXW.js} +2 -2
  37. package/dist-cli/chunks/{chunk-77CHXDYW.js → chunk-RMJMYLLR.js} +2 -2
  38. package/dist-cli/chunks/{chunk-LRXVXOBZ.js → chunk-RURKHOVS.js} +1 -1
  39. package/dist-cli/chunks/{chunk-EVLFOAYO.js → chunk-UUFG4HVZ.js} +1 -1
  40. package/dist-cli/chunks/chunk-VX6D2AI3.js +1 -0
  41. package/dist-cli/chunks/{chunk-YD5QHV5V.js → chunk-WGJRZVLO.js} +1 -1
  42. package/dist-cli/chunks/{chunk-754NJLTU.js → chunk-WOKSYJIY.js} +2 -2
  43. package/dist-cli/chunks/{chunk-GJNF7NDM.js → chunk-WSGVQEXB.js} +3 -3
  44. package/dist-cli/chunks/{chunk-C2YM755N.js → chunk-XK275OXC.js} +1 -1
  45. package/dist-cli/chunks/{chunk-2F23CQGP.js → chunk-XKKCAC3J.js} +2 -2
  46. package/dist-cli/chunks/{chunk-GQLGVNOO.js → chunk-XODSEUOW.js} +2 -2
  47. package/dist-cli/chunks/{chunk-6O7ZPHMU.js → chunk-XU6LOLAB.js} +2 -2
  48. package/dist-cli/chunks/{chunk-IBCFP7H3.js → chunk-XXORXONW.js} +2 -2
  49. package/dist-cli/chunks/chunk-YHK6RX3N.js +2 -0
  50. package/dist-cli/chunks/{chunk-FLW46VDB.js → chunk-ZVE6F53R.js} +2 -2
  51. package/dist-cli/chunks/cli-version-2F4UFHUR.js +2 -0
  52. package/dist-cli/chunks/{compat-M5B7FWA5.js → compat-F6VRDJEA.js} +3 -3
  53. package/dist-cli/chunks/{config-HEZJL2XE.js → config-K2VVAYH6.js} +2 -2
  54. package/dist-cli/chunks/control-UCTK6SGP.js +2 -0
  55. package/dist-cli/chunks/{cpu-profile-X3JC344V.js → cpu-profile-6SQKDUCL.js} +2 -2
  56. package/dist-cli/chunks/{daemon-NGNG4RSE.js → daemon-AWSYWILR.js} +2 -2
  57. package/dist-cli/chunks/{debug-GEQPT5ZQ.js → debug-PX35BAY4.js} +3 -3
  58. package/dist-cli/chunks/{detox-YWFH3NLO.js → detox-5SPMUGEX.js} +2 -2
  59. package/dist-cli/chunks/{device-CBX6IAYH.js → device-ACGNWZ3G.js} +2 -2
  60. package/dist-cli/chunks/{diagnose-ZLUHR42I.js → diagnose-V2VT4ASE.js} +2 -2
  61. package/dist-cli/chunks/drivers-G4ZQAYTR.js +2 -0
  62. package/dist-cli/chunks/{electron-ZLRMHUYW.js → electron-Y6E5R4SF.js} +3 -3
  63. package/dist-cli/chunks/flow-I642RDRA.js +2 -0
  64. package/dist-cli/chunks/help-BPNT2YES.js +2 -0
  65. package/dist-cli/chunks/{hints-PHI7CRGD.js → hints-2V7ZHG6L.js} +2 -2
  66. package/dist-cli/chunks/{home-paths-Z73CLQBV.js → home-paths-MVPKADD6.js} +2 -2
  67. package/dist-cli/chunks/inspect-DNRAUTAB.js +974 -0
  68. package/dist-cli/chunks/install-BMWYQTOZ.js +2 -0
  69. package/dist-cli/chunks/{install-desktop-GYU74OUP.js → install-desktop-CYJDYA53.js} +3 -3
  70. package/dist-cli/chunks/{keys-VL6BSLHF.js → keys-FUYSXI2L.js} +2 -2
  71. package/dist-cli/chunks/{launch-YQYFQBHR.js → launch-PQ6QTEP7.js} +3 -3
  72. package/dist-cli/chunks/{login-TPOBFENS.js → login-BMFJHJ3B.js} +4 -4
  73. package/dist-cli/chunks/{logout-XYTNNS2V.js → logout-PG2JFWQL.js} +2 -2
  74. package/dist-cli/chunks/{maestro-FQYRGDX7.js → maestro-2QFWLFF4.js} +2 -2
  75. package/dist-cli/chunks/{preview-L743LEXV.js → preview-L6YAPMSD.js} +2 -2
  76. package/dist-cli/chunks/{profile-7XRSHEEP.js → profile-2QQGLQBN.js} +2 -2
  77. package/dist-cli/chunks/{react-VD7WKMQT.js → react-ZKAMEZ6H.js} +3 -3
  78. package/dist-cli/chunks/{record-WJ7NBGQL.js → record-XZNFFK4K.js} +2 -2
  79. package/dist-cli/chunks/runtime-4X66LQCP.js +2 -0
  80. package/dist-cli/chunks/{runtime-delivery-VPDXWAX4.js → runtime-delivery-R5E2YPXP.js} +2 -2
  81. package/dist-cli/chunks/{screenshot-IQZRQQA5.js → screenshot-5BDZO36S.js} +2 -2
  82. package/dist-cli/chunks/{screenshot-mode-A6SXP6ZJ.js → screenshot-mode-OUMQT4EP.js} +2 -2
  83. package/dist-cli/chunks/{screenshots-X5KKZTAP.js → screenshots-RTIXDSFS.js} +2 -2
  84. package/dist-cli/chunks/{server-7I7V4H3O.js → server-ZF2XUVP7.js} +2 -2
  85. package/dist-cli/chunks/setup-repo-BBC3QXE7.js +2 -0
  86. package/dist-cli/chunks/{skills-WSSHVDJO.js → skills-YN2QLIXK.js} +2 -2
  87. package/dist-cli/chunks/{start-GS4VLBF6.js → start-5HZPS3B6.js} +4 -4
  88. package/dist-cli/chunks/store-JJAGDHAI.js +2 -0
  89. package/dist-cli/chunks/telemetry-MRP2ISHT.js +2 -0
  90. package/dist-cli/chunks/{test-K53D2IP2.js → test-RQWNRURE.js} +3 -3
  91. package/dist-cli/chunks/{three-mode-GH7JMUUR.js → three-mode-STWOAVTT.js} +2 -2
  92. package/dist-cli/chunks/{timeline-NRBWDOXR.js → timeline-NZS6LDPR.js} +2 -2
  93. package/dist-cli/chunks/{upgrade-YKNCFMWK.js → upgrade-PM4RZ7CD.js} +2 -2
  94. package/dist-cli/chunks/upload-LLA3GT6U.js +2 -0
  95. package/dist-cli/chunks/{web-MZGLRPIR.js → web-YQE55355.js} +2 -2
  96. package/dist-cli/chunks/{what-happened-XYUFUXYC.js → what-happened-4HEXIQ23.js} +2 -2
  97. package/dist-cli/chunks/{whoami-ZHYQ3OGI.js → whoami-RZVBLNXN.js} +2 -2
  98. package/dist-lib/agent-daemon-client.cjs +1 -1
  99. package/dist-lib/agent-events.cjs +1 -1
  100. package/dist-lib/agent-sessions.cjs +1 -1
  101. package/dist-lib/attached-projects.cjs +1 -1
  102. package/dist-lib/auth/shared-session.cjs +1 -1
  103. package/dist-lib/backend-origin.cjs +1 -1
  104. package/dist-lib/beta.cjs +1 -1
  105. package/dist-lib/beta.mjs +1 -1
  106. package/dist-lib/bridge-constants.cjs +1 -1
  107. package/dist-lib/cli-constants.cjs +1 -1
  108. package/dist-lib/config.cjs +1 -1
  109. package/dist-lib/detox/index.cjs +1 -1
  110. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  111. package/dist-lib/home-paths.cjs +1 -1
  112. package/dist-lib/host/bridge-host.cjs +1 -1
  113. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  114. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  115. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  116. package/dist-lib/host/websocket-proxy.cjs +1 -1
  117. package/dist-lib/index.cjs +1 -1
  118. package/dist-lib/metro.cjs +1 -1
  119. package/dist-lib/profiles.cjs +1 -1
  120. package/dist-lib/render-mode.cjs +1 -1
  121. package/dist-lib/scripts/demo-app-registry.cjs +1 -1
  122. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  123. package/dist-lib/sdk.cjs +1440 -0
  124. package/dist-lib/sdk.mjs +1360 -0
  125. package/dist-lib/skills.cjs +1 -1
  126. package/dist-lib/vite.cjs +1 -1
  127. package/package.json +8 -2
  128. package/src/sdk.ts +7 -0
  129. package/dist-cli/chunks/auto-bootstrap-OMA34BND.js +0 -2
  130. package/dist-cli/chunks/beta-OP6TYKKB.js +0 -2
  131. package/dist-cli/chunks/chunk-HKLM27ZP.js +0 -2
  132. package/dist-cli/chunks/chunk-MGSBDS2H.js +0 -1
  133. package/dist-cli/chunks/chunk-PTS25DZY.js +0 -1
  134. package/dist-cli/chunks/chunk-QH6IAFTO.js +0 -1
  135. package/dist-cli/chunks/cli-version-CBQ3OVJZ.js +0 -2
  136. package/dist-cli/chunks/control-SE4RPPWW.js +0 -2
  137. package/dist-cli/chunks/drivers-G5RZDGGI.js +0 -2
  138. package/dist-cli/chunks/flow-2JI67ETK.js +0 -2
  139. package/dist-cli/chunks/help-MXGX7BJE.js +0 -2
  140. package/dist-cli/chunks/inspect-N4AXJMZ4.js +0 -980
  141. package/dist-cli/chunks/install-FWXT4NDF.js +0 -2
  142. package/dist-cli/chunks/runtime-GNVSGKXO.js +0 -2
  143. package/dist-cli/chunks/setup-repo-Y4AUXZJI.js +0 -2
  144. package/dist-cli/chunks/store-YI2AYLU5.js +0 -2
  145. package/dist-cli/chunks/telemetry-DNQFR4IP.js +0 -2
  146. package/dist-cli/chunks/upload-IZSNPSBB.js +0 -2
@@ -0,0 +1,1360 @@
1
+ /*! sootsim v0.1.113 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
+
3
+ // cli/commands/inspect/core.ts
4
+ function resolveMaxMsFlag(args, fallbackMs) {
5
+ const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
6
+ for (const flag of aliases) {
7
+ const i = args.indexOf(flag);
8
+ if (i >= 0 && args[i + 1]) {
9
+ const n = Number(args[i + 1]);
10
+ if (Number.isFinite(n)) return Math.max(100, n);
11
+ }
12
+ }
13
+ return fallbackMs;
14
+ }
15
+ var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
16
+ "tap",
17
+ "double-tap",
18
+ "tap-text",
19
+ "tap-id",
20
+ "long-press",
21
+ "touch"
22
+ ]);
23
+ function isInspectPickCommand(subcommand) {
24
+ return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
25
+ }
26
+ async function isInspectModeActive(bridge) {
27
+ const active = await bridge.send({
28
+ type: "evaluate",
29
+ code: "window.__sootsimEngineState?.inspectActive === true"
30
+ });
31
+ return active === true;
32
+ }
33
+ async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
34
+ if (!isInspectPickCommand(subcommand)) return false;
35
+ try {
36
+ return await isInspectModeActive(bridge);
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function inspectNodeCount(bridge) {
42
+ const count = await bridge.send({
43
+ type: "evaluate",
44
+ code: "(async () => await window.__sootsimTest.getNodeCount())()"
45
+ });
46
+ return { nodes: typeof count === "number" ? count : 0 };
47
+ }
48
+ async function inspectTree(bridge, depth = 5) {
49
+ const tree = await bridge.send({
50
+ type: "evaluate",
51
+ code: `(async () => await window.__sootsimTest.dumpTree(${depth}))()`
52
+ });
53
+ return { depth, tree };
54
+ }
55
+ async function inspectUrl(bridge) {
56
+ const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
57
+ return { url: typeof url === "string" ? url : "" };
58
+ }
59
+ async function inspectDescribe(bridge, dumpOpts) {
60
+ const code = `(async () => {
61
+ const t = window.__sootsimTest
62
+ const mainShell = window.SootSim?.bridges?.mainShell
63
+ const kb = window.__sootsimKeyboard
64
+ if (!t) return { error: 'no test bridge' }
65
+
66
+ let shell = null
67
+ try {
68
+ shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
69
+ } catch {}
70
+
71
+ const tree = await t.dumpTree(12, ${JSON.stringify(dumpOpts)})
72
+ const nodeCount = (await t.getNodeCount?.()) || 0
73
+ const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
74
+ return { tree, shell, nodeCount, keyboard }
75
+ })()`;
76
+ const result = await bridge.send({ type: "evaluate", code });
77
+ return result ?? {};
78
+ }
79
+ var INSPECT_A11Y_CODE = `(async () => {
80
+ const t = window.__sootsimTest
81
+ if (!t) return []
82
+
83
+ const readLayout = (n) => n?.layout || null
84
+ const readAbs = (n) => n?.absolutePosition || n?.absolute || null
85
+ const readRole = (n) => n?.accessibilityRole || n?.role || null
86
+ const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
87
+ const readHint = (n) => n?.accessibilityHint || n?.hint || null
88
+ const readState = (n) => n?.accessibilityState || n?.state || null
89
+ const readTestId = (n) => n?.testID || n?.testId || null
90
+ const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
91
+ const readPressable = (n) => {
92
+ if (n?.pressable) return true
93
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
94
+ const role = readRole(n)
95
+ return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
96
+ }
97
+ const isTextInput = (n) => n?.isTextInput === true
98
+ const isTextNode = (n) => readType(n) === 'text'
99
+ const isVisibleTarget = (n) => {
100
+ const layout = readLayout(n)
101
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
102
+ const abs = readAbs(n)
103
+ if (!abs) return true
104
+ const device = n?.device || {}
105
+ const screenW = Number(device.width) || window.innerWidth || 0
106
+ const screenH = Number(device.height) || window.innerHeight || 0
107
+ return (
108
+ abs.x + layout.width > 0 &&
109
+ abs.y + layout.height > 0 &&
110
+ abs.x < screenW &&
111
+ abs.y < screenH
112
+ )
113
+ }
114
+ const hasAccessibleSignal = (n) => {
115
+ const role = readRole(n)
116
+ const label = readLabel(n)
117
+ const hint = readHint(n)
118
+ if (role) return true
119
+ if (hint) return true
120
+ if (isTextInput(n)) return true
121
+ if (readPressable(n)) return true
122
+ if (isTextNode(n) && n?.text) return true
123
+ if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
124
+ return false
125
+ }
126
+ const normalize = (n) => {
127
+ const layout = readLayout(n)
128
+ const abs = readAbs(n)
129
+ const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
130
+ return {
131
+ role,
132
+ label: readLabel(n),
133
+ hint: readHint(n),
134
+ state: readState(n),
135
+ testID: readTestId(n),
136
+ position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
137
+ size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
138
+ }
139
+ }
140
+
141
+ if (typeof t.listInspectable === 'function') {
142
+ const list = await t.listInspectable({})
143
+ if (Array.isArray(list)) {
144
+ return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
145
+ }
146
+ }
147
+
148
+ const all = await t.queryAll({ pruneHidden: true })
149
+ return all
150
+ .filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
151
+ .map(normalize)
152
+ })()`;
153
+ async function inspectAccessibilityTree(bridge) {
154
+ const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
155
+ return Array.isArray(nodes) ? nodes : [];
156
+ }
157
+ var FIND_INSPECTABLE_TARGETS = `
158
+ const fromInspectable = async () => {
159
+ if (typeof t.listInspectable !== 'function') return null
160
+ const list = await t.listInspectable({})
161
+ if (!Array.isArray(list)) return null
162
+ return list.map((n) => {
163
+ const role = n.accessibilityRole ?? n.role
164
+ const label = n.accessibilityLabel ?? n.label
165
+ const layout = n.layout
166
+ const absolutePosition = n.absolutePosition ?? n.absolute
167
+ const screenW = Number(n.device?.width) || window.innerWidth || 0
168
+ const screenH = Number(n.device?.height) || window.innerHeight || 0
169
+ const visibleFrame = layout && absolutePosition
170
+ ? {
171
+ x: Math.max(0, absolutePosition.x),
172
+ y: Math.max(0, absolutePosition.y),
173
+ width: Math.max(
174
+ 0,
175
+ Math.min(screenW, absolutePosition.x + layout.width) -
176
+ Math.max(0, absolutePosition.x),
177
+ ),
178
+ height: Math.max(
179
+ 0,
180
+ Math.min(screenH, absolutePosition.y + layout.height) -
181
+ Math.max(0, absolutePosition.y),
182
+ ),
183
+ }
184
+ : null
185
+ const handlers = Array.isArray(n.handlers) ? n.handlers : []
186
+ const pressable =
187
+ handlers.length > 0 ||
188
+ role === 'button' ||
189
+ role === 'link' ||
190
+ role === 'tab'
191
+ return {
192
+ type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
193
+ id: n.id ?? n.nodeId,
194
+ nodeId: n.nodeId,
195
+ testID: n.testID ?? n.testId,
196
+ text: n.text,
197
+ layout,
198
+ absolutePosition,
199
+ visibleFrame,
200
+ style: n.computedStyle ?? n.style ?? {},
201
+ childCount: n.childCount ?? 0,
202
+ pressed: n.pressed ?? false,
203
+ pressable,
204
+ isTextInput: n.isTextInput ?? false,
205
+ accessible: n.accessible ?? true,
206
+ accessibilityLabel: label,
207
+ accessibilityRole: role,
208
+ accessibilityHint: n.accessibilityHint,
209
+ accessibilityState: n.accessibilityState,
210
+ accessibilityValue: n.accessibilityValue,
211
+ }
212
+ })
213
+ }
214
+ const isVisibleTarget = (n) => {
215
+ if (!n?.layout) return false
216
+ const frame = n.visibleFrame
217
+ if (frame) return frame.width > 0 && frame.height > 0
218
+ const abs = n.absolutePosition
219
+ if (!abs) return n.layout.width > 0 && n.layout.height > 0
220
+ const screenW = window.innerWidth || 0
221
+ const screenH = window.innerHeight || 0
222
+ return (
223
+ abs.x + n.layout.width > 0 &&
224
+ abs.y + n.layout.height > 0 &&
225
+ abs.x < screenW &&
226
+ abs.y < screenH
227
+ )
228
+ }
229
+ `;
230
+ function resolveFindMode(q) {
231
+ if (q.testId) {
232
+ return {
233
+ mode: "testid",
234
+ code: `(async () => {
235
+ const t = window.__sootsimTest
236
+ return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
237
+ })()`
238
+ };
239
+ }
240
+ if (q.role) {
241
+ return {
242
+ mode: "role",
243
+ code: `(async () => await window.__sootsimTest.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true }))()`
244
+ };
245
+ }
246
+ if (q.type) {
247
+ return {
248
+ mode: "type",
249
+ code: `(async () => await window.__sootsimTest.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true }))()`
250
+ };
251
+ }
252
+ if (q.pressable) {
253
+ return {
254
+ mode: "pressable",
255
+ code: `(async () => {
256
+ const t = window.__sootsimTest
257
+ ${FIND_INSPECTABLE_TARGETS}
258
+ const inspectable = await fromInspectable()
259
+ if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
260
+ const all = await t.queryAll({ pruneHidden: true })
261
+ return all.filter(n => n.pressable && isVisibleTarget(n))
262
+ })()`
263
+ };
264
+ }
265
+ if (q.interactive) {
266
+ return {
267
+ mode: "interactive-targets",
268
+ code: `(async () => {
269
+ const t = window.__sootsimTest
270
+ ${FIND_INSPECTABLE_TARGETS}
271
+ const inspectable = await fromInspectable()
272
+ if (inspectable) {
273
+ return inspectable.filter(n => n.pressable && isVisibleTarget(n))
274
+ }
275
+ const all = await t.queryAll({ pruneHidden: true })
276
+ return all.filter(n => n.pressable && isVisibleTarget(n))
277
+ })()`
278
+ };
279
+ }
280
+ if (q.visible) {
281
+ return {
282
+ mode: "visible",
283
+ code: `(async () => {
284
+ const all = await window.__sootsimTest.queryAll({ pruneHidden: true })
285
+ return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
286
+ })()`
287
+ };
288
+ }
289
+ if (q.text) {
290
+ return {
291
+ mode: "text",
292
+ code: `(async () => await window.__sootsimTest.findByText(${JSON.stringify(q.text)}))()`
293
+ };
294
+ }
295
+ return null;
296
+ }
297
+ async function inspectFind(bridge, q) {
298
+ const resolved = resolveFindMode(q);
299
+ if (!resolved) return null;
300
+ const result = await bridge.send({ type: "evaluate", code: resolved.code });
301
+ return { mode: resolved.mode, result };
302
+ }
303
+ function rankInteractive(nodes) {
304
+ return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
305
+ }
306
+ function scoreInteractive(n) {
307
+ let score = 0;
308
+ if (n.testID) score += 100;
309
+ if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
310
+ if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
311
+ score += 30;
312
+ }
313
+ if (n.accessibilityRole) score += 15;
314
+ const w = n.layout?.width ?? 0;
315
+ const h = n.layout?.height ?? 0;
316
+ const area = w * h;
317
+ if (area >= 400 && area <= 6e4) score += 25;
318
+ else if (area > 6e4) score -= 20;
319
+ const y = n.absolutePosition?.y ?? 0;
320
+ if (y < 0) score -= 30;
321
+ return score;
322
+ }
323
+ function tapCommandForNode(n) {
324
+ if (n.testID) return `sootsim do tap-id ${shellEscape(n.testID)}`;
325
+ const text = typeof n.text === "string" ? n.text.trim() : "";
326
+ if (text.length > 0 && text.length <= 80)
327
+ return `sootsim do tap-text ${shellEscape(text)}`;
328
+ const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
329
+ const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
330
+ return `sootsim do tap ${x} ${y}`;
331
+ }
332
+ function shellEscape(value) {
333
+ if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
334
+ return `'${value.replace(/'/g, `'\\''`)}'`;
335
+ }
336
+ var WAIT_READY_PROBE = `(async () => {
337
+ const t = window.__sootsimTest
338
+ let nodes = 0
339
+ try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
340
+ let targets = 0
341
+ let loadingText = ''
342
+ try {
343
+ if (typeof t?.listInspectable === 'function') {
344
+ const list = await t.listInspectable({})
345
+ if (Array.isArray(list)) {
346
+ const screenW = window.innerWidth || 0
347
+ const screenH = window.innerHeight || 0
348
+ const isVisible = (n) => {
349
+ const layout = n?.layout
350
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
351
+ const abs = n?.absolutePosition || n?.absolute
352
+ if (!abs) return true
353
+ const device = n?.device || {}
354
+ const w = Number(device.width) || screenW
355
+ const h = Number(device.height) || screenH
356
+ return (
357
+ abs.x + layout.width > 0 &&
358
+ abs.y + layout.height > 0 &&
359
+ abs.x < w &&
360
+ abs.y < h
361
+ )
362
+ }
363
+ const hasContentSignal = (n) => {
364
+ const role = n?.accessibilityRole || n?.role
365
+ const label = n?.accessibilityLabel || n?.label
366
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
367
+ const text = typeof n?.text === 'string' ? n.text.trim() : ''
368
+ const testID = typeof (n?.testID || n?.testId) === 'string' ? (n.testID || n.testId) : ''
369
+ if (!loadingText && /^(opening app|loading|capturing|connecting)$/i.test(text)) {
370
+ loadingText = text
371
+ }
372
+ if (handlers.length > 0) return true
373
+ if (role) return true
374
+ if (n?.isTextInput) return true
375
+ if (text) return true
376
+ if (label) return true
377
+ if (testID && !/^splash|loading/i.test(testID)) return true
378
+ return false
379
+ }
380
+ targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
381
+ }
382
+ }
383
+ } catch {}
384
+ let externalReady = null
385
+ let externalStatus = ''
386
+ let externalError = ''
387
+ try {
388
+ const getExternalAppState = t?.getExternalAppState
389
+ const external = typeof getExternalAppState === 'function'
390
+ ? await Promise.race([
391
+ getExternalAppState(),
392
+ new Promise((resolve) => setTimeout(
393
+ () => resolve({
394
+ state: {
395
+ ready: false,
396
+ loading: true,
397
+ status: 'waiting for tenant worker...',
398
+ },
399
+ }),
400
+ 1000,
401
+ )),
402
+ ])
403
+ : null
404
+ const state = external?.state
405
+ if (state && typeof state === 'object') {
406
+ externalReady = state.ready === true
407
+ if (typeof state.status === 'string') externalStatus = state.status
408
+ if (typeof state.error === 'string') externalError = state.error
409
+ }
410
+ if (!externalError && Array.isArray(external?.entryErrors)) {
411
+ const entry = external.entryErrors.find((item) => {
412
+ return item && typeof item.message === 'string' && item.message.trim()
413
+ })
414
+ if (entry) externalError = entry.message
415
+ }
416
+ if (externalReady === false && externalStatus) loadingText = externalStatus
417
+ } catch {}
418
+ let errors = 0
419
+ try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
420
+ return {
421
+ flag: (window).__sootsimExternalAppReady,
422
+ at: (window).__sootsimExternalAppReadyAt || 0,
423
+ nodes,
424
+ targets,
425
+ errors,
426
+ loadingText,
427
+ externalReady,
428
+ externalStatus,
429
+ externalError,
430
+ }
431
+ })()`;
432
+ var READY_CONTENT_NODE_FLOOR = 100;
433
+ var READY_NODE_STABLE_MS = 750;
434
+ var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
435
+ function readyProbeHasContent(probe) {
436
+ return probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
437
+ }
438
+ function waitReadyReason(status) {
439
+ return status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
440
+ }
441
+ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
442
+ const start = Date.now();
443
+ const deadline = start + timeoutMs;
444
+ const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
445
+ let nextProgressAt = start + progressIntervalMs;
446
+ let lastNodes = -1;
447
+ let nodeStableSince = start;
448
+ let last = {
449
+ flag: void 0,
450
+ at: 0,
451
+ nodes: 0,
452
+ targets: 0,
453
+ errors: 0,
454
+ loadingText: "",
455
+ externalReady: null,
456
+ externalStatus: "",
457
+ externalError: ""
458
+ };
459
+ while (Date.now() < deadline) {
460
+ try {
461
+ last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
462
+ } catch {
463
+ }
464
+ const now = Date.now();
465
+ const status = {
466
+ ready: false,
467
+ elapsedMs: now - start,
468
+ nodes: last.nodes,
469
+ targets: last.targets,
470
+ flag: last.flag,
471
+ loadingText: last.loadingText,
472
+ externalReady: last.externalReady,
473
+ externalStatus: last.externalStatus,
474
+ externalError: last.externalError,
475
+ errors: last.errors
476
+ };
477
+ if (last.nodes !== lastNodes) {
478
+ lastNodes = last.nodes;
479
+ nodeStableSince = now;
480
+ }
481
+ if (last.flag === true) {
482
+ if (last.externalReady !== false && !last.externalError && !last.loadingText && readyProbeHasContent(last)) {
483
+ if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
484
+ return {
485
+ ...status,
486
+ ready: true
487
+ };
488
+ }
489
+ }
490
+ }
491
+ if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
492
+ options.onProgress(status);
493
+ do {
494
+ nextProgressAt += progressIntervalMs;
495
+ } while (now >= nextProgressAt);
496
+ }
497
+ await new Promise((r) => setTimeout(r, 150));
498
+ }
499
+ return {
500
+ ready: false,
501
+ elapsedMs: Date.now() - start,
502
+ nodes: last.nodes,
503
+ targets: last.targets,
504
+ flag: last.flag,
505
+ loadingText: last.loadingText,
506
+ externalReady: last.externalReady,
507
+ externalStatus: last.externalStatus,
508
+ externalError: last.externalError,
509
+ errors: last.errors
510
+ };
511
+ }
512
+ async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
513
+ const gone = opts.gone === true;
514
+ const result = await bridge.send(
515
+ {
516
+ type: "evaluate",
517
+ code: `(async () => {
518
+ const start = Date.now()
519
+ const deadline = start + ${timeoutMs}
520
+ const gone = ${gone}
521
+ const ID = ${JSON.stringify(testId)}
522
+ const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
523
+ const find = async () => {
524
+ const t = window.__sootsimTest
525
+ if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
526
+ try {
527
+ const matches = await t.queryAll?.({ hasId: ID, pruneHidden: true })
528
+ if (!Array.isArray(matches)) return undefined
529
+ return matches.find(present) || null
530
+ } catch {
531
+ return undefined
532
+ }
533
+ }
534
+ let goneStreak = 0
535
+ while (Date.now() < deadline) {
536
+ const node = await find()
537
+ if (gone) {
538
+ if (node === null) {
539
+ goneStreak += 1
540
+ if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
541
+ } else {
542
+ goneStreak = 0
543
+ }
544
+ } else if (present(node)) {
545
+ return { found: true, node, elapsed: Date.now() - start }
546
+ }
547
+ await new Promise((r) => setTimeout(r, 80))
548
+ }
549
+ return { found: false, elapsed: Date.now() - start }
550
+ })()`
551
+ },
552
+ {
553
+ timeoutMs: timeoutMs + 1e3
554
+ }
555
+ );
556
+ return result ?? { found: false, elapsed: timeoutMs };
557
+ }
558
+ async function inspectErrors(bridge, limit = 20) {
559
+ const result = await bridge.send({
560
+ type: "evaluate",
561
+ code: `window.__sootsimConsole?.getErrors(${limit}) || []`
562
+ });
563
+ return Array.isArray(result) ? result : [];
564
+ }
565
+ async function inspectWarnings(bridge, limit = 20) {
566
+ const result = await bridge.send({
567
+ type: "evaluate",
568
+ code: `window.__sootsimConsole?.getWarnings(${limit}) || []`
569
+ });
570
+ return Array.isArray(result) ? result : [];
571
+ }
572
+ async function clearConsole(bridge) {
573
+ await bridge.send({
574
+ type: "evaluate",
575
+ code: 'window.__sootsimConsole?.clear(); "cleared"'
576
+ });
577
+ }
578
+ async function inspectRequests(bridge, opts = {}) {
579
+ const limit = opts.limit ?? 20;
580
+ const method = opts.failed === false ? "getRequests" : "getFailedRequests";
581
+ const result = await bridge.send({
582
+ type: "call",
583
+ path: `__sootsimTest.${method}`,
584
+ args: [limit]
585
+ });
586
+ return Array.isArray(result) ? result : [];
587
+ }
588
+ async function clearRequests(bridge) {
589
+ await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
590
+ }
591
+ async function inspectScrollStateAt(bridge, x, y) {
592
+ const result = await bridge.send({
593
+ type: "call",
594
+ path: "__sootsimTest.getScrollStateAt",
595
+ args: [x, y]
596
+ });
597
+ return result && typeof result === "object" ? result : null;
598
+ }
599
+ async function inspectLogs(bridge) {
600
+ const res = await bridge.send({
601
+ type: "evaluate",
602
+ code: `(() => {
603
+ const obs = window.__sootsimObservability;
604
+ if (!obs) return { ok: false };
605
+ return { ok: true, entries: obs.logs.getSnapshot() };
606
+ })()`
607
+ });
608
+ if (!res || !res.ok) return [];
609
+ return res.entries ?? [];
610
+ }
611
+ async function clearLogs(bridge) {
612
+ await bridge.send({
613
+ type: "evaluate",
614
+ code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
615
+ });
616
+ }
617
+ function isForwardedWorkerLogTwin(a, b) {
618
+ if (a.source === b.source) return false;
619
+ if (a.level !== b.level) return false;
620
+ if (Math.abs(a.ts - b.ts) > 1e3) return false;
621
+ if (a.args.length !== b.args.length) return false;
622
+ const sources = /* @__PURE__ */ new Set([a.source, b.source]);
623
+ if (!sources.has("sootsim-worker")) return false;
624
+ if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
625
+ return false;
626
+ }
627
+ return a.args.every((arg, index) => arg === b.args[index]);
628
+ }
629
+ function filterLogEntries(entries, opts = {}) {
630
+ let out = [];
631
+ for (const entry of entries) {
632
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
633
+ continue;
634
+ }
635
+ out.push(entry);
636
+ }
637
+ if (!opts.showInternal) {
638
+ out = out.filter((e) => {
639
+ const first = e.args[0];
640
+ return !(typeof first === "string" && first.startsWith("[sootsim]"));
641
+ });
642
+ }
643
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
644
+ if (opts.filter) {
645
+ const lf = opts.filter.toLowerCase();
646
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
647
+ }
648
+ return out;
649
+ }
650
+ async function inspectTimelineSummary(bridge, query) {
651
+ return await bridge.send({
652
+ type: "call",
653
+ path: "SootSim.bridges.timeline.summary",
654
+ args: [query]
655
+ });
656
+ }
657
+ async function inspectTimelineRecent(bridge, query) {
658
+ return await bridge.send({
659
+ type: "call",
660
+ path: "SootSim.bridges.timeline.recent",
661
+ args: [query]
662
+ });
663
+ }
664
+ async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
665
+ await bridge.send({
666
+ type: "call",
667
+ path: "SootSim.bridges.timeline.cursorAdvance",
668
+ args: [cursorKey, watermark]
669
+ });
670
+ }
671
+ async function inspectKeyboard(bridge) {
672
+ const result = await bridge.send({
673
+ type: "evaluate",
674
+ code: `(() => {
675
+ const kb = window.__sootsimKeyboard
676
+ if (!kb || typeof kb.getLayout !== 'function') {
677
+ return { error: 'keyboard bridge getLayout() not available' }
678
+ }
679
+ return kb.getLayout()
680
+ })()`
681
+ });
682
+ return result ?? { error: "keyboard bridge returned no result" };
683
+ }
684
+ function isShellCommandUnavailable(error) {
685
+ const message = error instanceof Error ? error.message : String(error);
686
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
687
+ }
688
+ async function getShellState(bridge, readyTimeoutMs = 0) {
689
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
690
+ while (true) {
691
+ try {
692
+ return await bridge.send({
693
+ type: "call",
694
+ path: "SootSim.bridges.mainShell.getState",
695
+ args: []
696
+ });
697
+ } catch (error) {
698
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
699
+ await new Promise((r) => setTimeout(r, 50));
700
+ }
701
+ }
702
+ }
703
+ async function inspectScreens(bridge) {
704
+ const payload = await bridge.send({
705
+ type: "evaluate",
706
+ code: `(async () => {
707
+ const test = window.__sootsimTest
708
+ const kb = window.__sootsimKeyboard
709
+ const navSnap =
710
+ test && typeof test.getNavigationSnapshot === 'function'
711
+ ? await test.getNavigationSnapshot()
712
+ : null
713
+ const keyboard =
714
+ kb && typeof kb.getLayout === 'function'
715
+ ? (() => {
716
+ const layout = kb.getLayout()
717
+ return layout ? {
718
+ visible: layout.visible,
719
+ mode: layout.mode,
720
+ spec: layout.spec
721
+ ? {
722
+ keyboardType: layout.spec.keyboardType,
723
+ returnKeyType: layout.spec.returnKeyType,
724
+ }
725
+ : null,
726
+ } : null
727
+ })()
728
+ : null
729
+ return { nav: navSnap, keyboard }
730
+ })()`
731
+ });
732
+ const shell = await getShellState(bridge, 500).catch(() => null);
733
+ return {
734
+ shell,
735
+ nav: payload?.nav ?? null,
736
+ keyboard: payload?.keyboard ?? null
737
+ };
738
+ }
739
+ var DEBUG_CHANNELS = [
740
+ "portals",
741
+ "sheets",
742
+ "layout",
743
+ "onlayout",
744
+ "animated",
745
+ "render",
746
+ "touch",
747
+ "yoga"
748
+ ];
749
+ async function inspectDebugStatus(bridge) {
750
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
751
+ }
752
+ async function inspectDebugFlags(bridge) {
753
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
754
+ }
755
+ async function inspectDebugFind(bridge, target) {
756
+ const method = target === "sheets" ? "findSheets" : "findPortals";
757
+ return bridge.send({
758
+ type: "evaluate",
759
+ code: `window.__sootsimDebug.${method}()`
760
+ });
761
+ }
762
+ async function inspectDebugRecent(bridge, channel, limit = 50) {
763
+ const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
764
+ return bridge.send({ type: "evaluate", code });
765
+ }
766
+ async function setDebugChannels(bridge, action, channels) {
767
+ const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
768
+ return bridge.send({
769
+ type: "evaluate",
770
+ code: `window.__sootsimDebug.${action}(${args})`
771
+ });
772
+ }
773
+ async function inspectMemory(bridge) {
774
+ const result = await bridge.send({
775
+ type: "evaluate",
776
+ code: `(async () => {
777
+ const host = window.__sootsimRenderHost
778
+ const stats = host?.queryStats ? await host.queryStats() : null
779
+ const hostMem = performance.memory
780
+ ? {
781
+ usedJSHeapSize: performance.memory.usedJSHeapSize,
782
+ totalJSHeapSize: performance.memory.totalJSHeapSize,
783
+ jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
784
+ }
785
+ : null
786
+ return {
787
+ imageLoader: stats?.memory?.imageLoader ?? null,
788
+ workerHeap: stats?.memory?.workerHeap ?? null,
789
+ hostHeap: hostMem,
790
+ }
791
+ })()`
792
+ });
793
+ return result ?? { imageLoader: null, workerHeap: null, hostHeap: null };
794
+ }
795
+ function formatTimelineSummary(summary) {
796
+ if (summary.total === 0) return "nothing recorded";
797
+ const parts = [];
798
+ const ORDER = [
799
+ "error",
800
+ "warning",
801
+ "console",
802
+ "fetch",
803
+ "toast",
804
+ "alert",
805
+ "actionsheet",
806
+ "picker",
807
+ "notification",
808
+ "screen",
809
+ "route",
810
+ "keyboard",
811
+ "app-launch",
812
+ "shell",
813
+ "scroll",
814
+ "gesture",
815
+ "text-input",
816
+ "react-commit",
817
+ "animation",
818
+ "reanimated"
819
+ ];
820
+ const seen = /* @__PURE__ */ new Set();
821
+ for (const k of ORDER) {
822
+ const n = summary.byKind[k];
823
+ if (n) {
824
+ parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
825
+ seen.add(k);
826
+ }
827
+ }
828
+ for (const [k, n] of Object.entries(summary.byKind)) {
829
+ if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
830
+ }
831
+ return parts.join(" \xB7 ");
832
+ }
833
+
834
+ // cli/commands/inspect/settling.ts
835
+ async function waitForSootsimIdle({
836
+ bridge,
837
+ simId,
838
+ maxMs,
839
+ pollMs = 50,
840
+ stablePolls = 3,
841
+ strict = false
842
+ }) {
843
+ const result = await bridge.send(
844
+ {
845
+ type: "evaluate",
846
+ simId,
847
+ code: `(async () => {
848
+ const start = Date.now()
849
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
850
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
851
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
852
+ const strict = ${strict ? "true" : "false"}
853
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
854
+
855
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
856
+ // old screen still renders for a moment, then the new one mounts and
857
+ // its content loads. a pure layout-hash check reports "idle" on the
858
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
859
+ // and stacks duplicate navigations. drain any in-flight screen
860
+ // transition FIRST, bounded by the overall budget. when no
861
+ // transition is happening this returns in ~80ms (startWindowMs), so
862
+ // a plain button tap barely pays for it.
863
+ try {
864
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
865
+ if (typeof wfst === 'function') {
866
+ const remaining = deadline - Date.now()
867
+ if (remaining > 120) {
868
+ await wfst({
869
+ timeoutMs: Math.min(remaining - 80, 4000),
870
+ settleMs: 64,
871
+ startWindowMs: 80,
872
+ })
873
+ }
874
+ }
875
+ } catch {}
876
+
877
+ const readSnapshot = async () => {
878
+ let animating = false
879
+ let pendingFetches = 0
880
+ let requestTotal = 0
881
+ let requestInFlight = 0
882
+ try {
883
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
884
+ if (stats) {
885
+ animating =
886
+ stats.hasActiveAnims === true ||
887
+ stats.hasActiveNativeAnimations === true ||
888
+ stats.hasPendingAnimationFrames === true
889
+ // a freshly-pushed screen showing a skeleton is layout-stable
890
+ // but not actually settled \u2014 its data/images are still in
891
+ // flight. treat bounded image-loader fetches as not-idle so
892
+ // settle waits for real content, capped by the budget.
893
+ const pf = stats.memory && stats.memory.imageLoader
894
+ ? stats.memory.imageLoader.pendingFetches
895
+ : 0
896
+ pendingFetches = typeof pf === 'number' ? pf : 0
897
+ }
898
+ } catch {}
899
+ try {
900
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
901
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
902
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
903
+ } catch {}
904
+ const root = window.__sootsimRoot
905
+ const nodes = []
906
+ if (root) {
907
+ const walk = (n) => {
908
+ if (n.layout && n.layout.width > 0) {
909
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
910
+ }
911
+ for (const c of n.children || []) walk(c)
912
+ }
913
+ walk(root)
914
+ }
915
+ return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
916
+ }
917
+
918
+ let lastLayout = ''
919
+ let lastRequestTotal = -1
920
+ let stable = 0
921
+ while (Date.now() < deadline) {
922
+ const snapshot = await readSnapshot()
923
+ const strictOk = !strict || !snapshot.animating
924
+ const contentReady = snapshot.pendingFetches === 0 && snapshot.requestInFlight === 0
925
+ const requestsStable = snapshot.requestTotal === lastRequestTotal
926
+ if (strictOk && contentReady && requestsStable && snapshot.layout === lastLayout) {
927
+ stable++
928
+ if (stable >= requiredStablePolls) {
929
+ return { settled: true, elapsed: Date.now() - start }
930
+ }
931
+ } else {
932
+ stable = 0
933
+ }
934
+ lastLayout = snapshot.layout
935
+ lastRequestTotal = snapshot.requestTotal
936
+ await sleep(pollMs)
937
+ }
938
+ return { settled: false, elapsed: Date.now() - start }
939
+ })()`
940
+ },
941
+ {
942
+ timeoutMs: maxMs + 1e3
943
+ }
944
+ );
945
+ const { elapsed, settled } = result ?? {};
946
+ return {
947
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
948
+ settled: settled === true
949
+ };
950
+ }
951
+
952
+ // cli/commands/inspect/actions.ts
953
+ var DEFAULT_AGENT_TIMING = {
954
+ initialWaitMs: 3e3,
955
+ deadlineMs: 5e3,
956
+ retryWaitMs: 700
957
+ };
958
+ var DEFAULT_INTERACTIVE_TIMING = {
959
+ initialWaitMs: 1200,
960
+ deadlineMs: 2500,
961
+ retryWaitMs: 700
962
+ };
963
+ function tapTiming(agent, opts = {}) {
964
+ return {
965
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
966
+ ...opts
967
+ };
968
+ }
969
+ function isTapSuccess(result) {
970
+ if (!result || result.hit === false || result.ok === false) return false;
971
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
972
+ return false;
973
+ }
974
+ return true;
975
+ }
976
+ function tapTargetFromPayload(payload, textFallback) {
977
+ return {
978
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
979
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
980
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
981
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
982
+ };
983
+ }
984
+ async function tapResolvedTarget(bridge, args) {
985
+ const timing = tapTiming(!!args.agent, args.timing);
986
+ let attempts = 0;
987
+ let lastPayload = null;
988
+ let lastResult = null;
989
+ try {
990
+ await waitForSootsimIdle({
991
+ bridge,
992
+ maxMs: timing.initialWaitMs,
993
+ pollMs: 32,
994
+ stablePolls: 2
995
+ });
996
+ } catch {
997
+ }
998
+ const deadline = Date.now() + timing.deadlineMs;
999
+ while (Date.now() <= deadline || attempts === 0) {
1000
+ attempts++;
1001
+ const payload = await args.resolve();
1002
+ lastPayload = payload;
1003
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1004
+ return { payload, result: null, attempts, failure: "special" };
1005
+ }
1006
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1007
+ const result = await bridge.send({
1008
+ type: "tap",
1009
+ x: payload.cx,
1010
+ y: payload.cy,
1011
+ target: tapTargetFromPayload(payload, args.textFallback)
1012
+ });
1013
+ lastResult = result;
1014
+ if (isTapSuccess(result)) return { payload, result, attempts };
1015
+ }
1016
+ const remaining = deadline - Date.now();
1017
+ if (remaining <= 0) break;
1018
+ try {
1019
+ await waitForSootsimIdle({
1020
+ bridge,
1021
+ maxMs: Math.min(remaining, timing.retryWaitMs),
1022
+ pollMs: 32,
1023
+ stablePolls: 2
1024
+ });
1025
+ } catch {
1026
+ await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
1027
+ }
1028
+ }
1029
+ return {
1030
+ payload: lastPayload,
1031
+ result: lastResult,
1032
+ attempts,
1033
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1034
+ };
1035
+ }
1036
+ async function tapCoordinates(bridge, x, y, target) {
1037
+ return bridge.send({
1038
+ type: "tap",
1039
+ x,
1040
+ y,
1041
+ ...target ? { target } : {}
1042
+ });
1043
+ }
1044
+ async function tapById(bridge, query, opts = {}) {
1045
+ const arg = JSON.stringify(query);
1046
+ return tapResolvedTarget(bridge, {
1047
+ agent: opts.agent,
1048
+ timing: opts.timing,
1049
+ resolve: () => bridge.send({
1050
+ type: "evaluate",
1051
+ code: `(async () => {
1052
+ const t = window.__sootsimTest
1053
+ if (!t) return null
1054
+ const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1055
+ if (!n || !n.absolutePosition || !n.layout) return { cx: null }
1056
+ const resolved =
1057
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
1058
+ ? await t.resolveTapTarget(n.nodeId)
1059
+ : null
1060
+ const target = (resolved && resolved.target) || n
1061
+ const cx =
1062
+ resolved && typeof resolved.cx === 'number'
1063
+ ? resolved.cx
1064
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
1065
+ const cy =
1066
+ resolved && typeof resolved.cy === 'number'
1067
+ ? resolved.cy
1068
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
1069
+ return {
1070
+ cx,
1071
+ cy,
1072
+ match: {
1073
+ nodeId: n.nodeId ?? null,
1074
+ id: n.id,
1075
+ testID: n.testID,
1076
+ text: n.text ?? n.accessibilityLabel ?? null,
1077
+ type: n.type,
1078
+ },
1079
+ target: {
1080
+ nodeId: target.nodeId ?? null,
1081
+ id: target.id,
1082
+ testID: target.testID,
1083
+ text:
1084
+ target.text ??
1085
+ target.accessibilityLabel ??
1086
+ n.text ??
1087
+ n.accessibilityLabel ??
1088
+ null,
1089
+ type: target.type,
1090
+ },
1091
+ strategy: (resolved && resolved.strategy) || 'matched-node',
1092
+ }
1093
+ })()`
1094
+ })
1095
+ });
1096
+ }
1097
+ async function tapByText(bridge, query, options = {}, opts = {}) {
1098
+ const filterArg = JSON.stringify({
1099
+ query,
1100
+ exact: !!options.exact,
1101
+ role: options.role ?? null,
1102
+ within: options.within ?? null,
1103
+ minX: options.minX ?? null,
1104
+ maxX: options.maxX ?? null,
1105
+ minY: options.minY ?? null,
1106
+ maxY: options.maxY ?? null,
1107
+ near: options.near ?? null,
1108
+ nth: options.nth ?? null,
1109
+ first: !!options.first
1110
+ });
1111
+ return tapResolvedTarget(bridge, {
1112
+ agent: opts.agent,
1113
+ timing: opts.timing,
1114
+ textFallback: query,
1115
+ resolve: () => bridge.send({
1116
+ type: "evaluate",
1117
+ code: `(async () => {
1118
+ const t = window.__sootsimTest
1119
+ if (!t) return { error: 'bridge-not-ready' }
1120
+ const F = ${filterArg}
1121
+
1122
+ const res = await t.queryTextCandidates({
1123
+ query: F.query,
1124
+ exact: !!F.exact,
1125
+ ...(F.role ? { role: F.role } : {}),
1126
+ })
1127
+
1128
+ let candidates = res.candidates || []
1129
+
1130
+ candidates = candidates.filter((c) => {
1131
+ const ax = c.info.absolutePosition && c.info.absolutePosition.x
1132
+ const ay = c.info.absolutePosition && c.info.absolutePosition.y
1133
+ if (F.minX !== null && !(ax >= F.minX)) return false
1134
+ if (F.maxX !== null && !(ax <= F.maxX)) return false
1135
+ if (F.minY !== null && !(ay >= F.minY)) return false
1136
+ if (F.maxY !== null && !(ay <= F.maxY)) return false
1137
+ if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
1138
+ return true
1139
+ })
1140
+
1141
+ if (F.near) {
1142
+ candidates.sort((a, b) => {
1143
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
1144
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
1145
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
1146
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
1147
+ return (
1148
+ Math.hypot(ax - F.near.x, ay - F.near.y) -
1149
+ Math.hypot(bx - F.near.x, by - F.near.y)
1150
+ )
1151
+ })
1152
+ } else {
1153
+ candidates.sort((a, b) => {
1154
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
1155
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
1156
+ if (Math.abs(ay - by) > 2) return ay - by
1157
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
1158
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
1159
+ return ax - bx
1160
+ })
1161
+ }
1162
+
1163
+ const total = candidates.length
1164
+ if (total === 0) return { matched: 0, total: 0 }
1165
+
1166
+ let idx = 0
1167
+ if (F.nth !== null) {
1168
+ idx = F.nth < 0 ? total + F.nth : F.nth
1169
+ if (idx < 0 || idx >= total) {
1170
+ return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
1171
+ }
1172
+ } else if (total > 1 && !F.first && !F.near) {
1173
+ return {
1174
+ ambiguous: true,
1175
+ total,
1176
+ candidates: candidates.slice(0, 10).map((c, i) => ({
1177
+ idx: i,
1178
+ nodeId: c.info.nodeId,
1179
+ type: c.info.type,
1180
+ testID: c.info.testID,
1181
+ text: (c.info.text || '').slice(0, 60),
1182
+ abs: c.info.absolutePosition,
1183
+ layout: c.info.layout
1184
+ ? {
1185
+ width: Math.round(c.info.layout.width || 0),
1186
+ height: Math.round(c.info.layout.height || 0),
1187
+ }
1188
+ : null,
1189
+ ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
1190
+ })),
1191
+ }
1192
+ }
1193
+
1194
+ const picked = candidates[idx]
1195
+ const n = picked.info
1196
+ if (!n.absolutePosition || !n.layout) return { matched: 0, total }
1197
+
1198
+ const resolved =
1199
+ typeof n.nodeId === 'number' &&
1200
+ typeof t.resolveTapTarget === 'function'
1201
+ ? await t.resolveTapTarget(n.nodeId)
1202
+ : null
1203
+ const target = (resolved && resolved.target) || n
1204
+ const cx =
1205
+ resolved && typeof resolved.cx === 'number'
1206
+ ? resolved.cx
1207
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
1208
+ const cy =
1209
+ resolved && typeof resolved.cy === 'number'
1210
+ ? resolved.cy
1211
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
1212
+ return {
1213
+ cx,
1214
+ cy,
1215
+ match: {
1216
+ nodeId: n.nodeId ?? null,
1217
+ id: n.id,
1218
+ testID: n.testID,
1219
+ type: n.type,
1220
+ },
1221
+ target: {
1222
+ nodeId: target.nodeId ?? null,
1223
+ id: target.id,
1224
+ testID: target.testID,
1225
+ text:
1226
+ target.text ??
1227
+ target.accessibilityLabel ??
1228
+ n.text ??
1229
+ n.accessibilityLabel ??
1230
+ null,
1231
+ type: target.type,
1232
+ },
1233
+ strategy: (resolved && resolved.strategy) || 'matched-node',
1234
+ total,
1235
+ idx,
1236
+ }
1237
+ })()`
1238
+ })
1239
+ });
1240
+ }
1241
+ async function tapBest(bridge, query, opts = {}) {
1242
+ const arg = JSON.stringify(query);
1243
+ return tapResolvedTarget(bridge, {
1244
+ agent: opts.agent,
1245
+ timing: opts.timing,
1246
+ textFallback: query,
1247
+ resolve: async () => {
1248
+ const payload = await bridge.send({
1249
+ type: "evaluate",
1250
+ code: `(async () => {
1251
+ const t = window.__sootsimTest
1252
+ if (!t) return { error: 'bridge-not-ready' }
1253
+ const byTestId =
1254
+ (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1255
+ if (byTestId && byTestId.absolutePosition && byTestId.layout) {
1256
+ return {
1257
+ strategy: 'testid',
1258
+ node: {
1259
+ nodeId: byTestId.nodeId ?? null,
1260
+ id: byTestId.id,
1261
+ testID: byTestId.testID,
1262
+ type: byTestId.type,
1263
+ text: byTestId.text,
1264
+ absolutePosition: byTestId.absolutePosition,
1265
+ layout: byTestId.layout,
1266
+ },
1267
+ }
1268
+ }
1269
+ const byText = await t.findByText(${arg})
1270
+ if (byText && byText.absolutePosition && byText.layout) {
1271
+ return {
1272
+ strategy: 'text',
1273
+ node: {
1274
+ nodeId: byText.nodeId ?? null,
1275
+ id: byText.id,
1276
+ testID: byText.testID,
1277
+ type: byText.type,
1278
+ text: byText.text,
1279
+ absolutePosition: byText.absolutePosition,
1280
+ layout: byText.layout,
1281
+ },
1282
+ }
1283
+ }
1284
+ return { strategy: 'none' }
1285
+ })()`
1286
+ });
1287
+ if (!payload || !("node" in payload)) return payload;
1288
+ const node = payload.node;
1289
+ const cx = node.absolutePosition.x + node.layout.width / 2;
1290
+ const cy = node.absolutePosition.y + node.layout.height / 2;
1291
+ return {
1292
+ ...payload,
1293
+ cx,
1294
+ cy,
1295
+ target: {
1296
+ id: node.id,
1297
+ testID: node.testID,
1298
+ text: payload.strategy === "text" ? query : node.text,
1299
+ type: node.type
1300
+ }
1301
+ };
1302
+ }
1303
+ });
1304
+ }
1305
+ export {
1306
+ DEBUG_CHANNELS,
1307
+ READY_CONTENT_NODE_FLOOR,
1308
+ READY_NODE_STABLE_MS,
1309
+ WAIT_READY_PROBE,
1310
+ WAIT_READY_PROGRESS_INTERVAL_MS,
1311
+ clearConsole,
1312
+ clearLogs,
1313
+ clearRequests,
1314
+ filterLogEntries,
1315
+ formatTimelineSummary,
1316
+ getShellState,
1317
+ inspectAccessibilityTree,
1318
+ inspectDebugFind,
1319
+ inspectDebugFlags,
1320
+ inspectDebugRecent,
1321
+ inspectDebugStatus,
1322
+ inspectDescribe,
1323
+ inspectErrors,
1324
+ inspectFind,
1325
+ inspectKeyboard,
1326
+ inspectLogs,
1327
+ inspectMemory,
1328
+ inspectNodeCount,
1329
+ inspectRequests,
1330
+ inspectScreens,
1331
+ inspectScrollStateAt,
1332
+ inspectTimelineAdvanceCursor,
1333
+ inspectTimelineRecent,
1334
+ inspectTimelineSummary,
1335
+ inspectTree,
1336
+ inspectUrl,
1337
+ inspectWaitReady,
1338
+ inspectWaitSelector,
1339
+ inspectWarnings,
1340
+ isInspectModeActive,
1341
+ isInspectPickCommand,
1342
+ isShellCommandUnavailable,
1343
+ isTapSuccess,
1344
+ rankInteractive,
1345
+ readyProbeHasContent,
1346
+ resolveFindMode,
1347
+ resolveMaxMsFlag,
1348
+ scoreInteractive,
1349
+ setDebugChannels,
1350
+ shouldSkipAutoSettleForInspectPick,
1351
+ tapBest,
1352
+ tapById,
1353
+ tapByText,
1354
+ tapCommandForNode,
1355
+ tapCoordinates,
1356
+ tapResolvedTarget,
1357
+ tapTargetFromPayload,
1358
+ waitForSootsimIdle,
1359
+ waitReadyReason
1360
+ };