rnxsim 0.1.313 → 0.1.314

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 (297) hide show
  1. package/LICENSE +11 -10
  2. package/README.md +5 -0
  3. package/cli/app-config.ts +65 -0
  4. package/cli/app-fonts.ts +408 -0
  5. package/cli/app-project.ts +231 -0
  6. package/cli/app-splash.ts +185 -0
  7. package/cli/app-state-reset.ts +24 -0
  8. package/cli/auth.ts +155 -0
  9. package/cli/bin.ts +594 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2830 -0
  12. package/cli/browser-evals.ts +96 -0
  13. package/cli/commands/agent-wrapper.ts +986 -0
  14. package/cli/commands/agent.ts +423 -0
  15. package/cli/commands/app-fonts.ts +98 -0
  16. package/cli/commands/assert.ts +541 -0
  17. package/cli/commands/auth.ts +59 -0
  18. package/cli/commands/camera.ts +266 -0
  19. package/cli/commands/cleanup.ts +169 -0
  20. package/cli/commands/compat.ts +87 -0
  21. package/cli/commands/config.ts +32 -0
  22. package/cli/commands/control.ts +2142 -0
  23. package/cli/commands/cpu-profile.ts +269 -0
  24. package/cli/commands/daemon-mac-app.ts +169 -0
  25. package/cli/commands/daemon.ts +874 -0
  26. package/cli/commands/debug.ts +719 -0
  27. package/cli/commands/desktop.ts +39 -0
  28. package/cli/commands/detect.ts +197 -0
  29. package/cli/commands/detox.ts +385 -0
  30. package/cli/commands/device.ts +133 -0
  31. package/cli/commands/diagnose.ts +589 -0
  32. package/cli/commands/electron.ts +95 -0
  33. package/cli/commands/film.ts +379 -0
  34. package/cli/commands/flow.ts +1124 -0
  35. package/cli/commands/inspect/actions.ts +622 -0
  36. package/cli/commands/inspect/core.ts +2405 -0
  37. package/cli/commands/inspect/count.ts +17 -0
  38. package/cli/commands/inspect/describe.ts +192 -0
  39. package/cli/commands/inspect/env.ts +23 -0
  40. package/cli/commands/inspect/find.ts +171 -0
  41. package/cli/commands/inspect/get-layout.ts +39 -0
  42. package/cli/commands/inspect/keyboard.ts +52 -0
  43. package/cli/commands/inspect/list.ts +58 -0
  44. package/cli/commands/inspect/memory.ts +215 -0
  45. package/cli/commands/inspect/redaction.ts +39 -0
  46. package/cli/commands/inspect/resolve-target.ts +82 -0
  47. package/cli/commands/inspect/screens.ts +78 -0
  48. package/cli/commands/inspect/settle.ts +22 -0
  49. package/cli/commands/inspect/settling.ts +158 -0
  50. package/cli/commands/inspect/shared.ts +353 -0
  51. package/cli/commands/inspect/sleep.ts +14 -0
  52. package/cli/commands/inspect/tree.ts +32 -0
  53. package/cli/commands/inspect/url.ts +17 -0
  54. package/cli/commands/inspect/wait-event.ts +210 -0
  55. package/cli/commands/inspect/wait-idle.ts +24 -0
  56. package/cli/commands/inspect/wait-ready.ts +74 -0
  57. package/cli/commands/inspect/wait-selector.ts +54 -0
  58. package/cli/commands/inspect/wait.ts +31 -0
  59. package/cli/commands/inspect.ts +4519 -0
  60. package/cli/commands/install-desktop.ts +351 -0
  61. package/cli/commands/login.ts +331 -0
  62. package/cli/commands/logout.ts +31 -0
  63. package/cli/commands/maestro-generate.ts +361 -0
  64. package/cli/commands/maestro.ts +453 -0
  65. package/cli/commands/mode.ts +57 -0
  66. package/cli/commands/no-bridge-hint.ts +80 -0
  67. package/cli/commands/perf.ts +66 -0
  68. package/cli/commands/permissions.ts +203 -0
  69. package/cli/commands/profile.ts +108 -0
  70. package/cli/commands/react.ts +353 -0
  71. package/cli/commands/record.ts +1434 -0
  72. package/cli/commands/report-issue.ts +305 -0
  73. package/cli/commands/reset.ts +85 -0
  74. package/cli/commands/runtime.ts +351 -0
  75. package/cli/commands/screenshot-command.ts +106 -0
  76. package/cli/commands/screenshot-layers.ts +143 -0
  77. package/cli/commands/screenshot-mode.ts +37 -0
  78. package/cli/commands/screenshot.ts +488 -0
  79. package/cli/commands/screenshots-capture.ts +607 -0
  80. package/cli/commands/screenshots.ts +127 -0
  81. package/cli/commands/serve.ts +168 -0
  82. package/cli/commands/setup.ts +545 -0
  83. package/cli/commands/shell-boolean-mode.ts +81 -0
  84. package/cli/commands/skills.ts +467 -0
  85. package/cli/commands/slides.ts +361 -0
  86. package/cli/commands/state.ts +87 -0
  87. package/cli/commands/storage.ts +58 -0
  88. package/cli/commands/telemetry.ts +54 -0
  89. package/cli/commands/three-mode.ts +763 -0
  90. package/cli/commands/timeline.ts +122 -0
  91. package/cli/commands/upgrade.ts +208 -0
  92. package/cli/commands/upload.ts +1225 -0
  93. package/cli/commands/version.ts +54 -0
  94. package/cli/commands/what-happened.ts +327 -0
  95. package/cli/current-sim.ts +204 -0
  96. package/cli/desktop-companion.ts +300 -0
  97. package/cli/drivers/electron.ts +70 -0
  98. package/cli/drivers/index.ts +20 -0
  99. package/cli/drivers/playwright-provisioning.ts +180 -0
  100. package/cli/drivers/playwright.ts +698 -0
  101. package/cli/drivers/registry.ts +65 -0
  102. package/cli/drivers/types.ts +102 -0
  103. package/cli/flow-file.ts +142 -0
  104. package/cli/flow-live-status.ts +120 -0
  105. package/cli/flow-session.ts +187 -0
  106. package/cli/help.ts +80 -0
  107. package/cli/hidden-runtime-alias.ts +19 -0
  108. package/cli/hints.ts +216 -0
  109. package/cli/inspect-notice-state.ts +114 -0
  110. package/cli/maestro-js.ts +334 -0
  111. package/cli/open-url.ts +8 -0
  112. package/cli/parent-pid.ts +204 -0
  113. package/cli/parse-args.ts +211 -0
  114. package/cli/prompt.ts +51 -0
  115. package/cli/recording-access.ts +107 -0
  116. package/cli/registry.ts +1 -0
  117. package/cli/resolve-assets.ts +63 -0
  118. package/cli/run-registry.ts +226 -0
  119. package/cli/runtime-notes.ts +66 -0
  120. package/cli/runtime-summary.ts +25 -0
  121. package/cli/setup-repository.ts +187 -0
  122. package/cli/telemetry.ts +187 -0
  123. package/cli/ws-bridge.ts +798 -0
  124. package/dist-cli/bin.js +5 -5
  125. package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
  146. package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
  166. package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
  178. package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
  179. package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
  180. package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
  198. package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
  199. package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
  200. package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
  201. package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
  202. package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
  203. package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
  204. package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
  214. package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
  215. package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
  216. package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
  224. package/dist-cli/chunks/store-32HSPZHI.js +4 -0
  225. package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
  226. package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
  228. package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
  229. package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
  230. package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
  232. package/dist-lib/agent-daemon-client.cjs +1 -1
  233. package/dist-lib/agent-events.cjs +1 -1
  234. package/dist-lib/agent-identity.cjs +1 -1
  235. package/dist-lib/agent-sessions.cjs +1 -1
  236. package/dist-lib/attached-projects.cjs +1 -1
  237. package/dist-lib/auth/shared-session.cjs +1 -1
  238. package/dist-lib/backend-origin.cjs +1 -1
  239. package/dist-lib/beta.cjs +1 -1
  240. package/dist-lib/beta.mjs +1 -1
  241. package/dist-lib/bridge-constants.cjs +1 -1
  242. package/dist-lib/bridge-contract.cjs +20 -0
  243. package/dist-lib/cli-constants.cjs +1 -1
  244. package/dist-lib/config.cjs +1 -1
  245. package/dist-lib/detox/index.cjs +1 -1
  246. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  247. package/dist-lib/home-paths.cjs +67 -28
  248. package/dist-lib/host/bridge-host.cjs +140 -12
  249. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  250. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  251. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  252. package/dist-lib/host/websocket-proxy.cjs +1 -1
  253. package/dist-lib/index.cjs +2815 -40
  254. package/dist-lib/jump-to-source-babel.cjs +1 -1
  255. package/dist-lib/menu.cjs +1 -1
  256. package/dist-lib/menu.mjs +1 -1
  257. package/dist-lib/metro.cjs +1 -1
  258. package/dist-lib/profiles.cjs +1 -1
  259. package/dist-lib/public-brand.cjs +1 -1
  260. package/dist-lib/render-mode.cjs +1 -1
  261. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  262. package/dist-lib/sdk.cjs +2549 -2061
  263. package/dist-lib/sdk.mjs +2543 -2061
  264. package/dist-lib/skills.cjs +480 -280
  265. package/dist-lib/vite.cjs +1 -1
  266. package/package.json +8 -2
  267. package/src/bridge-constants.ts +3 -4
  268. package/src/bridge-contract.ts +251 -0
  269. package/src/connect.ts +83 -0
  270. package/src/disk-cleanup.ts +30 -0
  271. package/src/home-paths.ts +81 -38
  272. package/src/host/bridge-host.ts +134 -6
  273. package/src/index.ts +27 -1
  274. package/src/sdk.ts +8 -0
  275. package/src/sim-client.ts +660 -0
  276. package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
  277. package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
  278. package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
  279. package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
  280. package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
  281. package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
  282. package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
  283. package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
  284. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
  285. package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
  286. package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
  287. package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
  288. package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
  289. package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
  290. package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
  291. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
  292. package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
  293. package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
  294. package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
  295. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
  296. package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
  297. package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
@@ -0,0 +1,215 @@
1
+ // `sootsim get memory` / `sootsim debug memory` — per-worker live object
2
+ // counts (nodes registered, pictures, raster images, paragraphs, yoga wasm
3
+ // nodes), image-loader cache, and JS heaps. one-shot by default; --watch
4
+ // samples repeatedly and reports per-counter slope, which is the leak-RCA
5
+ // workflow: run the repro, watch which counter's slope tracks RSS.
6
+
7
+ import { inspectMemory, type MemoryReport, type WorkerMemorySample } from './core'
8
+ import { printJson, wantsJson } from './shared'
9
+ import type { WsBridge } from '../../ws-bridge'
10
+
11
+ function formatBytes(n: number): string {
12
+ if (n < 1024) return `${n}B`
13
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`
14
+ return `${(n / 1024 / 1024).toFixed(1)}MB`
15
+ }
16
+
17
+ function pct(num: number, denom: number): string {
18
+ if (denom <= 0) return '?'
19
+ return `${((num / denom) * 100).toFixed(0)}%`
20
+ }
21
+
22
+ // flatten one report into named numeric counters so watch mode can diff and
23
+ // slope every field uniformly.
24
+ function flattenCounters(report: MemoryReport): Record<string, number> {
25
+ const out: Record<string, number> = {}
26
+ const worker = (prefix: string, w: WorkerMemorySample | null) => {
27
+ if (!w) return
28
+ if (w.nodesRegistered != null) out[`${prefix}.nodes`] = w.nodesRegistered
29
+ if (w.nodesDetached != null) out[`${prefix}.detached`] = w.nodesDetached
30
+ if (w.objects) {
31
+ out[`${prefix}.paragraphs`] = w.objects.paragraphs
32
+ out[`${prefix}.yogaNodes`] = w.objects.yogaNodes
33
+ out[`${prefix}.pictures`] = w.objects.pictures
34
+ out[`${prefix}.rasterImages`] = w.objects.rasterImages
35
+ }
36
+ if (w.imageLoader) {
37
+ out[`${prefix}.imageCacheEntries`] = w.imageLoader.cacheEntries
38
+ out[`${prefix}.imagePixelBytes`] = w.imageLoader.cachePixelBytes
39
+ }
40
+ if (w.workerHeap) out[`${prefix}.heapBytes`] = w.workerHeap.usedJSHeapSize
41
+ if (w.wasmHeapBytes != null) out[`${prefix}.wasmHeapBytes`] = w.wasmHeapBytes
42
+ }
43
+ worker('tenant', report.tenant)
44
+ worker('shell', report.shell)
45
+ worker('compositor', report.compositor)
46
+ if (report.hostHeap) out['host.heapBytes'] = report.hostHeap.usedJSHeapSize
47
+ return out
48
+ }
49
+
50
+ function formatCounter(key: string, value: number): string {
51
+ return key.endsWith('Bytes') ? formatBytes(value) : String(value)
52
+ }
53
+
54
+ function printWorkerBlock(name: string, w: WorkerMemorySample | null): void {
55
+ if (!w) {
56
+ console.log(` ${name}: not available`)
57
+ return
58
+ }
59
+ const objects = w.objects
60
+ console.log(` ${name}`)
61
+ if (w.nodesRegistered != null) {
62
+ console.log(` nodes registered: ${w.nodesRegistered}`)
63
+ }
64
+ if (w.nodesDetached != null) {
65
+ const types = Object.entries(w.detachedTypes ?? {})
66
+ .map(([type, count]) => `${type}:${count}`)
67
+ .join(' ')
68
+ console.log(
69
+ ` nodes detached: ${w.nodesDetached}${types ? ` (${types})` : ''}`,
70
+ )
71
+ }
72
+ if (objects) {
73
+ console.log(` paragraphs: ${objects.paragraphs}`)
74
+ console.log(` yoga nodes: ${objects.yogaNodes}`)
75
+ console.log(` pictures: ${objects.pictures}`)
76
+ console.log(` raster images: ${objects.rasterImages}`)
77
+ }
78
+ if (w.activeNativeAnimations != null) {
79
+ console.log(` native anims: ${w.activeNativeAnimations}`)
80
+ }
81
+ if (w.imageLoader) {
82
+ const im = w.imageLoader
83
+ console.log(
84
+ ` image cache: ${im.cacheEntries}/${im.cacheMaxEntries} entries, ${formatBytes(im.cachePixelBytes)}/${formatBytes(im.cachePixelBudget)} (${pct(im.cachePixelBytes, im.cachePixelBudget)})`,
85
+ )
86
+ }
87
+ if (w.workerHeap) {
88
+ console.log(
89
+ ` js heap: ${formatBytes(w.workerHeap.usedJSHeapSize)} used / ${formatBytes(w.workerHeap.totalJSHeapSize)} total`,
90
+ )
91
+ }
92
+ if (w.wasmHeapBytes != null) {
93
+ console.log(` canvaskit wasm: ${formatBytes(w.wasmHeapBytes)}`)
94
+ }
95
+ }
96
+
97
+ function printReport(report: MemoryReport): void {
98
+ console.log(' memory:')
99
+ printWorkerBlock('tenant worker', report.tenant)
100
+ printWorkerBlock('shell worker', report.shell)
101
+ printWorkerBlock('compositor worker', report.compositor)
102
+ if (report.hostHeap) {
103
+ console.log(' host')
104
+ console.log(
105
+ ` js heap: ${formatBytes(report.hostHeap.usedJSHeapSize)} used / ${formatBytes(report.hostHeap.totalJSHeapSize)} total`,
106
+ )
107
+ } else {
108
+ console.log(' host js heap: not available (chrome only)')
109
+ }
110
+ }
111
+
112
+ function readNumberFlag(args: string[], flag: string, fallback: number): number {
113
+ const idx = args.indexOf(flag)
114
+ if (idx === -1) return fallback
115
+ const value = Number(args[idx + 1])
116
+ return Number.isFinite(value) && value > 0 ? value : fallback
117
+ }
118
+
119
+ export async function runMemorySubcommand(
120
+ bridge: WsBridge,
121
+ opts: { args: string[] } = { args: [] },
122
+ ): Promise<void> {
123
+ const json = wantsJson(opts.args)
124
+ const watchIdx = opts.args.indexOf('--watch')
125
+
126
+ if (watchIdx === -1) {
127
+ const report = await inspectMemory(bridge)
128
+ if (json) {
129
+ printJson(report)
130
+ return
131
+ }
132
+ printReport(report)
133
+ return
134
+ }
135
+
136
+ // --watch [seconds] — sample every --interval seconds (default 5) for the
137
+ // given duration (default 60s), then report per-counter slope.
138
+ const durationValue = Number(opts.args[watchIdx + 1])
139
+ const durationSec =
140
+ Number.isFinite(durationValue) && durationValue > 0 ? durationValue : 60
141
+ const intervalSec = readNumberFlag(opts.args, '--interval', 5)
142
+
143
+ const samples: Array<{ tSec: number; counters: Record<string, number> }> = []
144
+ const start = Date.now()
145
+ let prev: Record<string, number> | null = null
146
+
147
+ if (!json) {
148
+ console.log(` sampling every ${intervalSec}s for ${durationSec}s — deltas per tick:`)
149
+ }
150
+
151
+ while (true) {
152
+ const report = await inspectMemory(bridge)
153
+ const tSec = (Date.now() - start) / 1000
154
+ const counters = flattenCounters(report)
155
+ samples.push({ tSec, counters })
156
+
157
+ if (!json) {
158
+ const parts: string[] = []
159
+ for (const [key, value] of Object.entries(counters)) {
160
+ const delta = prev && key in prev ? value - prev[key] : null
161
+ if (delta === null || delta === 0) continue
162
+ const sign = delta > 0 ? '+' : '-'
163
+ const deltaStr = key.endsWith('Bytes')
164
+ ? `${sign}${formatBytes(Math.abs(delta))}`
165
+ : `${sign}${Math.abs(delta)}`
166
+ parts.push(`${key} ${formatCounter(key, value)} (${deltaStr})`)
167
+ }
168
+ console.log(
169
+ ` t+${tSec.toFixed(0)}s ${
170
+ parts.length
171
+ ? parts.join(' ')
172
+ : prev
173
+ ? 'no change'
174
+ : Object.entries(counters)
175
+ .map(([k, v]) => `${k} ${formatCounter(k, v)}`)
176
+ .join(' ')
177
+ }`,
178
+ )
179
+ }
180
+ prev = counters
181
+
182
+ if (tSec >= durationSec) break
183
+ await new Promise((resolve) => setTimeout(resolve, intervalSec * 1000))
184
+ }
185
+
186
+ // per-counter slope over the whole window
187
+ const first = samples[0]
188
+ const last = samples[samples.length - 1]
189
+ const elapsed = Math.max(last.tSec - first.tSec, 0.001)
190
+ const perSecond: Record<string, number> = {}
191
+ for (const key of Object.keys(last.counters)) {
192
+ if (!(key in first.counters)) continue
193
+ perSecond[key] = (last.counters[key] - first.counters[key]) / elapsed
194
+ }
195
+
196
+ if (json) {
197
+ printJson({ samples, perSecond, elapsedSec: elapsed })
198
+ return
199
+ }
200
+
201
+ console.log(`\n slope over ${elapsed.toFixed(0)}s:`)
202
+ const moving = Object.entries(perSecond)
203
+ .filter(([, v]) => v !== 0)
204
+ .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
205
+ if (moving.length === 0) {
206
+ console.log(' all counters flat')
207
+ return
208
+ }
209
+ for (const [key, rate] of moving) {
210
+ const label = key.endsWith('Bytes')
211
+ ? `${formatBytes(Math.abs(rate))}/s${rate < 0 ? ' (shrinking)' : ''}`
212
+ : `${rate > 0 ? '+' : ''}${rate.toFixed(2)}/s`
213
+ console.log(` ${key.padEnd(28)}${label}`)
214
+ }
215
+ }
@@ -0,0 +1,39 @@
1
+ export const SECURE_TEXT_REDACTION = '<redacted secure text>'
2
+
3
+ type KeyboardSpecLike = {
4
+ secureTextEntry?: boolean | null
5
+ keyboardType?: string | null
6
+ } | null
7
+
8
+ type KeyboardStateLike = {
9
+ layout?: { spec?: KeyboardSpecLike } | null
10
+ spec?: KeyboardSpecLike
11
+ focusedInput?: Record<string, unknown> | null
12
+ } | null
13
+
14
+ export function isSecureKeyboardState(state: KeyboardStateLike): boolean {
15
+ const spec = state?.layout?.spec ?? state?.spec ?? null
16
+ return !!spec?.secureTextEntry && spec.keyboardType !== 'visible-password'
17
+ }
18
+
19
+ export function redactTextForKeyboardState(
20
+ text: string,
21
+ state: KeyboardStateLike,
22
+ ): string {
23
+ return isSecureKeyboardState(state) ? SECURE_TEXT_REDACTION : text
24
+ }
25
+
26
+ export function redactKeyboardStateForOutput<T extends KeyboardStateLike>(state: T): T {
27
+ if (!state || !isSecureKeyboardState(state)) return state
28
+ const focusedInput = state.focusedInput
29
+ if (!focusedInput || typeof focusedInput !== 'object') return state
30
+ return {
31
+ ...state,
32
+ focusedInput: {
33
+ ...focusedInput,
34
+ secureTextEntry: true,
35
+ text:
36
+ typeof focusedInput.text === 'string' ? SECURE_TEXT_REDACTION : focusedInput.text,
37
+ },
38
+ }
39
+ }
@@ -0,0 +1,82 @@
1
+ import type { WsBridge } from '../../ws-bridge'
2
+
3
+ // read a --testid / --text flag from args. returns null when neither is
4
+ // present so coord-based do verbs can fall through to their positional path.
5
+ //
6
+ // pass `positional` from a verb whose positionals are x/y coordinates (tap,
7
+ // double-tap, long-press) and a bare non-numeric first positional reads as a
8
+ // testID: `sootsim do tap my-button` works like `do tap-id my-button` and like
9
+ // `do scroll <id>`, instead of failing with a usage line for the one verb an
10
+ // agent is most likely to reach for. a coordinate is always numeric, so there
11
+ // is nothing to disambiguate.
12
+ export function readTargetFlag(
13
+ args: string[],
14
+ positional?: string[],
15
+ ): { mode: 'testid' | 'text'; value: string } | null {
16
+ const tid = args.indexOf('--testid')
17
+ if (tid >= 0 && args[tid + 1]) return { mode: 'testid', value: args[tid + 1] }
18
+ const alt = args.indexOf('--test-id')
19
+ if (alt >= 0 && args[alt + 1]) return { mode: 'testid', value: args[alt + 1] }
20
+ const text = args.indexOf('--text')
21
+ if (text >= 0 && args[text + 1]) return { mode: 'text', value: args[text + 1] }
22
+ const bare = positional?.[1]
23
+ if (bare && !bare.startsWith('-') && !Number.isFinite(Number(bare))) {
24
+ return { mode: 'testid', value: bare }
25
+ }
26
+ return null
27
+ }
28
+
29
+ export async function resolveTargetCoords(
30
+ bridge: WsBridge,
31
+ target: { mode: 'testid' | 'text'; value: string },
32
+ ): Promise<{
33
+ x: number
34
+ y: number
35
+ id?: string | null
36
+ testID?: string | null
37
+ text?: string | null
38
+ type?: string | null
39
+ } | null> {
40
+ const arg = JSON.stringify(target.value)
41
+ const findExpr =
42
+ target.mode === 'testid'
43
+ ? `(await t.findByTestId(${arg})) || (await t.findById(${arg}))`
44
+ : `await t.findByText(${arg})`
45
+ const result = await bridge.send({
46
+ type: 'evaluate',
47
+ code: `(async () => {
48
+ const t = window.__sootsimTest
49
+ if (!t) return null
50
+ const n = ${findExpr}
51
+ if (!n || !n.absolutePosition || !n.layout) return null
52
+ const resolved =
53
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
54
+ ? await t.resolveTapTarget(n.nodeId)
55
+ : null
56
+ const cx =
57
+ typeof resolved?.cx === 'number'
58
+ ? resolved.cx
59
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
60
+ const cy =
61
+ typeof resolved?.cy === 'number'
62
+ ? resolved.cy
63
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
64
+ return {
65
+ x: cx,
66
+ y: cy,
67
+ id: n.id ?? null,
68
+ testID: n.testID ?? null,
69
+ text: ${JSON.stringify(target.mode === 'text')} ? ${arg} : (n.text ?? n.accessibilityLabel ?? null),
70
+ type: n.type ?? null,
71
+ }
72
+ })()`,
73
+ })
74
+ return (result ?? null) as {
75
+ x: number
76
+ y: number
77
+ id?: string | null
78
+ testID?: string | null
79
+ text?: string | null
80
+ type?: string | null
81
+ } | null
82
+ }
@@ -0,0 +1,78 @@
1
+ // `sootsim get screens` — print the current navigation stack + shell state +
2
+ // any presented modal + keyboard. answers "what screen am I on?" without
3
+ // having to dump the whole tree or scrape screen-specific testIDs.
4
+ //
5
+ // sources:
6
+ // - navigation-runtime snapshot (test bridge) for screen stack + phase
7
+ // - mainShell.getState() for active app + switcher / launch progress
8
+ // - __sootsimKeyboard.getLayout() for keyboard visibility + spec
9
+
10
+ import { inspectScreens } from './core'
11
+ import type { WsBridge } from '../../ws-bridge'
12
+
13
+ export async function runScreensSubcommand(
14
+ bridge: WsBridge,
15
+ opts: { json?: boolean } = {},
16
+ ): Promise<void> {
17
+ const { nav, route, keyboard, shell } = await inspectScreens(bridge)
18
+
19
+ if (opts.json) {
20
+ console.log(JSON.stringify({ shell: shell ?? null, nav, route, keyboard }, null, 2))
21
+ return
22
+ }
23
+
24
+ const lines: string[] = []
25
+
26
+ // shell — what app, what switcher state
27
+ if (shell) {
28
+ const activeApp = shell.activeApp ?? shell.state ?? '<none>'
29
+ const switcher = shell.showSwitcher ? ' (app switcher open)' : ''
30
+ const launchProgress =
31
+ typeof shell.launchProgress === 'number' && shell.launchProgress < 0.98
32
+ ? ` launching (${Math.round(shell.launchProgress * 100)}%)`
33
+ : ''
34
+ lines.push(`shell: ${activeApp}${switcher}${launchProgress}`)
35
+ } else {
36
+ lines.push(`shell: <unavailable>`)
37
+ }
38
+
39
+ // navigation stack
40
+ if (nav) {
41
+ const phase =
42
+ nav.transitionPhase !== 'idle'
43
+ ? ` (${nav.transitionPhase}, ${nav.activeTransitionCount} active)`
44
+ : ''
45
+ lines.push(`nav: phase=${nav.transitionPhase}${phase}`)
46
+ if (nav.screens.length === 0) {
47
+ lines.push(` <no registered screens — app may not use react-native-screens>`)
48
+ } else {
49
+ for (const s of nav.screens) {
50
+ const marker = s.isActive ? '▶' : ' '
51
+ const route = s.routeName ? ` ${s.routeName}` : ''
52
+ const header = s.headerHeight > 0 ? ` header=${s.headerHeight}` : ''
53
+ const lt =
54
+ s.largeTitleState && s.largeTitleState !== 'expanded'
55
+ ? ` large-title=${s.largeTitleState}`
56
+ : ''
57
+ lines.push(` ${marker} #${s.id}${route}${header}${lt}`)
58
+ }
59
+ }
60
+ } else {
61
+ lines.push(`nav: <runtime not available>`)
62
+ }
63
+ lines.push(`route: ${route?.currentPath ?? '<unavailable>'}`)
64
+ if (route?.coveringPath) {
65
+ lines.push(`covered: a presented screen (${route.coveringPath}) is above this route`)
66
+ }
67
+
68
+ // keyboard
69
+ if (keyboard && keyboard.visible) {
70
+ const type = keyboard.spec?.keyboardType ?? 'default'
71
+ const ret = keyboard.spec?.returnKeyType ?? 'default'
72
+ lines.push(`keyboard: visible (${type}, return=${ret}, mode=${keyboard.mode ?? '?'})`)
73
+ } else {
74
+ lines.push(`keyboard: hidden`)
75
+ }
76
+
77
+ console.log(lines.join('\n'))
78
+ }
@@ -0,0 +1,22 @@
1
+ import { waitForSootsimIdle } from './settling'
2
+ import type { WsBridge } from '../../ws-bridge'
3
+
4
+ // default: layout hash stable for 3 consecutive polls. --strict additionally
5
+ // requires worker queryStats animation flags to be false — use when the
6
+ // target app has no perpetual background animation.
7
+ export async function runSettleSubcommand(opts: {
8
+ bridge: WsBridge
9
+ args: string[]
10
+ positional: string[]
11
+ }): Promise<void> {
12
+ const { bridge, args, positional } = opts
13
+ const maxMs = positional[1] ? Number(positional[1]) * 1000 : 3000
14
+ const strict = args.includes('--strict')
15
+ const { elapsed, settled } = await waitForSootsimIdle({ bridge, maxMs, strict })
16
+
17
+ if (settled) {
18
+ console.log(` settled in ${elapsed}ms`)
19
+ } else {
20
+ console.log(` timed out after ${elapsed}ms (may still be animating)`)
21
+ }
22
+ }
@@ -0,0 +1,158 @@
1
+ import type { InspectBridge } from './core'
2
+
3
+ export type WaitForSootsimIdleOptions = {
4
+ bridge: InspectBridge
5
+ simId?: string
6
+ maxMs: number
7
+ pollMs?: number
8
+ stablePolls?: number
9
+ strict?: boolean
10
+ }
11
+
12
+ export type WaitForSootsimIdleResult = {
13
+ elapsed: number
14
+ settled: boolean
15
+ }
16
+
17
+ export async function waitForSootsimIdle({
18
+ bridge,
19
+ simId,
20
+ maxMs,
21
+ pollMs = 50,
22
+ stablePolls = 3,
23
+ strict = false,
24
+ }: WaitForSootsimIdleOptions): Promise<WaitForSootsimIdleResult> {
25
+ const result = await bridge.send(
26
+ {
27
+ type: 'evaluate',
28
+ simId,
29
+ code: `(async () => {
30
+ const start = Date.now()
31
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
32
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
33
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
34
+ const strict = ${strict ? 'true' : 'false'}
35
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
36
+
37
+ // route-aware settle: a tap that pushes/pops a screen is async — the
38
+ // old screen still renders for a moment, then the new one mounts and
39
+ // its content loads. a pure layout-hash check reports "idle" on the
40
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
41
+ // and stacks duplicate navigations. drain any in-flight screen
42
+ // transition FIRST, bounded by the overall budget. when no
43
+ // transition is happening this returns in ~80ms (startWindowMs), so
44
+ // a plain button tap barely pays for it.
45
+ try {
46
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
47
+ if (typeof wfst === 'function') {
48
+ const remaining = deadline - Date.now()
49
+ if (remaining > 120) {
50
+ await wfst({
51
+ timeoutMs: Math.min(remaining - 80, 4000),
52
+ settleMs: 64,
53
+ startWindowMs: 80,
54
+ })
55
+ }
56
+ }
57
+ } catch {}
58
+
59
+ const readSnapshot = async () => {
60
+ let animating = false
61
+ let pendingFetches = 0
62
+ let requestTotal = 0
63
+ let requestInFlight = 0
64
+ try {
65
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
66
+ if (stats) {
67
+ animating =
68
+ stats.hasActiveAnims === true ||
69
+ stats.hasActiveNativeAnimations === true ||
70
+ stats.hasPendingAnimationFrames === true
71
+ // a freshly-pushed screen showing a skeleton is layout-stable
72
+ // but not actually settled — its data/images are still in
73
+ // flight. treat bounded image-loader fetches as not-idle so
74
+ // settle waits for real content, capped by the budget.
75
+ const pf = stats.memory && stats.memory.imageLoader
76
+ ? stats.memory.imageLoader.pendingFetches
77
+ : 0
78
+ pendingFetches = typeof pf === 'number' ? pf : 0
79
+ }
80
+ } catch {}
81
+ try {
82
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
83
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
84
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
85
+ } catch {}
86
+ const root = window.__sootsimRoot
87
+ const nodes = []
88
+ if (root) {
89
+ const walk = (n) => {
90
+ if (n.layout && n.layout.width > 0) {
91
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
92
+ }
93
+ for (const c of n.children || []) walk(c)
94
+ }
95
+ walk(root)
96
+ }
97
+ return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
98
+ }
99
+
100
+ // how long generic background network is allowed to keep us waiting AFTER
101
+ // the screen is otherwise visually settled. apps with continuous traffic
102
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
103
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
104
+ // quiet burns the ENTIRE budget on every command — the leading idle that
105
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
106
+ // layout-stability + the image-loader already prove visible content
107
+ // loaded (a data load changes layout; an image load shows in the loader),
108
+ // so the generic request counters are a courtesy, not a gate: honor them
109
+ // only within this grace once the screen is visually still.
110
+ const networkQuietGraceMs = 1200
111
+ let lastLayout = ''
112
+ let lastRequestTotal = -1
113
+ let stable = 0
114
+ let visuallyStillSince = 0
115
+ while (Date.now() < deadline) {
116
+ const snapshot = await readSnapshot()
117
+ const strictOk = !strict || !snapshot.animating
118
+ // visually settled: no animations, no image content loading, layout
119
+ // unchanged since the last poll.
120
+ const visuallyStill =
121
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
122
+ // generic network quiet: nothing in flight and no new requests issued.
123
+ const networkQuiet =
124
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
125
+ if (visuallyStill) {
126
+ stable++
127
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
128
+ // settle once the screen has held still long enough AND either the
129
+ // network is genuinely quiet OR it has refused to quiet within the
130
+ // grace (continuous background traffic must not block forever).
131
+ if (
132
+ stable >= requiredStablePolls &&
133
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
134
+ ) {
135
+ return { settled: true, elapsed: Date.now() - start }
136
+ }
137
+ } else {
138
+ stable = 0
139
+ visuallyStillSince = 0
140
+ }
141
+ lastLayout = snapshot.layout
142
+ lastRequestTotal = snapshot.requestTotal
143
+ await sleep(pollMs)
144
+ }
145
+ return { settled: false, elapsed: Date.now() - start }
146
+ })()`,
147
+ },
148
+ {
149
+ timeoutMs: maxMs + 1_000,
150
+ },
151
+ )
152
+
153
+ const { elapsed, settled } = (result ?? {}) as Partial<WaitForSootsimIdleResult>
154
+ return {
155
+ elapsed: typeof elapsed === 'number' ? elapsed : maxMs,
156
+ settled: settled === true,
157
+ }
158
+ }