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
@@ -42,6 +42,9 @@ automation, inspecting the render tree, and managing sims.
42
42
  | `rnx debug` | drive __sootsimDebug: channels, snapshots, and inspectors |
43
43
  | `rnx what-happened` | show recent semantic events: toasts, keyboards, screens, alerts, fetches, errors |
44
44
  | `rnx timeline` | control the semantic event timeline (enable opt-in kinds, clear, dump) |
45
+ | `rnx box` | start a local box and enter its shell |
46
+ | `rnx ios` | start an iOS simulator |
47
+ | `rnx android` | start an Android simulator |
45
48
  | `rnx open` | load a target into the current sim or open a new one |
46
49
  | `rnx list` | list connected sims or available launch drivers |
47
50
  | `rnx use` | select a connected sim |
@@ -147,6 +147,14 @@ rnx debug diff before after # structural diff: what changed?
147
147
  rnx what-happened # timeline events during the repro
148
148
  ```
149
149
 
150
+ when the repro needs several closely timed inputs, replace the individual
151
+ write calls with one inline batch so WebSocket scheduling cannot change the
152
+ gesture:
153
+
154
+ ```sh
155
+ rnx do touch down 200 600 --then sleep 0.03 --then touch move 200 250 --then touch up 200 250
156
+ ```
157
+
150
158
  for nested scroll ownership or momentum transfer, compare the shell's resolved
151
159
  gesture chain with the published scroll registry:
152
160
 
@@ -21,6 +21,7 @@ the official Vite or Metro plugin, and test-only work must not open that window.
21
21
  - have an existing `.maestro/` directory? → [Maestro](#branch--maestro)
22
22
  - have existing Detox specs? → [detox drop-in](#branch--detox-drop-in)
23
23
  - writing tests fresh, want plain-language generation, or want live authoring? → [Maestro](#branch--maestro)
24
+ - need one short, timing-sensitive sequence from the shell? → [inline chains](#one-off-inline-chains)
24
25
 
25
26
  Do not invent a third runner for how a Maestro flow was authored.
26
27
 
@@ -43,6 +44,8 @@ Do not invent a third runner for how a Maestro flow was authored.
43
44
  - **don't run a 40-step "smoke everything" flow.** when it fails, you
44
45
  can't tell what regressed. split into `login.yaml`, `compose.yaml`,
45
46
  `settings.yaml`, chain with `runFlow`.
47
+ - **inside an `rnx do` chain, use `sleep` only for gesture timing.** after the
48
+ chain, use `rnx wait` for app state so automation advances on a real condition.
46
49
 
47
50
  ## you're done when
48
51
 
@@ -52,6 +55,24 @@ Do not invent a third runner for how a Maestro flow was authored.
52
55
  - the green run took noticeably less wall-clock than the previous
53
56
  baseline (a suite that grows slower without growing wider is a smell)
54
57
 
58
+ ## one-off inline chains
59
+
60
+ repeat `--then` before each additional `do` action to run one ordered engine
61
+ batch. use a chain for a short repro, form fill, or timing-sensitive gesture
62
+ that does not belong in a saved test:
63
+
64
+ ```sh
65
+ rnx do tap email --then type user@example.com --then tap submit
66
+ rnx wait selector dashboard
67
+ ```
68
+
69
+ the batch stops on the first failure. add `--json` for per-action results.
70
+ `tap <id>` and `tap-id <id>` are exact; use `tap-text <text>` for visible text.
71
+ punctuation in action data remains data. the exact `--then` token is reserved
72
+ as the action boundary. use Maestro when the sequence should be reviewed,
73
+ reused, or run in CI; an inline chain is not a third test runner and does not
74
+ read flow files.
75
+
55
76
  ## branch — Maestro
56
77
 
57
78
  Write declarative Maestro YAML. The same command runs existing suites,
@@ -0,0 +1,359 @@
1
+ // untrusted JSON -> bridge-contract argument. one owner for that conversion, so
2
+ // every transport in front of a sim (the RNX Cloud sim service's HTTP and
3
+ // WebSocket routes) validates its request bodies here rather than growing a
4
+ // second reader for the same shapes.
5
+
6
+ import type {
7
+ ResetOptions,
8
+ SemanticWaitOptions,
9
+ SimSemanticQuery,
10
+ SimSemanticResolveSelector,
11
+ SimSemanticSelector,
12
+ SimStateOptions,
13
+ } from './bridge-contract'
14
+ import type { SimPerformStep, SimScreenshotOptions } from './sim-client'
15
+
16
+ /** the caller sent a value this bridge argument cannot be read from. */
17
+ export class BridgeInputError extends Error {
18
+ constructor(message: string) {
19
+ super(message)
20
+ this.name = 'BridgeInputError'
21
+ }
22
+ }
23
+
24
+ export const SEMANTIC_WAIT_MAX_TIMEOUT_MS = 30_000
25
+
26
+ function validationFailure(path: string, expectation: string): never {
27
+ throw new BridgeInputError(`${path} ${expectation}`)
28
+ }
29
+
30
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
31
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
32
+ }
33
+
34
+ function recordValue(value: unknown, path: string): Record<string, unknown> {
35
+ if (!isRecordValue(value)) {
36
+ validationFailure(path, 'must be an object')
37
+ }
38
+ return value
39
+ }
40
+
41
+ function stringValue(value: unknown, path: string): string {
42
+ if (typeof value !== 'string') validationFailure(path, 'must be a string')
43
+ return value
44
+ }
45
+
46
+ function numberValue(value: unknown, path: string): number {
47
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
48
+ validationFailure(path, 'must be a finite number')
49
+ }
50
+ return value
51
+ }
52
+
53
+ function booleanValue(value: unknown, path: string): boolean {
54
+ if (typeof value !== 'boolean') validationFailure(path, 'must be a boolean')
55
+ return value
56
+ }
57
+
58
+ function optionalNumber(
59
+ record: Record<string, unknown>,
60
+ key: string,
61
+ path: string,
62
+ ): number | undefined {
63
+ return record[key] === undefined
64
+ ? undefined
65
+ : numberValue(record[key], `${path}.${key}`)
66
+ }
67
+
68
+ function optionalBoolean(
69
+ record: Record<string, unknown>,
70
+ key: string,
71
+ path: string,
72
+ ): boolean | undefined {
73
+ return record[key] === undefined
74
+ ? undefined
75
+ : booleanValue(record[key], `${path}.${key}`)
76
+ }
77
+
78
+ function semanticWaitTimeout(
79
+ record: Record<string, unknown>,
80
+ path: string,
81
+ ): number | undefined {
82
+ const timeoutMs = optionalNumber(record, 'timeoutMs', path)
83
+ if (timeoutMs !== undefined && timeoutMs > SEMANTIC_WAIT_MAX_TIMEOUT_MS) {
84
+ validationFailure(
85
+ `${path}.timeoutMs`,
86
+ `must be at most ${SEMANTIC_WAIT_MAX_TIMEOUT_MS}`,
87
+ )
88
+ }
89
+ return timeoutMs
90
+ }
91
+
92
+ export function parsePerformStep(value: unknown, index: number): SimPerformStep {
93
+ const path = `perform.steps[${index}]`
94
+ const step = recordValue(value, path)
95
+ const type = stringValue(step.type, `${path}.type`)
96
+ switch (type) {
97
+ case 'touchDown':
98
+ case 'touchMove':
99
+ case 'touchUp': {
100
+ const pointerId = optionalNumber(step, 'pointerId', path)
101
+ return {
102
+ type,
103
+ x: numberValue(step.x, `${path}.x`),
104
+ y: numberValue(step.y, `${path}.y`),
105
+ ...(pointerId === undefined ? {} : { pointerId }),
106
+ }
107
+ }
108
+ case 'touchCancel': {
109
+ const pointerId = optionalNumber(step, 'pointerId', path)
110
+ return { type, ...(pointerId === undefined ? {} : { pointerId }) }
111
+ }
112
+ case 'tap':
113
+ return {
114
+ type,
115
+ x: numberValue(step.x, `${path}.x`),
116
+ y: numberValue(step.y, `${path}.y`),
117
+ }
118
+ case 'tapId':
119
+ return { type, id: stringValue(step.id, `${path}.id`) }
120
+ case 'tapText':
121
+ return { type, text: stringValue(step.text, `${path}.text`) }
122
+ case 'doubleTap': {
123
+ const gapMs = optionalNumber(step, 'gapMs', path)
124
+ return {
125
+ type,
126
+ x: numberValue(step.x, `${path}.x`),
127
+ y: numberValue(step.y, `${path}.y`),
128
+ ...(gapMs === undefined ? {} : { gapMs }),
129
+ }
130
+ }
131
+ case 'longPress': {
132
+ const durationMs = optionalNumber(step, 'durationMs', path)
133
+ return {
134
+ type,
135
+ x: numberValue(step.x, `${path}.x`),
136
+ y: numberValue(step.y, `${path}.y`),
137
+ ...(durationMs === undefined ? {} : { durationMs }),
138
+ }
139
+ }
140
+ case 'drag': {
141
+ const steps = optionalNumber(step, 'steps', path)
142
+ const stepMs = optionalNumber(step, 'stepMs', path)
143
+ return {
144
+ type,
145
+ fromX: numberValue(step.fromX, `${path}.fromX`),
146
+ fromY: numberValue(step.fromY, `${path}.fromY`),
147
+ toX: numberValue(step.toX, `${path}.toX`),
148
+ toY: numberValue(step.toY, `${path}.toY`),
149
+ ...(steps === undefined ? {} : { steps }),
150
+ ...(stepMs === undefined ? {} : { stepMs }),
151
+ }
152
+ }
153
+ case 'pinch': {
154
+ const steps = optionalNumber(step, 'steps', path)
155
+ const stepMs = optionalNumber(step, 'stepMs', path)
156
+ return {
157
+ type,
158
+ fromX1: numberValue(step.fromX1, `${path}.fromX1`),
159
+ fromY1: numberValue(step.fromY1, `${path}.fromY1`),
160
+ fromX2: numberValue(step.fromX2, `${path}.fromX2`),
161
+ fromY2: numberValue(step.fromY2, `${path}.fromY2`),
162
+ toX1: numberValue(step.toX1, `${path}.toX1`),
163
+ toY1: numberValue(step.toY1, `${path}.toY1`),
164
+ toX2: numberValue(step.toX2, `${path}.toX2`),
165
+ toY2: numberValue(step.toY2, `${path}.toY2`),
166
+ ...(steps === undefined ? {} : { steps }),
167
+ ...(stepMs === undefined ? {} : { stepMs }),
168
+ }
169
+ }
170
+ case 'scroll': {
171
+ const x = optionalNumber(step, 'x', path)
172
+ const y = optionalNumber(step, 'y', path)
173
+ const animated = optionalBoolean(step, 'animated', path)
174
+ return {
175
+ type,
176
+ id: stringValue(step.id, `${path}.id`),
177
+ ...(x === undefined ? {} : { x }),
178
+ ...(y === undefined ? {} : { y }),
179
+ ...(animated === undefined ? {} : { animated }),
180
+ }
181
+ }
182
+ case 'type':
183
+ return { type, text: stringValue(step.text, `${path}.text`) }
184
+ case 'key':
185
+ return { type, key: stringValue(step.key, `${path}.key`) }
186
+ case 'dismissKeyboard':
187
+ return { type }
188
+ case 'wait':
189
+ return { type, ms: numberValue(step.ms, `${path}.ms`) }
190
+ case 'waitFor': {
191
+ const timeoutMs = semanticWaitTimeout(step, path)
192
+ const selector =
193
+ step.testID === undefined
194
+ ? { id: stringValue(step.id, `${path}.id`) }
195
+ : { testID: stringValue(step.testID, `${path}.testID`) }
196
+ return {
197
+ type,
198
+ ...selector,
199
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
200
+ }
201
+ }
202
+ default:
203
+ return validationFailure(`${path}.type`, 'is not a supported perform step')
204
+ }
205
+ }
206
+
207
+ export function parseStateOptions(value: unknown): SimStateOptions {
208
+ const input = recordValue(value, 'state')
209
+ const route = optionalBoolean(input, 'route', 'state')
210
+ const errors = optionalBoolean(input, 'errors', 'state')
211
+ const depth = optionalNumber(input, 'depth', 'state')
212
+ const errorLimit = optionalNumber(input, 'errorLimit', 'state')
213
+ const layers =
214
+ input.layers === undefined ? undefined : stringValue(input.layers, 'state.layers')
215
+ if (
216
+ layers !== undefined &&
217
+ layers !== 'full' &&
218
+ layers !== 'tenant' &&
219
+ layers !== 'shell'
220
+ ) {
221
+ return validationFailure('state.layers', 'must be full, tenant, or shell')
222
+ }
223
+ return {
224
+ screenshot: optionalBoolean(input, 'screenshot', 'state') ?? true,
225
+ tree: optionalBoolean(input, 'tree', 'state') ?? true,
226
+ ...(route === undefined ? {} : { route }),
227
+ ...(errors === undefined ? {} : { errors }),
228
+ ...(depth === undefined ? {} : { depth }),
229
+ ...(layers === undefined ? {} : { layers }),
230
+ ...(errorLimit === undefined ? {} : { errorLimit }),
231
+ }
232
+ }
233
+
234
+ function parseSemanticSelector(value: unknown, path: string): SimSemanticSelector {
235
+ const selector = recordValue(value, path)
236
+ if (selector.testID !== undefined) {
237
+ return { testID: stringValue(selector.testID, `${path}.testID`) }
238
+ }
239
+ if (selector.text !== undefined) {
240
+ return { text: stringValue(selector.text, `${path}.text`) }
241
+ }
242
+ if (selector.role !== undefined) {
243
+ return { role: stringValue(selector.role, `${path}.role`) }
244
+ }
245
+ if (selector.type !== undefined) {
246
+ return { type: stringValue(selector.type, `${path}.type`) }
247
+ }
248
+ if (selector.pressable !== undefined) {
249
+ if (booleanValue(selector.pressable, `${path}.pressable`) !== true) {
250
+ return validationFailure(`${path}.pressable`, 'must be true')
251
+ }
252
+ return { pressable: true }
253
+ }
254
+ if (selector.visible !== undefined) {
255
+ if (booleanValue(selector.visible, `${path}.visible`) !== true) {
256
+ return validationFailure(`${path}.visible`, 'must be true')
257
+ }
258
+ return { visible: true }
259
+ }
260
+ return validationFailure(
261
+ path,
262
+ 'must select testID, text, role, type, pressable, or visible',
263
+ )
264
+ }
265
+
266
+ export function parseSemanticQuery(value: unknown): SimSemanticQuery {
267
+ const query = recordValue(value, 'query')
268
+ const kind = stringValue(query.kind, 'query.kind')
269
+ if (kind === 'count') return { kind }
270
+ if (kind === 'tree') {
271
+ const depth = optionalNumber(query, 'depth', 'query')
272
+ return { kind, ...(depth === undefined ? {} : { depth }) }
273
+ }
274
+ if (kind === 'find') {
275
+ return { kind, selector: parseSemanticSelector(query.selector, 'query.selector') }
276
+ }
277
+ return validationFailure('query.kind', 'must be tree, find, or count')
278
+ }
279
+
280
+ export function parseSemanticResolveSelector(value: unknown): SimSemanticResolveSelector {
281
+ const selector = recordValue(value, 'selector')
282
+ if (selector.testID !== undefined) {
283
+ return { testID: stringValue(selector.testID, 'selector.testID') }
284
+ }
285
+ if (selector.text !== undefined) {
286
+ return { text: stringValue(selector.text, 'selector.text') }
287
+ }
288
+ return validationFailure('selector', 'must select testID or text')
289
+ }
290
+
291
+ export function parseSemanticWaitOptions(value: unknown): SemanticWaitOptions {
292
+ const input = recordValue(value, 'waitFor')
293
+ const condition = recordValue(input.condition, 'waitFor.condition')
294
+ const type = stringValue(condition.type, 'waitFor.condition.type')
295
+ const timeoutMs = semanticWaitTimeout(input, 'waitFor')
296
+ if (type === 'ready') {
297
+ return {
298
+ condition: { type },
299
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
300
+ }
301
+ }
302
+ if (type !== 'present' && type !== 'absent') {
303
+ return validationFailure(
304
+ 'waitFor.condition.type',
305
+ 'must be ready, present, or absent',
306
+ )
307
+ }
308
+ return {
309
+ condition: {
310
+ type,
311
+ testID: stringValue(condition.testID, 'waitFor.condition.testID'),
312
+ },
313
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
314
+ }
315
+ }
316
+
317
+ export function parseResetOptions(value: unknown): ResetOptions {
318
+ const input = recordValue(value, 'reset')
319
+ const strategy =
320
+ input.strategy === undefined ? 'data' : stringValue(input.strategy, 'reset.strategy')
321
+ if (strategy !== 'data' && strategy !== 'full') {
322
+ return validationFailure('reset.strategy', 'must be data or full')
323
+ }
324
+ return {
325
+ strategy,
326
+ relaunch: optionalBoolean(input, 'relaunch', 'reset') ?? true,
327
+ }
328
+ }
329
+
330
+ export function parseScreenshotOptions(value: unknown): SimScreenshotOptions {
331
+ const input = recordValue(value, 'screenshot')
332
+ const layers =
333
+ input.layers === undefined
334
+ ? undefined
335
+ : stringValue(input.layers, 'screenshot.layers')
336
+ if (
337
+ layers !== undefined &&
338
+ layers !== 'full' &&
339
+ layers !== 'tenant' &&
340
+ layers !== 'shell'
341
+ ) {
342
+ return validationFailure('screenshot.layers', 'must be full, tenant, or shell')
343
+ }
344
+ const crop =
345
+ input.crop === undefined ? undefined : recordValue(input.crop, 'screenshot.crop')
346
+ return {
347
+ ...(layers === undefined ? {} : { layers }),
348
+ ...(crop === undefined
349
+ ? {}
350
+ : {
351
+ crop: {
352
+ x: numberValue(crop.x, 'screenshot.crop.x'),
353
+ y: numberValue(crop.y, 'screenshot.crop.y'),
354
+ w: numberValue(crop.w, 'screenshot.crop.w'),
355
+ h: numberValue(crop.h, 'screenshot.crop.h'),
356
+ },
357
+ }),
358
+ }
359
+ }
@@ -25,13 +25,18 @@ export type BridgeCommandType =
25
25
  | 'close'
26
26
  | 'flowStatus'
27
27
  | 'perform'
28
+ | 'waitFor'
28
29
  | 'state'
30
+ | 'query'
31
+ | 'resolve'
29
32
  | 'reset'
30
33
  | 'camera'
31
34
 
32
35
  export interface WsCommand {
33
36
  id: string | number
34
37
  type: BridgeCommandType
38
+ // local daemon routing; cloud transport strips it before execution.
39
+ simId?: string
35
40
  // flowStatus — live flow-runner progress payload (see flow-run-store)
36
41
  status?: Record<string, unknown>
37
42
  code?: string
@@ -57,8 +62,13 @@ export interface WsCommand {
57
62
  // perform — ordered batch executed inside the page, no round trips
58
63
  steps?: PerformStep[]
59
64
  performOptions?: PerformOptions
65
+ // waitFor — one engine-side polling loop and one transport round trip
66
+ waitForOptions?: SemanticWaitOptions
60
67
  // state — compound screenshot + tree + route + errors
61
68
  stateOptions?: SimStateOptions
69
+ // query / resolve — structured visible-tree reads and selector geometry
70
+ query?: SimSemanticQuery
71
+ selector?: SimSemanticResolveSelector
62
72
  // reset — two-tier app state wipe
63
73
  resetOptions?: ResetOptions
64
74
  // camera — fixture injection into the host camera pipeline
@@ -158,6 +168,67 @@ export interface PerformResult {
158
168
  error?: string
159
169
  }
160
170
 
171
+ // ---------------------------------------------------------------------------
172
+ // waitFor — engine-side semantic condition polling
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export type SemanticWaitCondition =
176
+ | { type: 'ready' }
177
+ | { type: 'present'; testID: string }
178
+ | { type: 'absent'; testID: string }
179
+
180
+ export interface SemanticWaitOptions {
181
+ condition: SemanticWaitCondition
182
+ timeoutMs?: number
183
+ }
184
+
185
+ export interface SemanticReadyProbe {
186
+ flag: unknown
187
+ at: number
188
+ nodes: number
189
+ targets: number
190
+ liveFrameActive: boolean
191
+ liveFrameChannels: number
192
+ liveFramePublishes: number
193
+ errors: number
194
+ loadingText: string
195
+ externalReady: boolean | null
196
+ externalStatus: string
197
+ externalError: string
198
+ suppressedEntryError: string
199
+ }
200
+
201
+ export const SEMANTIC_READY_CONTENT_NODE_FLOOR = 100
202
+ export const SEMANTIC_READY_NODE_STABLE_MS = 750
203
+ export const SEMANTIC_READY_POLL_MS = 150
204
+
205
+ export function readyProbeHasContent(
206
+ probe: Pick<SemanticReadyProbe, 'nodes' | 'targets'> &
207
+ Partial<Pick<SemanticReadyProbe, 'liveFrameActive'>>,
208
+ ): boolean {
209
+ return (
210
+ probe.liveFrameActive ||
211
+ probe.targets > 0 ||
212
+ probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR
213
+ )
214
+ }
215
+
216
+ export function readyProbeHasTargetContent(
217
+ probe: Pick<SemanticReadyProbe, 'targets'> &
218
+ Partial<Pick<SemanticReadyProbe, 'liveFrameActive'>>,
219
+ ): boolean {
220
+ return probe.liveFrameActive || probe.targets > 0
221
+ }
222
+
223
+ export interface SemanticWaitResult {
224
+ matched: boolean
225
+ elapsedMs: number
226
+ polls: number
227
+ probe?: SemanticReadyProbe
228
+ node?: SimSemanticNode
229
+ error?: string
230
+ }
231
+
161
232
  // ---------------------------------------------------------------------------
162
233
  // state — one round-trip situational awareness
163
234
  // ---------------------------------------------------------------------------
@@ -178,13 +249,62 @@ export interface SimStateOptions {
178
249
  export interface SimStateResult {
179
250
  // png data uri, same encoding the `screenshot` command returns
180
251
  screenshot?: unknown
181
- tree?: unknown
252
+ tree?: SimSemanticNode[]
182
253
  route?: { url?: string; screen?: string | null; stack?: string[] } | null
183
254
  recentErrors?: Array<{ level: string; text: string; at?: number }>
184
255
  keyboard?: { visible: boolean; mode?: string | null }
185
256
  capturedAt: number
186
257
  }
187
258
 
259
+ // ---------------------------------------------------------------------------
260
+ // semantic tree — pruned visible structure shared by local and cloud runtimes
261
+ // ---------------------------------------------------------------------------
262
+
263
+ export interface SimSemanticGeometry {
264
+ x: number
265
+ y: number
266
+ width: number
267
+ height: number
268
+ }
269
+
270
+ export interface SimSemanticNode {
271
+ nodeId: number
272
+ type: string
273
+ testID?: string
274
+ text?: string
275
+ role?: string
276
+ label?: string
277
+ pressable?: true
278
+ geometry: SimSemanticGeometry
279
+ children?: SimSemanticNode[]
280
+ }
281
+
282
+ export type SimSemanticSelector =
283
+ | { testID: string }
284
+ | { text: string }
285
+ | { role: string }
286
+ | { type: string }
287
+ | { pressable: true }
288
+ | { visible: true }
289
+
290
+ export type SimSemanticQuery =
291
+ | { kind: 'tree'; depth?: number }
292
+ | { kind: 'find'; selector: SimSemanticSelector }
293
+ | { kind: 'count' }
294
+
295
+ export interface SimSemanticQueryResult {
296
+ nodes: SimSemanticNode[]
297
+ total: number
298
+ }
299
+
300
+ export type SimSemanticResolveSelector = { testID: string } | { text: string }
301
+
302
+ export interface SimSemanticResolveResult {
303
+ match: SimSemanticNode
304
+ target: SimSemanticNode
305
+ point: { x: number; y: number }
306
+ }
307
+
188
308
  // ---------------------------------------------------------------------------
189
309
  // reset — two-tier app state wipe
190
310
  // ---------------------------------------------------------------------------
@@ -54,6 +54,7 @@ import {
54
54
  isReplacementModuleRequestUrl,
55
55
  } from './replacement-module-handler.ts'
56
56
  import { handleWebSocketProxyUpgrade } from './websocket-proxy.ts'
57
+ import type { WsCommand } from '../bridge-contract.ts'
57
58
 
58
59
  const SOOTSIM_CROSS_ORIGIN_EMBEDDER_POLICY = 'require-corp'
59
60
 
@@ -92,17 +93,7 @@ export interface BridgeLockInfo {
92
93
  expiresInMs: number
93
94
  }
94
95
 
95
- export interface BridgeSimCommand {
96
- id: number
97
- type: 'evaluate' | 'screenshot' | 'tap' | 'keyboard' | 'tree' | 'focus' | 'close'
98
- code?: string
99
- x?: number
100
- y?: number
101
- action?: string
102
- text?: string
103
- depth?: number
104
- simId?: string
105
- }
96
+ export type BridgeSimCommand = Omit<WsCommand, 'id'> & { id: number }
106
97
 
107
98
  interface BridgeSimRegistrationMessage {
108
99
  type: 'bridge:register'
@@ -173,7 +164,7 @@ interface BridgeCliLease {
173
164
  }
174
165
 
175
166
  // interactive commands change app state and must respect the lease.
176
- // observational/lifecycle commands (evaluate/tree/screenshot/focus/call/close)
167
+ // observational/lifecycle commands (evaluate/tree/query/resolve/screenshot/focus/call/close)
177
168
  // pass through without taking or checking a lease so cleanup never gets stuck
178
169
  // behind a stale cli owner.
179
170
  // `call` used to acquire a lease too, but most call paths are queries
@@ -2555,6 +2546,10 @@ export class SootSimBridgeHost {
2555
2546
  return msg?.action === 'type' ? 'typed text' : 'used keyboard'
2556
2547
  case 'tree':
2557
2548
  return 'dumped tree'
2549
+ case 'query':
2550
+ return 'queried semantic tree'
2551
+ case 'resolve':
2552
+ return 'resolved semantic selector'
2558
2553
  case 'focus':
2559
2554
  return 'focused sim'
2560
2555
  case 'close':
package/src/index.ts CHANGED
@@ -27,6 +27,13 @@ export type {
27
27
  ResetResult,
28
28
  SimStateOptions,
29
29
  SimStateResult,
30
+ SimSemanticGeometry,
31
+ SimSemanticNode,
32
+ SimSemanticQuery,
33
+ SimSemanticQueryResult,
34
+ SimSemanticResolveResult,
35
+ SimSemanticResolveSelector,
36
+ SimSemanticSelector,
30
37
  } from './bridge-contract'
31
38
 
32
39
  export { rnxPlugin } from './vite-plugin-one'
package/src/sim-client.ts CHANGED
@@ -55,6 +55,9 @@ import type {
55
55
  PerformStep,
56
56
  ResetOptions,
57
57
  ResetResult,
58
+ SimSemanticNode,
59
+ SimSemanticResolveResult,
60
+ SimSemanticResolveSelector,
58
61
  SimStateOptions,
59
62
  SimStateResult,
60
63
  } from './bridge-contract'
@@ -386,12 +389,18 @@ export class SimClient implements WsBridge, InspectBridge {
386
389
  return result ? [result as InteractiveNode] : []
387
390
  }
388
391
 
389
- async tree(depth = 5): Promise<unknown> {
392
+ async tree(depth = 5): Promise<SimSemanticNode[]> {
390
393
  const { tree } = await inspectTree(this, depth)
391
- return tree
394
+ return Array.isArray(tree) ? tree : []
392
395
  }
393
396
 
394
- /** compact visible-UI dump with shell + keyboard state, as the CLI prints. */
397
+ resolve(
398
+ selector: SimSemanticResolveSelector,
399
+ ): Promise<SimSemanticResolveResult | null> {
400
+ return this.send({ type: 'resolve', selector })
401
+ }
402
+
403
+ /** compact visible semantic tree, formatted as the CLI prints it. */
395
404
  describe(opts: { filter?: string; verbose?: boolean } = {}): Promise<DescribeResult> {
396
405
  return inspectDescribe(this, {
397
406
  describe: true,