rnxsim 0.1.366 → 0.1.367

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 (239) hide show
  1. package/README.md +14 -2
  2. package/cli/bin.ts +18 -0
  3. package/cli/bridge-flow-runner.ts +14 -151
  4. package/cli/commands/assert.ts +1 -1
  5. package/cli/commands/box/checkout-plane.ts +126 -0
  6. package/cli/commands/box/rnx-command.ts +68 -0
  7. package/cli/commands/box.ts +200 -0
  8. package/cli/commands/control.ts +12 -32
  9. package/cli/commands/do-chain.ts +412 -0
  10. package/cli/commands/flow.ts +0 -10
  11. package/cli/commands/inspect/core.ts +302 -725
  12. package/cli/commands/inspect/describe.ts +3 -76
  13. package/cli/commands/inspect/find.ts +3 -6
  14. package/cli/commands/inspect/get-layout.ts +14 -1
  15. package/cli/commands/inspect/resolve-target.ts +41 -47
  16. package/cli/commands/inspect/shared.ts +28 -123
  17. package/cli/commands/inspect/tree.ts +6 -15
  18. package/cli/commands/inspect/wait-ready.ts +2 -2
  19. package/cli/commands/inspect/wait-selector.ts +4 -8
  20. package/cli/commands/inspect.ts +23 -17
  21. package/cli/commands/install-desktop.ts +44 -7
  22. package/cli/commands/maestro-generate.ts +0 -2
  23. package/cli/commands/platform.ts +68 -0
  24. package/cli/commands/state.ts +5 -2
  25. package/cli/outbound-endpoints.ts +6 -1
  26. package/cli/parse-args.ts +13 -2
  27. package/cli/ws-bridge.ts +30 -2
  28. package/detox/index.ts +34 -17
  29. package/dist-cli/bin.js +5 -5
  30. package/dist-cli/chunks/{agent-SO6IOPRK.js → agent-EKKIAKP5.js} +2 -2
  31. package/dist-cli/chunks/{agent-wrapper-PMYCB2KO.js → agent-wrapper-MHLQUGA6.js} +2 -2
  32. package/dist-cli/chunks/{app-fonts-LULQGFY7.js → app-fonts-HN6ADRTQ.js} +2 -2
  33. package/dist-cli/chunks/{assert-WGNCD4ZU.js → assert-IMR6QCN6.js} +3 -3
  34. package/dist-cli/chunks/{auth-G7CCHH3M.js → auth-27LPMQNH.js} +2 -2
  35. package/dist-cli/chunks/{beta-N4LXL5TO.js → beta-6RXBB5AI.js} +2 -2
  36. package/dist-cli/chunks/box-DORLF3GP.js +1709 -0
  37. package/dist-cli/chunks/{camera-4JHCYBQS.js → camera-2G7JZ5DL.js} +2 -2
  38. package/dist-cli/chunks/{chunk-2UXU53MG.js → chunk-3QZJLNYY.js} +1 -1
  39. package/dist-cli/chunks/{chunk-YXXKSL6R.js → chunk-3VBVEDDA.js} +1 -1
  40. package/dist-cli/chunks/{chunk-ZJIS3CTW.js → chunk-4BWGLTKU.js} +2 -2
  41. package/dist-cli/chunks/chunk-4XSNO374.js +423 -0
  42. package/dist-cli/chunks/{chunk-RYIMSKDO.js → chunk-5DGMFUZG.js} +2 -2
  43. package/dist-cli/chunks/{chunk-M3BNCF7E.js → chunk-6DBS5EGX.js} +2 -2
  44. package/dist-cli/chunks/{chunk-ONORMKWX.js → chunk-6SO6I3D5.js} +2 -2
  45. package/dist-cli/chunks/chunk-75EFOPXE.js +32 -0
  46. package/dist-cli/chunks/{chunk-5NMCF5VF.js → chunk-A5AJFVOF.js} +1 -1
  47. package/dist-cli/chunks/{chunk-N3UC7HIX.js → chunk-AM2ODYAN.js} +1 -1
  48. package/dist-cli/chunks/{chunk-I6KIM6PN.js → chunk-AVOROGA3.js} +1 -1
  49. package/dist-cli/chunks/chunk-AXMMLO7D.js +130 -0
  50. package/dist-cli/chunks/{chunk-ORWP2E7R.js → chunk-BSEXEYNX.js} +2 -2
  51. package/dist-cli/chunks/{chunk-NQFJOCSF.js → chunk-CM6ZKZW7.js} +2 -2
  52. package/dist-cli/chunks/{chunk-ZMJ75EDG.js → chunk-DNVABCEC.js} +2 -2
  53. package/dist-cli/chunks/{chunk-2S7BKUP2.js → chunk-E33A4AIR.js} +2 -2
  54. package/dist-cli/chunks/{chunk-GXAB52K7.js → chunk-EFMOXYDD.js} +2 -2
  55. package/dist-cli/chunks/{chunk-TIAEYO6I.js → chunk-EV4P5UBP.js} +2 -2
  56. package/dist-cli/chunks/{chunk-JHXETDVC.js → chunk-G5IUIGGC.js} +1 -1
  57. package/dist-cli/chunks/chunk-GMLOCV3E.js +312 -0
  58. package/dist-cli/chunks/{chunk-ZLVRYXZ7.js → chunk-GRN2FHVW.js} +1 -1
  59. package/dist-cli/chunks/{chunk-A5FXX5PJ.js → chunk-HFFXPUEP.js} +1 -1
  60. package/dist-cli/chunks/chunk-HG6T4OOR.js +4 -0
  61. package/dist-cli/chunks/{chunk-OW5XHAA4.js → chunk-HNT2OXPZ.js} +1 -1
  62. package/dist-cli/chunks/chunk-IPEURNLI.js +24 -0
  63. package/dist-cli/chunks/chunk-IR44SFBA.js +5 -0
  64. package/dist-cli/chunks/chunk-JMSLXZFW.js +4 -0
  65. package/dist-cli/chunks/{chunk-EFR5DVNP.js → chunk-JPXQKLNH.js} +1 -1
  66. package/dist-cli/chunks/chunk-K3EC6JDE.js +4 -0
  67. package/dist-cli/chunks/{chunk-YGMEDJKJ.js → chunk-KHX65HAU.js} +2 -2
  68. package/dist-cli/chunks/{chunk-EQK5MJCU.js → chunk-LWOVKHRC.js} +2 -2
  69. package/dist-cli/chunks/{chunk-5OK3LCEJ.js → chunk-MQN5DWTI.js} +2 -2
  70. package/dist-cli/chunks/{chunk-TTASWLKM.js → chunk-N3BXQEUF.js} +1 -1
  71. package/dist-cli/chunks/{chunk-UCHW7ZHR.js → chunk-N5OSV26B.js} +2 -2
  72. package/dist-cli/chunks/chunk-PHNURIH3.js +4 -0
  73. package/dist-cli/chunks/{chunk-AFV32WZT.js → chunk-PP2KNVXP.js} +3 -3
  74. package/dist-cli/chunks/{chunk-XJU2QIRY.js → chunk-PROKNJVY.js} +2 -2
  75. package/dist-cli/chunks/chunk-QXLLY4IM.js +7 -0
  76. package/dist-cli/chunks/{chunk-HT6GFZOZ.js → chunk-RNQTOGM7.js} +1 -1
  77. package/dist-cli/chunks/{chunk-CN6XYYEA.js → chunk-RPNKKVBN.js} +27 -27
  78. package/dist-cli/chunks/{chunk-LN3RNW6X.js → chunk-RSNLI2NH.js} +2 -2
  79. package/dist-cli/chunks/{chunk-YCCCPFWT.js → chunk-RZGHV2WI.js} +2 -2
  80. package/dist-cli/chunks/chunk-SGGL3AFV.js +497 -0
  81. package/dist-cli/chunks/chunk-SH5ETJKG.js +4 -0
  82. package/dist-cli/chunks/{chunk-77ZHQJZR.js → chunk-T6J4YP56.js} +2 -2
  83. package/dist-cli/chunks/{chunk-MGDSJZ37.js → chunk-TWUE7WF5.js} +2 -2
  84. package/dist-cli/chunks/{chunk-SA6XVI55.js → chunk-U44VEQRN.js} +1 -1
  85. package/dist-cli/chunks/{chunk-IZKHOM37.js → chunk-URLVQHC3.js} +1 -1
  86. package/dist-cli/chunks/{chunk-GAJQSITE.js → chunk-VANZFTUE.js} +1 -1
  87. package/dist-cli/chunks/{chunk-EGAGZHO6.js → chunk-VB2YBH46.js} +1 -1
  88. package/dist-cli/chunks/{chunk-H23KWIKA.js → chunk-VFU2SJ7M.js} +2 -2
  89. package/dist-cli/chunks/{chunk-34IQM22N.js → chunk-VUBJ6FXU.js} +15 -15
  90. package/dist-cli/chunks/{chunk-NM2L2QV4.js → chunk-W5PATILQ.js} +2 -2
  91. package/dist-cli/chunks/chunk-WI5V4TYG.js +38 -0
  92. package/dist-cli/chunks/{chunk-6PL2CCGP.js → chunk-XTRHQAOV.js} +2 -2
  93. package/dist-cli/chunks/chunk-XWUTJLBM.js +27 -0
  94. package/dist-cli/chunks/{chunk-5DAIA77C.js → chunk-XYRUQQUR.js} +2 -2
  95. package/dist-cli/chunks/chunk-YBFB3RM7.js +4 -0
  96. package/dist-cli/chunks/chunk-YGQVQ2VG.js +4 -0
  97. package/dist-cli/chunks/{chunk-CTG7HBCA.js → chunk-YINACV32.js} +1 -1
  98. package/dist-cli/chunks/chunk-YV7ERO2V.js +5 -0
  99. package/dist-cli/chunks/{cleanup-KRIATKFH.js → cleanup-GVFHQ3KH.js} +2 -2
  100. package/dist-cli/chunks/cli-version-DPX5MA2M.js +4 -0
  101. package/dist-cli/chunks/{compat-6PTQ3QEA.js → compat-AYRTXYAM.js} +2 -2
  102. package/dist-cli/chunks/{config-ETGDUANO.js → config-K7LOOV62.js} +2 -2
  103. package/dist-cli/chunks/connect-H3CNY4IO.js +4 -0
  104. package/dist-cli/chunks/control-SMHZ2CF5.js +4 -0
  105. package/dist-cli/chunks/daemon-TEZUNI73.js +4 -0
  106. package/dist-cli/chunks/{debug-WIIB6ZKC.js → debug-ELGHG57T.js} +3 -3
  107. package/dist-cli/chunks/{desktop-TD7Q5LNB.js → desktop-4G7FK3MP.js} +3 -3
  108. package/dist-cli/chunks/{detox-7GNR33VM.js → detox-JKXGEJIZ.js} +2 -2
  109. package/dist-cli/chunks/{device-QOR3PGUF.js → device-IV3AR6KG.js} +2 -2
  110. package/dist-cli/chunks/{diagnose-RRCBLP6E.js → diagnose-3ERGF7EW.js} +2 -2
  111. package/dist-cli/chunks/{disk-cleanup-MJUGJPXT.js → disk-cleanup-ALFEVZ4B.js} +2 -2
  112. package/dist-cli/chunks/do-chain-K2B4OHM7.js +5 -0
  113. package/dist-cli/chunks/drivers-KJYXA3M3.js +4 -0
  114. package/dist-cli/chunks/env-2KF6IYZE.js +4 -0
  115. package/dist-cli/chunks/{film-D7Y64LID.js → film-UQXJFAD5.js} +3 -3
  116. package/dist-cli/chunks/flow-ASTUH56E.js +4 -0
  117. package/dist-cli/chunks/help-SNNYFELA.js +4 -0
  118. package/dist-cli/chunks/{hidden-runtime-alias-JGURVR2Z.js → hidden-runtime-alias-VJCWCFVO.js} +2 -2
  119. package/dist-cli/chunks/{home-paths-4ICFIGXO.js → home-paths-EGUZZ6J6.js} +2 -2
  120. package/dist-cli/chunks/inspect-XANAADQT.js +4 -0
  121. package/dist-cli/chunks/install-desktop-JKQA7UY6.js +4 -0
  122. package/dist-cli/chunks/{login-RU33QR2G.js → login-6KQGD7HK.js} +2 -2
  123. package/dist-cli/chunks/{logout-2IUG3Y5J.js → logout-2FCOVLBC.js} +2 -2
  124. package/dist-cli/chunks/{maestro-2MXTGG7I.js → maestro-HPSSEVJD.js} +3 -3
  125. package/dist-cli/chunks/{maestro-generate-2PYIFGLR.js → maestro-generate-KQQ6JHMR.js} +5 -5
  126. package/dist-cli/chunks/{mode-3DAPG3HF.js → mode-SH7KRZET.js} +2 -2
  127. package/dist-cli/chunks/{optional-demo-registry-BWSDJF2Z.js → optional-demo-registry-NAMCAVFX.js} +2 -2
  128. package/dist-cli/chunks/{perf-2TKPWB4N.js → perf-LWKAHXUL.js} +2 -2
  129. package/dist-cli/chunks/{permissions-5CSMHYNA.js → permissions-EZTKWTXV.js} +2 -2
  130. package/dist-cli/chunks/platform-KSOJIVN5.js +6 -0
  131. package/dist-cli/chunks/privacy-EJULWP7L.js +4 -0
  132. package/dist-cli/chunks/{record-I2HJCFP3.js → record-2IC7BAQN.js} +3 -3
  133. package/dist-cli/chunks/{report-issue-A4FZWBOC.js → report-issue-MMX6HOQO.js} +2 -2
  134. package/dist-cli/chunks/reset-PKFZA4NC.js +4 -0
  135. package/dist-cli/chunks/resolve-target-AFKU3BRE.js +4 -0
  136. package/dist-cli/chunks/runtime-XHWWPOIN.js +4 -0
  137. package/dist-cli/chunks/{screenshot-command-UFW5BFHP.js → screenshot-command-JYUXDVSA.js} +3 -3
  138. package/dist-cli/chunks/{screenshot-layers-5BIAL4YN.js → screenshot-layers-OGRY6AA3.js} +2 -2
  139. package/dist-cli/chunks/{screenshots-capture-EHYS3BV2.js → screenshots-capture-QHDN65XS.js} +2 -2
  140. package/dist-cli/chunks/{serve-O4FDM3AC.js → serve-W4BKAG4J.js} +3 -3
  141. package/dist-cli/chunks/setup-WJPMPSAC.js +4 -0
  142. package/dist-cli/chunks/sim-client-UOQGFBPO.js +4 -0
  143. package/dist-cli/chunks/{skills-ZK55SY32.js → skills-OPPXOD63.js} +2 -2
  144. package/dist-cli/chunks/state-TDAN7BOM.js +14 -0
  145. package/dist-cli/chunks/{storage-WP33ISEM.js → storage-CFB2YGLY.js} +2 -2
  146. package/dist-cli/chunks/store-4TEED4ZU.js +4 -0
  147. package/dist-cli/chunks/telemetry-S6N7CV6G.js +4 -0
  148. package/dist-cli/chunks/{timeline-DKGW5S4I.js → timeline-ASOOVP4C.js} +2 -2
  149. package/dist-cli/chunks/{upgrade-LL7G4B7G.js → upgrade-6MVJISHG.js} +2 -2
  150. package/dist-cli/chunks/upload-CBNIRFEW.js +4 -0
  151. package/dist-cli/chunks/{version-YLWR5G4F.js → version-5GS7CWRL.js} +2 -2
  152. package/dist-cli/chunks/{web-QELZ4CP6.js → web-QRRMYMV4.js} +2 -2
  153. package/dist-cli/chunks/welcome-33FXMAMB.js +5 -0
  154. package/dist-cli/chunks/{what-happened-MIOMZWM5.js → what-happened-GAZJGKXX.js} +2 -2
  155. package/dist-cli/chunks/ws-bridge-DIXUAPLQ.js +4 -0
  156. package/dist-lib/agent-daemon-client.cjs +1 -1
  157. package/dist-lib/agent-events.cjs +1 -1
  158. package/dist-lib/agent-identity.cjs +1 -1
  159. package/dist-lib/agent-sessions.cjs +1 -1
  160. package/dist-lib/attached-projects.cjs +1 -1
  161. package/dist-lib/auth/shared-session.cjs +1 -1
  162. package/dist-lib/backend-origin.cjs +1 -1
  163. package/dist-lib/beta.cjs +1 -1
  164. package/dist-lib/beta.mjs +1 -1
  165. package/dist-lib/bridge-constants.cjs +1 -1
  166. package/dist-lib/bridge-contract-input.cjs +336 -0
  167. package/dist-lib/bridge-contract-input.mjs +303 -0
  168. package/dist-lib/bridge-contract.cjs +29 -1
  169. package/dist-lib/bridge-contract.mjs +19 -0
  170. package/dist-lib/cli-constants.cjs +1 -1
  171. package/dist-lib/config.cjs +1 -1
  172. package/dist-lib/detox/index.cjs +32 -18
  173. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  174. package/dist-lib/home-paths.cjs +1 -1
  175. package/dist-lib/host/bridge-host.cjs +5 -1
  176. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  177. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  178. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  179. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  180. package/dist-lib/host/websocket-proxy.cjs +1 -1
  181. package/dist-lib/index.cjs +214 -594
  182. package/dist-lib/jump-to-source-babel.cjs +1 -1
  183. package/dist-lib/menu.cjs +1 -1
  184. package/dist-lib/menu.mjs +1 -1
  185. package/dist-lib/metro-production-bundle.cjs +1 -1
  186. package/dist-lib/metro-production-bundle.mjs +1 -1
  187. package/dist-lib/metro.cjs +1 -1
  188. package/dist-lib/profiles.cjs +1 -1
  189. package/dist-lib/public-brand.cjs +1 -1
  190. package/dist-lib/react-native-host-modules.cjs +1 -1
  191. package/dist-lib/react-native-host-modules.mjs +1 -1
  192. package/dist-lib/render-mode.cjs +1 -1
  193. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  194. package/dist-lib/sdk.cjs +273 -657
  195. package/dist-lib/sdk.mjs +271 -652
  196. package/dist-lib/skills.cjs +190 -287
  197. package/dist-lib/vite.cjs +1 -1
  198. package/package.json +9 -6
  199. package/skills/contrast/SKILL.md +3 -0
  200. package/skills/rnx-debug/SKILL.md +8 -0
  201. package/skills/rnx-test/SKILL.md +21 -0
  202. package/src/bridge-contract-input.ts +359 -0
  203. package/src/bridge-contract.ts +121 -1
  204. package/src/host/bridge-host.ts +7 -12
  205. package/src/index.ts +7 -0
  206. package/src/sim-client.ts +12 -3
  207. package/dist-cli/chunks/chunk-3X6IBUBX.js +0 -38
  208. package/dist-cli/chunks/chunk-7B37IZBN.js +0 -15
  209. package/dist-cli/chunks/chunk-DMPF2D24.js +0 -4
  210. package/dist-cli/chunks/chunk-DW37EVX5.js +0 -767
  211. package/dist-cli/chunks/chunk-DWFIH7CB.js +0 -32
  212. package/dist-cli/chunks/chunk-GUI3PDH2.js +0 -27
  213. package/dist-cli/chunks/chunk-KLF3K6ZZ.js +0 -4
  214. package/dist-cli/chunks/chunk-M5QCGLXR.js +0 -944
  215. package/dist-cli/chunks/chunk-M6SKQGAN.js +0 -5
  216. package/dist-cli/chunks/chunk-NAYLPOC5.js +0 -24
  217. package/dist-cli/chunks/chunk-VIND4SP3.js +0 -124
  218. package/dist-cli/chunks/chunk-XXNLYR64.js +0 -5
  219. package/dist-cli/chunks/chunk-ZFM24DOX.js +0 -4
  220. package/dist-cli/chunks/cli-version-SYGNHGFR.js +0 -4
  221. package/dist-cli/chunks/control-RBTNOLQM.js +0 -4
  222. package/dist-cli/chunks/daemon-N3SC5DK6.js +0 -4
  223. package/dist-cli/chunks/drivers-W6E3BZH4.js +0 -4
  224. package/dist-cli/chunks/flow-7QAE2V5Y.js +0 -4
  225. package/dist-cli/chunks/help-NLD6KWVG.js +0 -4
  226. package/dist-cli/chunks/inspect-C347BG52.js +0 -4
  227. package/dist-cli/chunks/install-desktop-YR6TWZYK.js +0 -4
  228. package/dist-cli/chunks/privacy-PWWC2BXO.js +0 -4
  229. package/dist-cli/chunks/reset-Y3PBPFYA.js +0 -4
  230. package/dist-cli/chunks/runtime-IC6NVVGA.js +0 -4
  231. package/dist-cli/chunks/setup-Y4QGSJBM.js +0 -4
  232. package/dist-cli/chunks/state-KFLAZNEY.js +0 -14
  233. package/dist-cli/chunks/store-VEE22CAY.js +0 -4
  234. package/dist-cli/chunks/telemetry-PZV4BISJ.js +0 -4
  235. package/dist-cli/chunks/upload-Y4C3YZDK.js +0 -4
  236. package/dist-cli/chunks/welcome-ZPKA5PZJ.js +0 -5
  237. package/dist-lib/mcp.cjs +0 -673
  238. package/dist-lib/mcp.mjs +0 -641
  239. package/src/mcp.ts +0 -791
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.366 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.367 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -653,6 +653,15 @@ async function tapBest(bridge, query, opts = {}) {
653
653
  });
654
654
  }
655
655
 
656
+ // src/bridge-contract.ts
657
+ var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
658
+ function readyProbeHasContent(probe) {
659
+ return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
660
+ }
661
+ function readyProbeHasTargetContent(probe) {
662
+ return probe.liveFrameActive || probe.targets > 0;
663
+ }
664
+
656
665
  // cli/commands/inspect/core.ts
657
666
  function resolveMaxMsFlag(args, fallbackMs) {
658
667
  const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
@@ -692,121 +701,116 @@ async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
692
701
  }
693
702
  }
694
703
  async function inspectNodeCount(bridge) {
695
- const count = await bridge.send({
696
- type: "evaluate",
697
- code: "(async () => await window.__sootsimTest.getNodeCount())()"
704
+ const result = await bridge.send({
705
+ type: "query",
706
+ query: { kind: "count" }
698
707
  });
699
- return { nodes: typeof count === "number" ? count : 0 };
708
+ return { nodes: isSemanticQueryResult(result) ? result.total : 0 };
700
709
  }
701
- async function inspectTree(bridge, depth = 5, opts) {
702
- const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
703
- const tree = await bridge.send({
704
- type: "evaluate",
705
- code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
710
+ async function inspectTree(bridge, depth = 5) {
711
+ const result = await bridge.send({
712
+ type: "query",
713
+ query: { kind: "tree", depth }
706
714
  });
707
- if (opts?.format === "boxes" && typeof tree === "string") {
708
- try {
709
- return { depth, tree: JSON.parse(tree) };
710
- } catch {
711
- return { depth, tree };
712
- }
713
- }
715
+ const tree = isSemanticQueryResult(result) ? result.nodes : [];
714
716
  return { depth, tree };
715
717
  }
716
718
  async function inspectUrl(bridge) {
717
719
  const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
718
720
  return { url: typeof url === "string" ? url : "" };
719
721
  }
720
- async function inspectDescribe(bridge, dumpOpts) {
721
- const dumpDepth = dumpOpts.maxDepth ?? 12;
722
- const code = `(async () => {
723
- const t = window.__sootsimTest
724
- const mainShell = window.SootSim?.bridges?.mainShell
725
- const kb = window.__sootsimKeyboard
726
- if (!t) return { error: 'no test bridge' }
727
-
728
- let shell = null
729
- try {
730
- shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
731
- } catch {}
732
-
733
- const opts = ${JSON.stringify(dumpOpts)}
734
- const needsRecursiveDump =
735
- opts.verbose || opts.subtreeRoot || opts.includeOccluded || opts.maxDepth != null
736
- const globToRegex = (glob) => {
737
- const escaped = String(glob).replace(/[.+^$(){}|[\\]\\\\]/g, '\\\\$&')
738
- const body = escaped.replace(/\\*/g, '.*').replace(/\\?/g, '.')
739
- return new RegExp('^' + body + '$', 'i')
740
- }
741
- // clipping a describe line is a DISPLAY decision. opts.fullText (set by
742
- // \`describe --json\`) keeps the real string, so a bundle-eval stack or a
743
- // red-box message reaches the caller whole instead of ending in an
744
- // ellipsis nothing downstream can undo.
745
- const clipForDisplay = (value, max) => {
746
- const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
747
- if (opts.fullText || text.length <= max) return text
748
- return text.slice(0, max - 1).trimEnd() + '\u2026'
722
+ function isSemanticGeometry(value) {
723
+ return value !== null && typeof value === "object" && [
724
+ Reflect.get(value, "x"),
725
+ Reflect.get(value, "y"),
726
+ Reflect.get(value, "width"),
727
+ Reflect.get(value, "height")
728
+ ].every(
729
+ (coordinate) => typeof coordinate === "number" ? Number.isFinite(coordinate) : false
730
+ );
731
+ }
732
+ function isSimSemanticTree(value) {
733
+ if (!Array.isArray(value)) return false;
734
+ return value.every((node) => {
735
+ if (node === null || typeof node !== "object" || !Number.isInteger(Reflect.get(node, "nodeId")) || typeof Reflect.get(node, "type") !== "string" || !isSemanticGeometry(Reflect.get(node, "geometry"))) {
736
+ return false;
737
+ }
738
+ for (const key of ["testID", "text", "role", "label"]) {
739
+ const value2 = Reflect.get(node, key);
740
+ if (value2 !== void 0 && typeof value2 !== "string") return false;
741
+ }
742
+ const pressable = Reflect.get(node, "pressable");
743
+ if (pressable !== void 0 && pressable !== true) return false;
744
+ const children = Reflect.get(node, "children");
745
+ return children === void 0 || isSimSemanticTree(children);
746
+ });
747
+ }
748
+ function isSemanticQueryResult(value) {
749
+ const total = value !== null && typeof value === "object" ? Reflect.get(value, "total") : void 0;
750
+ return value !== null && typeof value === "object" && typeof total === "number" && Number.isInteger(total) && total >= 0 && isSimSemanticTree(Reflect.get(value, "nodes"));
751
+ }
752
+ function globRegex(glob) {
753
+ const escaped = glob.replace(/[.+^$(){}|[\]\\]/g, "\\$&");
754
+ return new RegExp(`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
755
+ }
756
+ function formatSemanticTree(nodes, opts = {}) {
757
+ const filter = opts.filter?.toLowerCase() ?? "";
758
+ const testIDRegex = opts.testIdLike ? globRegex(opts.testIdLike) : null;
759
+ const onlyRegex = opts.onlyGlob ? globRegex(opts.onlyGlob) : null;
760
+ const lines = [];
761
+ const visit = (node, indent) => {
762
+ const joined = [node.role, node.label, node.text, node.testID].filter((value) => typeof value === "string").join(" ");
763
+ const matches = (!filter || joined.toLowerCase().includes(filter)) && (!testIDRegex || testIDRegex.test(node.testID ?? "")) && (!onlyRegex || onlyRegex.test(joined));
764
+ if (matches) {
765
+ const parts = [
766
+ node.role ? `[${node.role}]` : node.pressable ? "[tap]" : `<${node.type}>`
767
+ ];
768
+ if (node.text) {
769
+ const text = opts.fullText || node.text.length <= 50 ? node.text : `${node.text.slice(0, 49).trimEnd()}\u2026`;
770
+ parts.push(JSON.stringify(text));
749
771
  }
750
- const formatInspectableTree = async () => {
751
- if (typeof t.listInspectable !== 'function') return null
752
- const list = await t.listInspectable({})
753
- if (!Array.isArray(list)) return null
754
- const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
755
- const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
756
- const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
757
- const lines = []
758
- let matched = 0
759
- for (const node of list) {
760
- const testID = node?.testID || node?.testId || ''
761
- const role = node?.role || ''
762
- const label = node?.accessibilityLabel || node?.label || ''
763
- const text = node?.text || ''
764
- if (testIdRegex && !testIdRegex.test(testID)) continue
765
- const joined = [role, label, text, testID].filter(Boolean).join(' ')
766
- if (onlyRegex && !onlyRegex.test(joined)) continue
767
- if (filter && !joined.toLowerCase().includes(filter)) continue
768
- matched++
769
-
770
- const type = String(node?.type || 'node').toLowerCase()
771
- const handlers = Array.isArray(node?.handlers) ? node.handlers : []
772
- const parts = []
773
- if (role) parts.push('[' + role + ']')
774
- else if (handlers.length > 0) parts.push('[tap]')
775
- else parts.push('<' + type + '>')
776
- const visibleText = clipForDisplay(text, 50)
777
- if (visibleText) parts.push(JSON.stringify(visibleText))
778
- if (testID) parts.push('#' + testID)
779
- if (label && label !== text) {
780
- parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
781
- }
782
- if (!opts.hideXy) {
783
- const abs = node?.absolute || {}
784
- const layout = node?.layout || {}
785
- const x = Math.round(Number(abs.x) || 0)
786
- const y = Math.round(Number(abs.y) || 0)
787
- const w = Math.round(Number(layout.width) || 0)
788
- const h = Math.round(Number(layout.height) || 0)
789
- parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
790
- }
791
- if (handlers.length > 0) parts.push('(tap)')
792
- lines.push(' ' + parts.join(' '))
793
- }
794
- return { tree: lines.join('\\n'), matched }
772
+ if (node.testID) parts.push(`#${node.testID}`);
773
+ if (node.label && node.label !== node.text) {
774
+ parts.push(`label=${JSON.stringify(node.label)}`);
795
775
  }
796
-
797
- let tree = ''
798
- const compactResult = needsRecursiveDump ? null : await formatInspectableTree()
799
- if (compactResult) {
800
- tree = compactResult.tree
801
- } else {
802
- tree = await t.dumpTree(${dumpDepth}, opts)
776
+ if (!opts.hideXy) {
777
+ const geometry = node.geometry;
778
+ parts.push(
779
+ `@(${Math.round(geometry.x)},${Math.round(geometry.y)}) ${Math.round(geometry.width)}x${Math.round(geometry.height)}`
780
+ );
803
781
  }
804
- const nodeCount = (await t.getNodeCount?.()) || 0
805
- const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
806
- return { tree, shell, nodeCount, keyboard }
807
- })()`;
808
- const result = await bridge.send({ type: "evaluate", code });
809
- return result ?? {};
782
+ if (node.pressable) parts.push("(tap)");
783
+ lines.push(`${" ".repeat(indent + 1)}${parts.join(" ")}`);
784
+ }
785
+ for (const child of node.children ?? []) visit(child, indent + 1);
786
+ };
787
+ let roots = nodes;
788
+ if (opts.subtreeRoot) {
789
+ const pending = [...nodes];
790
+ let found;
791
+ while (pending.length > 0 && !found) {
792
+ const candidate = pending.shift();
793
+ if (!candidate) continue;
794
+ if (candidate.testID === opts.subtreeRoot) found = candidate;
795
+ else pending.push(...candidate.children ?? []);
796
+ }
797
+ if (!found) return `__SUBTREE_NOT_FOUND__:${opts.subtreeRoot}`;
798
+ roots = [found];
799
+ }
800
+ for (const root of roots) visit(root, 0);
801
+ return lines.join("\n");
802
+ }
803
+ async function inspectDescribe(bridge, dumpOpts) {
804
+ const dumpDepth = dumpOpts.maxDepth ?? 12;
805
+ const result = await bridge.send({
806
+ type: "query",
807
+ query: { kind: "tree", depth: dumpDepth }
808
+ });
809
+ if (!isSemanticQueryResult(result)) return {};
810
+ return {
811
+ tree: formatSemanticTree(result.nodes, dumpOpts),
812
+ nodeCount: result.total
813
+ };
810
814
  }
811
815
  function scoreLayoutTextContrast(input) {
812
816
  const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
@@ -1021,9 +1025,6 @@ function layoutGetNativeEval(opts = {}) {
1021
1025
  type: typeof n.type === 'string' ? n.type : 'node',
1022
1026
  text,
1023
1027
  box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
1024
- borderRadius: 0,
1025
- padding: { t: 0, r: 0, b: 0, l: 0 },
1026
- fontSize: 0,
1027
1028
  }
1028
1029
  if (!includeStyle) return base
1029
1030
  // computedStyle has the resolved values; style is the curated subset \u2014
@@ -1094,6 +1095,32 @@ function layoutGetNativeEval(opts = {}) {
1094
1095
  }
1095
1096
  var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
1096
1097
  async function inspectGetLayout(bridge, opts = {}) {
1098
+ if (!opts.styling) {
1099
+ const result = await bridge.send({
1100
+ type: "query",
1101
+ query: { kind: "find", selector: { visible: true } }
1102
+ });
1103
+ if (!isSemanticQueryResult(result)) return [];
1104
+ const filter = opts.filter?.trim().toLowerCase() ?? "";
1105
+ return result.nodes.filter((node) => {
1106
+ if (!filter) return true;
1107
+ return [node.text, node.testID, node.role, node.type].some(
1108
+ (value) => typeof value === "string" && value.toLowerCase().includes(filter)
1109
+ );
1110
+ }).map((node) => ({
1111
+ nodeId: node.nodeId,
1112
+ ...node.testID ? { testID: node.testID } : {},
1113
+ ...node.role ? { role: node.role } : {},
1114
+ type: node.type,
1115
+ ...node.text ? { text: node.text } : {},
1116
+ box: {
1117
+ x: Math.round(node.geometry.x),
1118
+ y: Math.round(node.geometry.y),
1119
+ w: Math.round(node.geometry.width),
1120
+ h: Math.round(node.geometry.height)
1121
+ }
1122
+ }));
1123
+ }
1097
1124
  const raw = await bridge.send({
1098
1125
  type: "evaluate",
1099
1126
  code: layoutGetNativeEval(opts)
@@ -1105,13 +1132,17 @@ function formatLayoutElements(elements, opts) {
1105
1132
  const lines = elements.map((el) => {
1106
1133
  const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
1107
1134
  const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
1108
- if (el.borderRadius > 0) parts.push(`radius:${el.borderRadius}`);
1135
+ if (el.borderRadius !== void 0 && el.borderRadius > 0) {
1136
+ parts.push(`radius:${el.borderRadius}`);
1137
+ }
1109
1138
  const p = el.padding;
1110
- if (p.t || p.r || p.b || p.l) {
1139
+ if (p && (p.t || p.r || p.b || p.l)) {
1111
1140
  const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
1112
1141
  parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
1113
1142
  }
1114
- if (el.fontSize > 0) parts.push(`font:${el.fontSize}`);
1143
+ if (el.fontSize !== void 0 && el.fontSize > 0) {
1144
+ parts.push(`font:${el.fontSize}`);
1145
+ }
1115
1146
  if (opts?.styling) {
1116
1147
  if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
1117
1148
  if (el.color) parts.push(`color:${el.color}`);
@@ -1138,246 +1169,80 @@ function formatLayoutElements(elements, opts) {
1138
1169
  });
1139
1170
  return lines.join("\n");
1140
1171
  }
1141
- var INSPECT_A11Y_CODE = `(async () => {
1142
- const t = window.__sootsimTest
1143
- if (!t) return []
1144
-
1145
- const readLayout = (n) => n?.layout || null
1146
- const readAbs = (n) => n?.absolutePosition || n?.absolute || null
1147
- const readRole = (n) => n?.accessibilityRole || n?.role || null
1148
- const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
1149
- const readHint = (n) => n?.accessibilityHint || n?.hint || null
1150
- const readState = (n) => n?.accessibilityState || n?.state || null
1151
- const readTestId = (n) => n?.testID || n?.testId || null
1152
- const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
1153
- const readPressable = (n) => {
1154
- if (n?.pressable) return true
1155
- const handlers = Array.isArray(n?.handlers) ? n.handlers : []
1156
- const role = readRole(n)
1157
- return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
1158
- }
1159
- const isTextInput = (n) => n?.isTextInput === true
1160
- const isTextNode = (n) => readType(n) === 'text'
1161
- const isVisibleTarget = (n) => {
1162
- const layout = readLayout(n)
1163
- if (!layout || layout.width <= 0 || layout.height <= 0) return false
1164
- const abs = readAbs(n)
1165
- if (!abs) return true
1166
- const device = n?.device || {}
1167
- const screenW = Number(device.width) || window.innerWidth || 0
1168
- const screenH = Number(device.height) || window.innerHeight || 0
1169
- return (
1170
- abs.x + layout.width > 0 &&
1171
- abs.y + layout.height > 0 &&
1172
- abs.x < screenW &&
1173
- abs.y < screenH
1174
- )
1175
- }
1176
- const hasAccessibleSignal = (n) => {
1177
- const role = readRole(n)
1178
- const label = readLabel(n)
1179
- const hint = readHint(n)
1180
- if (role) return true
1181
- if (hint) return true
1182
- if (isTextInput(n)) return true
1183
- if (readPressable(n)) return true
1184
- if (isTextNode(n) && n?.text) return true
1185
- if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
1186
- return false
1187
- }
1188
- const normalize = (n) => {
1189
- const layout = readLayout(n)
1190
- const abs = readAbs(n)
1191
- const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
1192
- return {
1193
- role,
1194
- label: readLabel(n),
1195
- hint: readHint(n),
1196
- state: readState(n),
1197
- testID: readTestId(n),
1198
- position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
1199
- size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
1200
- }
1201
- }
1202
-
1203
- if (typeof t.listInspectable === 'function') {
1204
- const list = await t.listInspectable({})
1205
- if (Array.isArray(list)) {
1206
- return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
1207
- }
1208
- }
1209
-
1210
- const all = await t.queryAll({ pruneHidden: true })
1211
- return all
1212
- .filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
1213
- .map(normalize)
1214
- })()`;
1215
1172
  async function inspectAccessibilityTree(bridge) {
1216
- const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
1217
- return Array.isArray(nodes) ? nodes : [];
1218
- }
1219
- var FIND_INSPECTABLE_TARGETS = `
1220
- const fromInspectable = async () => {
1221
- if (typeof t.listInspectable !== 'function') return null
1222
- const list = await t.listInspectable({})
1223
- if (!Array.isArray(list)) return null
1224
- return list.map((n) => {
1225
- const role = n.accessibilityRole ?? n.role
1226
- const label = n.accessibilityLabel ?? n.label
1227
- const layout = n.layout
1228
- const absolutePosition = n.absolutePosition ?? n.absolute
1229
- const screenW = Number(n.device?.width) || window.innerWidth || 0
1230
- const screenH = Number(n.device?.height) || window.innerHeight || 0
1231
- const visibleFrame = layout && absolutePosition
1232
- ? {
1233
- x: Math.max(0, absolutePosition.x),
1234
- y: Math.max(0, absolutePosition.y),
1235
- width: Math.max(
1236
- 0,
1237
- Math.min(screenW, absolutePosition.x + layout.width) -
1238
- Math.max(0, absolutePosition.x),
1239
- ),
1240
- height: Math.max(
1241
- 0,
1242
- Math.min(screenH, absolutePosition.y + layout.height) -
1243
- Math.max(0, absolutePosition.y),
1244
- ),
1245
- }
1246
- : null
1247
- const handlers = Array.isArray(n.handlers) ? n.handlers : []
1248
- const pressable =
1249
- handlers.length > 0 ||
1250
- role === 'button' ||
1251
- role === 'link' ||
1252
- role === 'tab'
1253
- return {
1254
- type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
1255
- id: n.id ?? n.nodeId,
1256
- nodeId: n.nodeId,
1257
- testID: n.testID ?? n.testId,
1258
- text: n.text,
1259
- layout,
1260
- absolutePosition,
1261
- visibleFrame,
1262
- style: n.computedStyle ?? n.style ?? {},
1263
- childCount: n.childCount ?? 0,
1264
- pressed: n.pressed ?? false,
1265
- pressable,
1266
- isTextInput: n.isTextInput ?? false,
1267
- accessible: n.accessible ?? true,
1268
- accessibilityLabel: label,
1269
- accessibilityRole: role,
1270
- accessibilityHint: n.accessibilityHint,
1271
- accessibilityState: n.accessibilityState,
1272
- accessibilityValue: n.accessibilityValue,
1273
- }
1274
- })
1275
- }
1276
- const isVisibleTarget = (n) => {
1277
- if (!n?.layout) return false
1278
- const frame = n.visibleFrame
1279
- if (frame) return frame.width > 0 && frame.height > 0
1280
- const abs = n.absolutePosition
1281
- if (!abs) return n.layout.width > 0 && n.layout.height > 0
1282
- const screenW = window.innerWidth || 0
1283
- const screenH = window.innerHeight || 0
1284
- return (
1285
- abs.x + n.layout.width > 0 &&
1286
- abs.y + n.layout.height > 0 &&
1287
- abs.x < screenW &&
1288
- abs.y < screenH
1289
- )
1173
+ const result = await bridge.send({
1174
+ type: "query",
1175
+ query: { kind: "find", selector: { visible: true } }
1176
+ });
1177
+ if (!isSemanticQueryResult(result)) return [];
1178
+ return result.nodes.flatMap((node) => {
1179
+ if (!node.role && !node.label && !node.text && !node.pressable) return [];
1180
+ return [
1181
+ {
1182
+ role: node.role ?? (node.pressable ? "button" : node.type === "text" ? "statictext" : "none"),
1183
+ label: node.label ?? node.text ?? null,
1184
+ hint: null,
1185
+ state: null,
1186
+ testID: node.testID ?? null,
1187
+ position: {
1188
+ x: Math.round(node.geometry.x),
1189
+ y: Math.round(node.geometry.y)
1190
+ },
1191
+ size: {
1192
+ w: Math.round(node.geometry.width),
1193
+ h: Math.round(node.geometry.height)
1194
+ }
1290
1195
  }
1291
- `;
1196
+ ];
1197
+ });
1198
+ }
1292
1199
  function resolveFindMode(q) {
1293
1200
  if (q.testId) {
1294
- return {
1295
- mode: "testid",
1296
- code: `(async () => {
1297
- const t = window.__sootsimTest
1298
- if (!t) return null
1299
- return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
1300
- })()`
1301
- };
1201
+ return { mode: "testid", selector: { testID: q.testId } };
1302
1202
  }
1303
1203
  if (q.role) {
1304
- return {
1305
- mode: "role",
1306
- code: `(async () => {
1307
- const t = window.__sootsimTest
1308
- if (!t) return []
1309
- return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
1310
- })()`
1311
- };
1204
+ return { mode: "role", selector: { role: q.role } };
1312
1205
  }
1313
1206
  if (q.type) {
1314
- return {
1315
- mode: "type",
1316
- code: `(async () => {
1317
- const t = window.__sootsimTest
1318
- if (!t) return []
1319
- return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
1320
- })()`
1321
- };
1207
+ return { mode: "type", selector: { type: q.type } };
1322
1208
  }
1323
1209
  if (q.pressable) {
1324
- return {
1325
- mode: "pressable",
1326
- code: `(async () => {
1327
- const t = window.__sootsimTest
1328
- if (!t) return []
1329
- ${FIND_INSPECTABLE_TARGETS}
1330
- const inspectable = await fromInspectable()
1331
- if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
1332
- const all = await t.queryAll({ pruneHidden: true })
1333
- return all.filter(n => n.pressable && isVisibleTarget(n))
1334
- })()`
1335
- };
1210
+ return { mode: "pressable", selector: { pressable: true } };
1336
1211
  }
1337
1212
  if (q.interactive) {
1338
- return {
1339
- mode: "interactive-targets",
1340
- code: `(async () => {
1341
- const t = window.__sootsimTest
1342
- if (!t) return []
1343
- ${FIND_INSPECTABLE_TARGETS}
1344
- const inspectable = await fromInspectable()
1345
- if (inspectable) {
1346
- return inspectable.filter(n => n.pressable && isVisibleTarget(n))
1347
- }
1348
- const all = await t.queryAll({ pruneHidden: true })
1349
- return all.filter(n => n.pressable && isVisibleTarget(n))
1350
- })()`
1351
- };
1213
+ return { mode: "interactive-targets", selector: { pressable: true } };
1352
1214
  }
1353
1215
  if (q.visible) {
1354
- return {
1355
- mode: "visible",
1356
- code: `(async () => {
1357
- const t = window.__sootsimTest
1358
- if (!t) return []
1359
- const all = await t.queryAll({ pruneHidden: true })
1360
- return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
1361
- })()`
1362
- };
1216
+ return { mode: "visible", selector: { visible: true } };
1363
1217
  }
1364
1218
  if (q.text) {
1365
- return {
1366
- mode: "text",
1367
- code: `(async () => {
1368
- const t = window.__sootsimTest
1369
- if (!t) return null
1370
- return await t.findByText(${JSON.stringify(q.text)})
1371
- })()`
1372
- };
1219
+ return { mode: "text", selector: { text: q.text } };
1373
1220
  }
1374
1221
  return null;
1375
1222
  }
1376
1223
  async function inspectFind(bridge, q) {
1377
1224
  const resolved = resolveFindMode(q);
1378
1225
  if (!resolved) return null;
1379
- const result = await bridge.send({ type: "evaluate", code: resolved.code });
1380
- return { mode: resolved.mode, result };
1226
+ const result = await bridge.send({
1227
+ type: "query",
1228
+ query: { kind: "find", selector: resolved.selector }
1229
+ });
1230
+ if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
1231
+ const nodes = result.nodes.map(
1232
+ (node) => ({
1233
+ type: node.type,
1234
+ nodeId: node.nodeId,
1235
+ ...node.testID ? { testID: node.testID } : {},
1236
+ ...node.text ? { text: node.text } : {},
1237
+ ...node.pressable ? { pressable: true } : {},
1238
+ ...node.role ? { accessibilityRole: node.role } : {},
1239
+ ...node.label ? { accessibilityLabel: node.label } : {},
1240
+ absolutePosition: { x: node.geometry.x, y: node.geometry.y },
1241
+ layout: { width: node.geometry.width, height: node.geometry.height }
1242
+ })
1243
+ );
1244
+ const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
1245
+ return { mode: resolved.mode, result: resultValue };
1381
1246
  }
1382
1247
  function rankInteractive(nodes) {
1383
1248
  return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
@@ -1411,158 +1276,6 @@ function shellEscape(value) {
1411
1276
  if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
1412
1277
  return `'${value.replace(/'/g, `'\\''`)}'`;
1413
1278
  }
1414
- var WAIT_READY_PROBE = `(async () => {
1415
- const t = window.__sootsimTest
1416
- let nodes = 0
1417
- try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
1418
- let targets = 0
1419
- let liveFrameActive = false
1420
- let liveFrameChannels = 0
1421
- let liveFramePublishes = 0
1422
- let loadingText = ''
1423
- const cleanText = (value) =>
1424
- typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
1425
- const loadingSignal = (value) => {
1426
- const text = cleanText(value)
1427
- if (!text) return ''
1428
- return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnx runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
1429
- ? text
1430
- : ''
1431
- }
1432
- try {
1433
- const loading = window.__sootsimConnectRNLoading
1434
- if (loading && loading.active === true) {
1435
- loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
1436
- }
1437
- } catch {}
1438
- try {
1439
- if (typeof t?.listInspectable === 'function') {
1440
- const list = await t.listInspectable({})
1441
- if (Array.isArray(list)) {
1442
- const screenW = window.innerWidth || 0
1443
- const screenH = window.innerHeight || 0
1444
- const isVisible = (n) => {
1445
- const layout = n?.layout
1446
- if (!layout || layout.width <= 0 || layout.height <= 0) return false
1447
- const abs = n?.absolutePosition || n?.absolute
1448
- if (!abs) return true
1449
- const device = n?.device || {}
1450
- const w = Number(device.width) || screenW
1451
- const h = Number(device.height) || screenH
1452
- return (
1453
- abs.x + layout.width > 0 &&
1454
- abs.y + layout.height > 0 &&
1455
- abs.x < w &&
1456
- abs.y < h
1457
- )
1458
- }
1459
- const hasContentSignal = (n) => {
1460
- const role = n?.accessibilityRole || n?.role
1461
- const label = cleanText(n?.accessibilityLabel || n?.label)
1462
- const handlers = Array.isArray(n?.handlers) ? n.handlers : []
1463
- const text = cleanText(n?.text)
1464
- const testID = cleanText(n?.testID || n?.testId)
1465
- const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
1466
- if (!loadingText && loading) {
1467
- loadingText = loading
1468
- }
1469
- if (handlers.length > 0) return true
1470
- if (role) return true
1471
- if (n?.isTextInput) return true
1472
- if (text) return true
1473
- if (label) return true
1474
- if (testID && !/^splash|loading/i.test(testID)) return true
1475
- return false
1476
- }
1477
- targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
1478
- }
1479
- }
1480
- } catch {}
1481
- try {
1482
- const stats = await t?.getImageLoaderStats?.()
1483
- const presentation = stats?.liveFramePresentation
1484
- liveFrameChannels = Number(presentation?.activeChannels) || 0
1485
- liveFramePublishes = Number(presentation?.publishes) || 0
1486
- const previous = window.__sootsimWaitReadyLiveFramePublishes
1487
- liveFrameActive =
1488
- liveFrameChannels > 0 &&
1489
- typeof previous === 'number' &&
1490
- liveFramePublishes > previous
1491
- window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
1492
- } catch {}
1493
- let externalReady = null
1494
- let externalStatus = ''
1495
- let externalError = ''
1496
- let suppressedEntryError = ''
1497
- try {
1498
- const getExternalAppState = t?.getExternalAppState
1499
- const external = typeof getExternalAppState === 'function'
1500
- ? await Promise.race([
1501
- getExternalAppState(),
1502
- new Promise((resolve) => setTimeout(
1503
- () => resolve({
1504
- state: {
1505
- ready: false,
1506
- loading: true,
1507
- status: 'waiting for tenant worker...',
1508
- },
1509
- }),
1510
- 1000,
1511
- )),
1512
- ])
1513
- : null
1514
- const state = external?.state
1515
- if (state && typeof state === 'object') {
1516
- externalReady = state.ready === true
1517
- if (typeof state.status === 'string') externalStatus = state.status
1518
- if (typeof state.error === 'string') externalError = state.error
1519
- }
1520
- if (!externalError && Array.isArray(external?.entryErrors)) {
1521
- // suppressed-and-continued entry errors (bundle-loader's patched __r
1522
- // swallows entry throws and moves on) must NOT gate readiness \u2014 the
1523
- // app frequently runs fine after one (e.g. metro's "Unknown named
1524
- // module" probing noise). they surface separately as a diagnostic
1525
- // for the timeout path.
1526
- const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
1527
- const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
1528
- if (fatal) externalError = fatal.message
1529
- const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
1530
- if (suppressed) suppressedEntryError = suppressed.message
1531
- }
1532
- // once the app has errored its last status ("injecting sootsim
1533
- // modules...") is stale \u2014 reporting it makes a dead app read as one that
1534
- // is still booting.
1535
- if (externalError) loadingText = ''
1536
- else if (externalReady === false && externalStatus) loadingText = externalStatus
1537
- } catch {}
1538
- let errors = 0
1539
- try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
1540
- return {
1541
- flag: (window).__sootsimExternalAppReady,
1542
- at: (window).__sootsimExternalAppReadyAt || 0,
1543
- nodes,
1544
- targets,
1545
- liveFrameActive,
1546
- liveFrameChannels,
1547
- liveFramePublishes,
1548
- errors,
1549
- loadingText,
1550
- externalReady,
1551
- externalStatus,
1552
- externalError,
1553
- suppressedEntryError,
1554
- }
1555
- })()`;
1556
- var READY_CONTENT_NODE_FLOOR = 100;
1557
- var READY_NODE_STABLE_MS = 750;
1558
- var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
1559
- var READY_PROBE_POLL_MS = 150;
1560
- function readyProbeHasContent(probe) {
1561
- return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
1562
- }
1563
- function readyProbeHasTargetContent(probe) {
1564
- return probe.liveFrameActive || probe.targets > 0;
1565
- }
1566
1279
  function waitReadyReason(status) {
1567
1280
  if (status.bridgeError) {
1568
1281
  return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
@@ -1572,155 +1285,69 @@ function waitReadyReason(status) {
1572
1285
  }
1573
1286
  async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
1574
1287
  const start = Date.now();
1575
- const deadline = start + timeoutMs;
1576
- const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
1577
- let nextProgressAt = start + progressIntervalMs;
1578
- let lastNodes = -1;
1579
- let nodeStableSince = start;
1580
- let bridgeError = "";
1581
- let last = {
1582
- flag: void 0,
1583
- at: 0,
1584
- nodes: 0,
1585
- targets: 0,
1586
- liveFrameActive: false,
1587
- liveFrameChannels: 0,
1588
- liveFramePublishes: 0,
1589
- errors: 0,
1590
- loadingText: "",
1591
- externalReady: null,
1592
- externalStatus: "",
1593
- externalError: "",
1594
- suppressedEntryError: ""
1595
- };
1596
- while (Date.now() < deadline) {
1597
- try {
1598
- last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
1599
- bridgeError = "";
1600
- } catch (error) {
1601
- bridgeError = error instanceof Error ? error.message : String(error);
1602
- if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
1603
- throw error;
1604
- }
1288
+ let result;
1289
+ try {
1290
+ result = await bridge.send(
1291
+ {
1292
+ type: "waitFor",
1293
+ waitForOptions: { condition: { type: "ready" }, timeoutMs }
1294
+ },
1295
+ { timeoutMs: timeoutMs + 1e3 }
1296
+ );
1297
+ } catch (error) {
1298
+ const bridgeError = error instanceof Error ? error.message : String(error);
1299
+ if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
1300
+ throw error;
1605
1301
  }
1606
- const now = Date.now();
1607
- const status2 = {
1608
- ready: false,
1609
- elapsedMs: now - start,
1610
- nodes: last.nodes,
1611
- targets: last.targets,
1612
- liveFrameActive: last.liveFrameActive,
1613
- liveFrameChannels: last.liveFrameChannels,
1614
- liveFramePublishes: last.liveFramePublishes,
1615
- flag: last.flag,
1616
- loadingText: last.loadingText,
1617
- externalReady: last.externalReady,
1618
- externalStatus: last.externalStatus,
1619
- externalError: last.externalError,
1620
- suppressedEntryError: last.suppressedEntryError,
1621
- errors: last.errors,
1622
- bridgeError
1302
+ result = {
1303
+ matched: false,
1304
+ elapsedMs: Date.now() - start,
1305
+ polls: 0,
1306
+ error: bridgeError
1623
1307
  };
1624
- if (last.nodes !== lastNodes) {
1625
- lastNodes = last.nodes;
1626
- nodeStableSince = now;
1627
- }
1628
- const hasReadySignal = last.flag === true && readyProbeHasContent(last);
1629
- const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
1630
- if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
1631
- if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
1632
- return {
1633
- ...status2,
1634
- ready: true
1635
- };
1636
- }
1637
- }
1638
- if (last.externalError) return status2;
1639
- if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
1640
- options.onProgress(status2);
1641
- do {
1642
- nextProgressAt += progressIntervalMs;
1643
- } while (now >= nextProgressAt);
1644
- }
1645
- await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
1646
1308
  }
1309
+ const probe = result.probe;
1647
1310
  const status = {
1648
- ready: false,
1649
- elapsedMs: Date.now() - start,
1650
- nodes: last.nodes,
1651
- targets: last.targets,
1652
- liveFrameActive: last.liveFrameActive,
1653
- liveFrameChannels: last.liveFrameChannels,
1654
- liveFramePublishes: last.liveFramePublishes,
1655
- flag: last.flag,
1656
- loadingText: last.loadingText,
1657
- externalReady: last.externalReady,
1658
- externalStatus: last.externalStatus,
1659
- externalError: last.externalError,
1660
- suppressedEntryError: last.suppressedEntryError,
1661
- errors: last.errors,
1662
- bridgeError
1311
+ ready: result.matched,
1312
+ elapsedMs: result.elapsedMs,
1313
+ nodes: probe?.nodes ?? 0,
1314
+ targets: probe?.targets ?? 0,
1315
+ liveFrameActive: probe?.liveFrameActive ?? false,
1316
+ liveFrameChannels: probe?.liveFrameChannels ?? 0,
1317
+ liveFramePublishes: probe?.liveFramePublishes ?? 0,
1318
+ flag: probe?.flag,
1319
+ loadingText: probe?.loadingText ?? "",
1320
+ externalReady: probe?.externalReady ?? null,
1321
+ externalStatus: probe?.externalStatus ?? "",
1322
+ externalError: probe?.externalError ?? "",
1323
+ suppressedEntryError: probe?.suppressedEntryError ?? "",
1324
+ errors: probe?.errors ?? 0,
1325
+ bridgeError: result.error ?? ""
1663
1326
  };
1664
- if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
1665
- options.onProgress(status);
1666
- }
1327
+ if (!status.ready && options.onProgress) options.onProgress(status);
1667
1328
  return status;
1668
1329
  }
1669
1330
  async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
1670
1331
  const gone = opts.gone === true;
1671
1332
  const result = await bridge.send(
1672
1333
  {
1673
- type: "evaluate",
1674
- code: `(async () => {
1675
- const start = Date.now()
1676
- const deadline = start + ${timeoutMs}
1677
- const gone = ${gone}
1678
- const ID = ${JSON.stringify(testId)}
1679
- const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
1680
- const find = async () => {
1681
- const t = window.__sootsimTest
1682
- if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
1683
- try {
1684
- // gone-mode confirms a gate/screen LEFT \u2014 it only needs structural
1685
- // existence + layout, NOT the full-tree occlusion pass (pruneHidden).
1686
- // that pass (computeOccludedInspectNodeIds) walks the whole tree and
1687
- // does O(n\xB2)-ish rect-occlusion testing EVERY poll; on a large
1688
- // post-login app tree it costs seconds per poll, so --gone can never
1689
- // get two clean "absent" polls inside budget and falsely reports the
1690
- // gate as still blocked \u2014 the root cause of the 3pc PR-preview outage.
1691
- // presence-mode keeps pruneHidden so "appeared" still means
1692
- // visibly-reachable. a departed gate is unmounted or zero-sized, so the
1693
- // cheap structural+layout check is correct here and ~O(n) cheap.
1694
- const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
1695
- if (!Array.isArray(matches)) return undefined
1696
- return matches.find(present) || null
1697
- } catch {
1698
- return undefined
1699
- }
1700
- }
1701
- let goneStreak = 0
1702
- while (Date.now() < deadline) {
1703
- const node = await find()
1704
- if (gone) {
1705
- if (node === null) {
1706
- goneStreak += 1
1707
- if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
1708
- } else {
1709
- goneStreak = 0
1710
- }
1711
- } else if (present(node)) {
1712
- return { found: true, node, elapsed: Date.now() - start }
1713
- }
1714
- await new Promise((r) => setTimeout(r, 80))
1334
+ type: "waitFor",
1335
+ waitForOptions: {
1336
+ condition: { type: gone ? "absent" : "present", testID: testId },
1337
+ timeoutMs
1715
1338
  }
1716
- return { found: false, elapsed: Date.now() - start }
1717
- })()`
1718
1339
  },
1719
- {
1720
- timeoutMs: timeoutMs + 1e3
1721
- }
1340
+ { timeoutMs: timeoutMs + 1e3 }
1722
1341
  );
1723
- return result ?? { found: false, elapsed: timeoutMs };
1342
+ if (result?.error) throw new Error(result.error);
1343
+ if (result?.node && !isSimSemanticTree([result.node])) {
1344
+ throw new Error("waitFor returned an invalid semantic node");
1345
+ }
1346
+ return {
1347
+ found: result?.matched === true,
1348
+ ...result?.node ? { node: result.node } : {},
1349
+ elapsed: result?.elapsedMs ?? timeoutMs
1350
+ };
1724
1351
  }
1725
1352
  function consoleEntriesEval(level, limit) {
1726
1353
  const getter = level === "error" ? "getErrors" : "getWarnings";
@@ -2183,38 +1810,30 @@ function formatTimelineEvents(events) {
2183
1810
 
2184
1811
  // cli/commands/inspect/resolve-target.ts
2185
1812
  async function resolveTargetCoords(bridge, target) {
2186
- const arg = JSON.stringify(target.value);
2187
- const findExpr = target.mode === "testid" ? `(await t.findByTestId(${arg})) || (await t.findById(${arg}))` : `await t.findByText(${arg})`;
2188
1813
  const result = await bridge.send({
2189
- type: "evaluate",
2190
- code: `(async () => {
2191
- const t = window.__sootsimTest
2192
- if (!t) return null
2193
- const n = ${findExpr}
2194
- if (!n || !n.absolutePosition || !n.layout) return null
2195
- const resolved =
2196
- typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
2197
- ? await t.resolveTapTarget(n.nodeId)
2198
- : null
2199
- const cx =
2200
- typeof resolved?.cx === 'number'
2201
- ? resolved.cx
2202
- : n.absolutePosition.x + (n.layout.width || 0) / 2
2203
- const cy =
2204
- typeof resolved?.cy === 'number'
2205
- ? resolved.cy
2206
- : n.absolutePosition.y + (n.layout.height || 0) / 2
2207
- return {
2208
- x: cx,
2209
- y: cy,
2210
- id: n.id ?? null,
2211
- testID: n.testID ?? null,
2212
- text: ${JSON.stringify(target.mode === "text")} ? ${arg} : (n.text ?? n.accessibilityLabel ?? null),
2213
- type: n.type ?? null,
2214
- }
2215
- })()`
1814
+ type: "resolve",
1815
+ selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
2216
1816
  });
2217
- return result ?? null;
1817
+ if (result === null) return null;
1818
+ if (typeof result !== "object") throw new Error("resolve returned an invalid result");
1819
+ const match = Reflect.get(result, "match");
1820
+ const resolvedTarget = Reflect.get(result, "target");
1821
+ const semanticNodes = [match, resolvedTarget];
1822
+ const point = Reflect.get(result, "point");
1823
+ const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
1824
+ const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
1825
+ if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
1826
+ throw new Error("resolve returned an invalid result");
1827
+ }
1828
+ const semanticMatch = semanticNodes[0];
1829
+ if (!semanticMatch) throw new Error("resolve returned an invalid result");
1830
+ return {
1831
+ x,
1832
+ y,
1833
+ testID: semanticMatch.testID ?? null,
1834
+ text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
1835
+ type: semanticMatch.type
1836
+ };
2218
1837
  }
2219
1838
 
2220
1839
  // src/app-url.ts
@@ -2452,9 +2071,12 @@ var SimClient = class {
2452
2071
  }
2453
2072
  async tree(depth = 5) {
2454
2073
  const { tree } = await inspectTree(this, depth);
2455
- return tree;
2074
+ return Array.isArray(tree) ? tree : [];
2075
+ }
2076
+ resolve(selector) {
2077
+ return this.send({ type: "resolve", selector });
2456
2078
  }
2457
- /** compact visible-UI dump with shell + keyboard state, as the CLI prints. */
2079
+ /** compact visible semantic tree, formatted as the CLI prints it. */
2458
2080
  describe(opts = {}) {
2459
2081
  return inspectDescribe(this, {
2460
2082
  describe: true,
@@ -2683,9 +2305,6 @@ export {
2683
2305
  LAYOUT_GET_NATIVE_EVAL,
2684
2306
  MERGED_CONSOLE_COUNT_EVAL,
2685
2307
  NOISY_TIMELINE_KINDS,
2686
- READY_CONTENT_NODE_FLOOR,
2687
- READY_NODE_STABLE_MS,
2688
- READY_PROBE_POLL_MS,
2689
2308
  SimClient,
2690
2309
  SimClientError,
2691
2310
  SimConnectError,
@@ -2694,8 +2313,6 @@ export {
2694
2313
  SimReadyTimeoutError,
2695
2314
  SimResetError,
2696
2315
  SimTapError,
2697
- WAIT_READY_PROBE,
2698
- WAIT_READY_PROGRESS_INTERVAL_MS,
2699
2316
  buildSimOpenUrl,
2700
2317
  clearConsole,
2701
2318
  clearLogs,
@@ -2703,6 +2320,7 @@ export {
2703
2320
  detectOpenNativeUI,
2704
2321
  filterLogEntries,
2705
2322
  formatLayoutElements,
2323
+ formatSemanticTree,
2706
2324
  formatTimelineEvent,
2707
2325
  formatTimelineEvents,
2708
2326
  formatTimelinePayload,
@@ -2735,6 +2353,7 @@ export {
2735
2353
  isInspectModeActive,
2736
2354
  isInspectPickCommand,
2737
2355
  isShellCommandUnavailable,
2356
+ isSimSemanticTree,
2738
2357
  isTapSuccess,
2739
2358
  layoutGetNativeEval,
2740
2359
  rankInteractive,