@reticlehq/server 3.9.0 → 3.10.1

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 (351) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +43 -2
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -12,7 +12,7 @@
12
12
  import { type ScriptCoverage } from './js-coverage.js';
13
13
  import type { Browser, Page } from 'playwright';
14
14
  import { DriveError } from './drive-error.js';
15
- import { ActionType, InputMode } from '@reticlehq/core';
15
+ import { ActionType, InputMode, type BoxFraction } from '@reticlehq/core';
16
16
  import { type MockRule } from './network-mock.js';
17
17
  import { type NetworkDetail } from './network-detail.js';
18
18
  /** Viewport CSS-px box as returned by the INSPECT command (getBoundingClientRect). */
@@ -49,8 +49,12 @@ export type RealInputArgs = {
49
49
  * for exactly as long as one driven in the page.
50
50
  */
51
51
  holdMs?: number;
52
- /** For drag: the resolved box of the drop-target ref (toRef). */
52
+ /** For drag: the resolved box of the drop-target ref (toRef), or the source box for `to` alone. */
53
53
  toBox?: ElementBox;
54
+ /** For a pointer action: the point inside the box, as fractions of it. Absent means the centre. */
55
+ at?: BoxFraction;
56
+ /** For drag: the end point inside `toBox`, as fractions of it. Absent means its centre. */
57
+ to?: BoxFraction;
54
58
  steps?: number;
55
59
  };
56
60
  /** What a provider reports for one driven gesture. Exported so a fake can build it by name. */
@@ -80,19 +84,8 @@ export interface RealInputResult {
80
84
  */
81
85
  heldMs?: number;
82
86
  }
83
- /** Options for a page screenshot — full-page scroll capture and/or a clip box. */
84
- export interface ScreenshotOpts {
85
- fullPage?: boolean;
86
- /** Restrict the capture to one element/region (viewport CSS px). */
87
- clip?: ElementBox;
88
- /**
89
- * A live picture, as a JPEG at this quality: what a person is watching, taken as it is. The
90
- * baseline tweaks (hiding the HUD, stopping animations) would make their window flicker each second.
91
- */
92
- jpegQuality?: number;
93
- /** One image pixel per CSS pixel, so a high-density display does not double a step screenshot. */
94
- cssScale?: boolean;
95
- }
87
+ export { capturePage, type ScreenshotOpts } from './page-capture.js';
88
+ import { type ScreenshotOpts } from './page-capture.js';
96
89
  /** The capability surface reticle_act depends on. A FAKE implementing this is injected in tests. */
97
90
  export interface RealInputProvider {
98
91
  /** Whether a Playwright Page currently matches this SDK session URL. */
@@ -120,6 +113,14 @@ export interface RealInputProvider {
120
113
  */
121
114
  storageState?(sessionUrl: string): Promise<unknown>;
122
115
  applyStorageState?(sessionUrl: string, state: unknown): Promise<boolean>;
116
+ /**
117
+ * Close every page of the driven context except the one this provider drives, and say how many.
118
+ *
119
+ * A flow that opens another tab (a modifier-click on a link) leaves it behind; its page connects
120
+ * as a second session, and everything after that asks for "the" session and is refused. Optional:
121
+ * only a provider that owns its context can know which pages are its own.
122
+ */
123
+ closeOtherPages?(): Promise<number>;
123
124
  /**
124
125
  * Install (or replace, or with [] clear) network-mock rules on the correlated page — stub a 500,
125
126
  * force offline, delay a response — for deterministic error/edge-state testing. Returns true when
@@ -162,6 +163,11 @@ export declare function boxCenter(box: ElementBox): {
162
163
  cx: number;
163
164
  cy: number;
164
165
  };
166
+ /** The point `at` names inside a box, or its centre when `at` is absent. */
167
+ export declare function boxPoint(box: ElementBox, at: BoxFraction | undefined): {
168
+ cx: number;
169
+ cy: number;
170
+ };
165
171
  /**
166
172
  * Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
167
173
  * pointer, and widening this would change what every existing caller of it means.
@@ -191,11 +197,6 @@ now?: () => number): Promise<RealInputResult>;
191
197
  export declare function withChromePassThrough<T>(page: {
192
198
  evaluate: (script: string) => Promise<unknown>;
193
199
  }, gesture: () => Promise<T>): Promise<T>;
194
- /**
195
- * Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
196
- * screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
197
- */
198
- export declare function capturePage(page: Page, opts: ScreenshotOpts): Promise<Uint8Array>;
199
200
  interface CdpProviderOptions {
200
201
  cdpUrl: string;
201
202
  /** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
@@ -297,6 +298,7 @@ export declare class LaunchedRealInputProvider implements OwnedRealInputProvider
297
298
  * driven session had no fixture port at all, and every suite seeded nothing while reading as if it
298
299
  * seeded everything. Same two methods, same contract, one page instead of a page lookup.
299
300
  */
301
+ closeOtherPages(): Promise<number>;
300
302
  storageState(_sessionUrl: string): Promise<unknown>;
301
303
  /**
302
304
  * Put a captured state back: cookies through the context, per-origin storage through the page.
@@ -15,20 +15,33 @@ import { pressViaKeyboard } from './real-input-press.js';
15
15
  import { stampedDriveUrl } from './drive-url-stamp.js';
16
16
  import { launchChromium } from '../../launch-chromium.js';
17
17
  import { pageViewportOptions } from '../../chromium-launch-options.js';
18
- import { chromiumLaunchHint, gotoOptions, SCREENSHOT_DETERMINISM, captureWithChromeHidden, } from '../pool/playwright-launcher.js';
18
+ import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
19
19
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
20
20
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
21
21
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
22
- import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, ClickButton, clickButtonFromArgs, InputMode, pressModifiersFromArgs, RETICLE_CHROME_PASS_THROUGH_SCRIPT, RETICLE_CHROME_RESTORE_POINTER_SCRIPT, } from '@reticlehq/core';
22
+ import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, ClickButton, clickButtonFromArgs, InputMode, pointInBox, pressModifiersFromArgs, RETICLE_CHROME_PASS_THROUGH_SCRIPT, RETICLE_CHROME_RESTORE_POINTER_SCRIPT, } from '@reticlehq/core';
23
23
  import { installNetworkMocks } from './network-mock.js';
24
24
  import { attachNetworkDetail } from './network-detail.js';
25
+ import { attachDownloadCapture } from './download-capture.js';
25
26
  import { injectedConnectArgs } from '../pool/zero-install.js';
27
+ import { EARLY_CAPTURE_ARG, queueEarlyConsole } from '../pool/lease-page-error.js';
28
+ // The screenshot capture lives in page-capture.ts; re-exported for existing importers.
29
+ export { capturePage } from './page-capture.js';
30
+ import { capturePage } from './page-capture.js';
26
31
  export { DriveError };
27
32
  export { unspellablePressReason } from './real-input-press.js';
28
33
  /** Center of a viewport box in CSS px. Pure — unit-tested directly. */
29
34
  export function boxCenter(box) {
30
35
  return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
31
36
  }
37
+ /** The point `at` names inside a box, or its centre when `at` is absent. */
38
+ export function boxPoint(box, at) {
39
+ if (at === undefined) {
40
+ return boxCenter(box);
41
+ }
42
+ const p = pointInBox(box, at);
43
+ return { cx: p.x, cy: p.y };
44
+ }
32
45
  /**
33
46
  * Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
34
47
  * pointer, and widening this would change what every existing caller of it means.
@@ -107,7 +120,7 @@ export async function withChromePassThrough(page, gesture) {
107
120
  }
108
121
  /** The pointer half of `performGesture`: every action that lands at a point. */
109
122
  async function pointerGesture(page, action, box, args, sleep) {
110
- const center = boxCenter(box);
123
+ const center = boxPoint(box, args.at);
111
124
  const { cx, cy } = center;
112
125
  const real = (performed) => ({
113
126
  performed,
@@ -147,7 +160,7 @@ async function pointerGesture(page, action, box, args, sleep) {
147
160
  if (args.toBox === undefined) {
148
161
  return real(false);
149
162
  }
150
- const dst = boxCenter(args.toBox);
163
+ const dst = boxPoint(args.toBox, args.to);
151
164
  const steps = args.steps ?? DEFAULT_DRAG_STEPS;
152
165
  await page.mouse.move(cx, cy);
153
166
  await page.mouse.down();
@@ -166,31 +179,6 @@ async function pointerGesture(page, action, box, args, sleep) {
166
179
  }
167
180
  return real(false);
168
181
  }
169
- /**
170
- * Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
171
- * screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
172
- */
173
- export async function capturePage(page, opts) {
174
- // A step capture injects nothing into the page, so Reticle's own hosts are hidden another way.
175
- if (true === opts.cssScale) {
176
- return captureWithChromeHidden({ evaluate: (script) => page.evaluate(script) }, () => shoot(page, opts));
177
- }
178
- return shoot(page, opts);
179
- }
180
- async function shoot(page, opts) {
181
- const buf = await page.screenshot(opts.jpegQuality !== undefined
182
- ? {
183
- type: 'jpeg',
184
- quality: opts.jpegQuality,
185
- ...(true === opts.cssScale ? { scale: 'css' } : {}),
186
- }
187
- : opts.clip !== undefined
188
- ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip }
189
- : true === opts.fullPage
190
- ? { ...SCREENSHOT_DETERMINISM, fullPage: true }
191
- : { ...SCREENSHOT_DETERMINISM });
192
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
193
- }
194
182
  const nodeSleep = (ms) => new Promise((resolve) => {
195
183
  setTimeout(resolve, ms);
196
184
  });
@@ -433,9 +421,19 @@ export class LaunchedRealInputProvider {
433
421
  ...(this.#storageState !== undefined ? { storageState: this.#storageState } : {}),
434
422
  });
435
423
  this.#page = page;
424
+ // Before goto: a console error the page logs while it loads comes before its SDK connects.
425
+ await page.addInitScript(queueEarlyConsole, EARLY_CAPTURE_ARG);
436
426
  // Capture CDP-authoritative response detail into the driven session's journal (best-effort).
437
- if (this.#onNetworkDetail !== undefined) {
438
- attachNetworkDetail(page, this.#onNetworkDetail);
427
+ const sink = this.#onNetworkDetail;
428
+ if (sink !== undefined) {
429
+ attachNetworkDetail(page, sink);
430
+ // A server-sent download never reaches the page's network events; CDP request interception
431
+ // does see it (download-capture.ts). Attached before the first navigation, best-effort.
432
+ await page
433
+ .context()
434
+ .newCDPSession(page)
435
+ .then((cdp) => attachDownloadCapture(cdp, sink, () => page.url()))
436
+ .catch(() => undefined);
439
437
  }
440
438
  try {
441
439
  // Same navigation rule as the pool, and for the same measured reason: Playwright's default
@@ -538,6 +536,18 @@ export class LaunchedRealInputProvider {
538
536
  * driven session had no fixture port at all, and every suite seeded nothing while reading as if it
539
537
  * seeded everything. Same two methods, same contract, one page instead of a page lookup.
540
538
  */
539
+ async closeOtherPages() {
540
+ const page = this.#livePage();
541
+ if (page === undefined) {
542
+ return 0;
543
+ }
544
+ const others = page
545
+ .context()
546
+ .pages()
547
+ .filter((other) => other !== page && !other.isClosed());
548
+ await Promise.all(others.map((other) => other.close()));
549
+ return others.length;
550
+ }
541
551
  async storageState(_sessionUrl) {
542
552
  const page = this.#livePage();
543
553
  if (page === undefined) {
@@ -10,6 +10,10 @@ export declare const REDUCED_MOTION_ARG: z.ZodOptional<z.ZodEnum<{
10
10
  reduce: "reduce";
11
11
  "no-preference": "no-preference";
12
12
  }>>;
13
+ export declare const POINTER_ARG: z.ZodOptional<z.ZodEnum<{
14
+ fine: "fine";
15
+ coarse: "coarse";
16
+ }>>;
13
17
  export declare const LEASE_EMULATE_ARG: z.ZodOptional<z.ZodObject<{
14
18
  colorScheme: z.ZodOptional<z.ZodEnum<{
15
19
  light: "light";
@@ -22,6 +26,10 @@ export declare const LEASE_EMULATE_ARG: z.ZodOptional<z.ZodObject<{
22
26
  }>>;
23
27
  width: z.ZodOptional<z.ZodNumber>;
24
28
  height: z.ZodOptional<z.ZodNumber>;
29
+ pointer: z.ZodOptional<z.ZodEnum<{
30
+ fine: "fine";
31
+ coarse: "coarse";
32
+ }>>;
25
33
  }, z.core.$strict>>;
26
34
  /** The media part of what a caller asked for, or undefined when it asked for none. */
27
35
  export declare function mediaOf(raw: {
@@ -4,11 +4,12 @@ import { ReticleTool } from '@reticlehq/core';
4
4
  import { sessionIdShape } from '../../surface/tools/tool-kit.js';
5
5
  import { MAX_VIEWPORT_PX, MIN_VIEWPORT_PX, viewportPxSchema, } from '../../surface/tools/args/numeric-bounds.js';
6
6
  import { asString } from '@reticlehq/core';
7
- import { ColorScheme, ReducedMotion, } from '../pool/lease-emulation.js';
7
+ import { HEADLESS_VIEWPORT } from '../../chromium-launch-options.js';
8
+ import { ColorScheme, Pointer, ReducedMotion, } from '../pool/lease-emulation.js';
8
9
  // ── Media and width, as a tool call names them (reticle_viewport here, reticle_lease acquire) ──
9
- /** Playwright's own default page size, used for the side a caller did not name. */
10
- const DEFAULT_WIDTH = 1280;
11
- const DEFAULT_HEIGHT = 720;
10
+ /** The side a caller did not name is the page size a headless lease already opens with. */
11
+ const DEFAULT_WIDTH = HEADLESS_VIEWPORT.width;
12
+ const DEFAULT_HEIGHT = HEADLESS_VIEWPORT.height;
12
13
  export const COLOR_SCHEME_ARG = z
13
14
  .enum([ColorScheme.LIGHT, ColorScheme.DARK, ColorScheme.NO_PREFERENCE])
14
15
  .optional()
@@ -17,16 +18,24 @@ export const REDUCED_MOTION_ARG = z
17
18
  .enum([ReducedMotion.REDUCE, ReducedMotion.NO_PREFERENCE])
18
19
  .optional()
19
20
  .describe('prefers-reduced-motion for this page: reduce or no-preference.');
21
+ export const POINTER_ARG = z
22
+ .enum([Pointer.FINE, Pointer.COARSE])
23
+ .optional()
24
+ .describe('The primary pointer this page reports to @media (pointer: ...): fine (a mouse) or coarse (touch). ' +
25
+ 'Settable only when the lease opens, because touch support is fixed for a browser context.');
20
26
  const EmulateSchema = z
21
27
  .object({
22
28
  colorScheme: COLOR_SCHEME_ARG,
23
29
  reducedMotion: REDUCED_MOTION_ARG,
24
30
  width: viewportPxSchema.optional(),
25
31
  height: viewportPxSchema.optional(),
32
+ pointer: POINTER_ARG,
26
33
  })
27
34
  .strict();
28
- export const LEASE_EMULATE_ARG = EmulateSchema.optional().describe('Open the lease with these set before the first paint: { colorScheme, reducedMotion, width, height }. ' +
29
- 'A side you leave out keeps its default (1280 by 720). Change them later with reticle_viewport.');
35
+ export const LEASE_EMULATE_ARG = EmulateSchema.optional().describe('Open the lease with these set before the first paint: { colorScheme, reducedMotion, width, height, pointer }. ' +
36
+ 'A side you leave out keeps its default (1440 by 900). Change the media and the size later with ' +
37
+ 'reticle_viewport; `pointer` cannot change on an open lease, so a touch-only rule needs a lease ' +
38
+ 'acquired with pointer: "coarse".');
30
39
  /** The media part of what a caller asked for, or undefined when it asked for none. */
31
40
  export function mediaOf(raw) {
32
41
  const parsed = EmulateSchema.pick({ colorScheme: true, reducedMotion: true }).parse({
@@ -41,21 +50,27 @@ export function mediaOf(raw) {
41
50
  }
42
51
  /** A lease's `emulate` argument, read; a value it does not know is refused by name. */
43
52
  export function leaseEmulationOf(raw) {
44
- if (raw === undefined)
53
+ if (raw === undefined) {
45
54
  return undefined;
55
+ }
46
56
  const parsed = EmulateSchema.safeParse(raw);
47
57
  if (!parsed.success) {
48
58
  const field = parsed.error.issues[0]?.path.join('.') ?? 'emulate';
49
59
  throw new Error(`emulate.${field}: ${parsed.error.issues[0]?.message ?? 'not valid'}`);
50
60
  }
51
- const { width, height } = parsed.data;
61
+ const { width, height, pointer } = parsed.data;
52
62
  const media = mediaOf(parsed.data);
53
63
  const viewport = width === undefined && height === undefined
54
64
  ? undefined
55
65
  : { width: width ?? DEFAULT_WIDTH, height: height ?? DEFAULT_HEIGHT };
56
- if (media === undefined && viewport === undefined)
66
+ if (media === undefined && viewport === undefined && pointer === undefined) {
57
67
  return undefined;
58
- return { ...media, ...(viewport === undefined ? {} : { viewport }) };
68
+ }
69
+ return {
70
+ ...media,
71
+ ...(viewport === undefined ? {} : { viewport }),
72
+ ...(pointer === undefined ? {} : { pointer }),
73
+ };
59
74
  }
60
75
  /** Bounds so a viewport request stays sane (and a typo can't ask for a 1px or 100k-px window). */
61
76
  const MIN_DIM = MIN_VIEWPORT_PX;
@@ -66,6 +81,25 @@ function viewportProvider(deps) {
66
81
  ? p
67
82
  : undefined;
68
83
  }
84
+ /**
85
+ * Whether this caller holds the session as a lease. Another agent's lease is never this caller's
86
+ * to change (#1226).
87
+ */
88
+ function holdsLease(deps, sessionId) {
89
+ return true === deps.pool?.heldBy(sessionId, deps.attachId);
90
+ }
91
+ /** Whether this call can resize the session: a lease of this caller, or a driven page. */
92
+ async function canResize(deps, session) {
93
+ if (holdsLease(deps, session.id)) {
94
+ return true;
95
+ }
96
+ // A driven browser resizes only a page it has. Having a driven browser is not enough.
97
+ const driven = viewportProvider(deps);
98
+ if (driven === undefined) {
99
+ return false;
100
+ }
101
+ return driven.isAvailableFor(session.url);
102
+ }
69
103
  export const VIEWPORT_TOOLS = [
70
104
  {
71
105
  name: ReticleTool.VIEWPORT,
@@ -73,12 +107,14 @@ export const VIEWPORT_TOOLS = [
73
107
  'reproducible across machines — the missing piece of CI-stable visual regression, alongside ' +
74
108
  'reticle_visual_diff `masks` and a frozen clock (reticle_clock). Set it once before reticle_screenshot / ' +
75
109
  'reticle_visual_diff. On a lease it also sets prefers-color-scheme and prefers-reduced-motion; ' +
76
- 'give only those to change the media and keep the size. Returns { applied, width, height, observed } or the no-provider recommendation; `observed` is the size the leased page reports after the resize.',
110
+ 'give only those to change the media and keep the size. A `pointer` is refused here: touch support is fixed when a lease opens, so ask for it with reticle_lease acquire `emulate.pointer`. ' +
111
+ 'Returns { applied, width, height, observed } or the no-provider recommendation; `observed` is the size the leased page reports after the resize.',
77
112
  inputSchema: {
78
113
  width: viewportPxSchema.optional().describe('Viewport width in CSS px (e.g. 1280).'),
79
114
  height: viewportPxSchema.optional().describe('Viewport height in CSS px (e.g. 800).'),
80
115
  colorScheme: COLOR_SCHEME_ARG,
81
116
  reducedMotion: REDUCED_MOTION_ARG,
117
+ pointer: POINTER_ARG,
82
118
  ...sessionIdShape,
83
119
  },
84
120
  outputSchema: {
@@ -88,14 +124,27 @@ export const VIEWPORT_TOOLS = [
88
124
  observed: z.object({ width: z.number(), height: z.number() }).optional(),
89
125
  colorScheme: z.string().optional(),
90
126
  reducedMotion: z.string().optional(),
127
+ pointer: z.string().optional(),
91
128
  ok: z.boolean().optional(),
92
129
  reason: z.string().optional(),
93
130
  recommendation: z.string().optional(),
94
131
  },
95
132
  handler: async (deps, args) => {
133
+ // Touch support is a Playwright context option and a lease is one context, so a pointer can
134
+ // only be chosen when the lease opens. Said out loud instead of dropped: a silently ignored
135
+ // `pointer` would hand back a verdict about a touch-only rule that was taken with a mouse.
136
+ if (args['pointer'] !== undefined) {
137
+ return {
138
+ applied: false,
139
+ ok: false,
140
+ reason: POINTER_IS_FIXED,
141
+ recommendation: POINTER_NEEDS_A_NEW_LEASE,
142
+ };
143
+ }
96
144
  const media = mediaOf(args);
97
145
  if (media !== undefined && args['width'] === undefined && args['height'] === undefined) {
98
- const session = deps.sessions.resolve(asString(args['sessionId']));
146
+ // Only a lease can set the media.
147
+ const session = await deps.sessions.resolveServing(asString(args['sessionId']), (s) => holdsLease(deps, s.id));
99
148
  if (true === (await deps.pool?.emulateLease(session.id, media))) {
100
149
  // No size was asked for, so answer the one the lease has; 0 by 0 reads as a broken
101
150
  // viewport (#1902). When the page cannot be read, leave the size out.
@@ -106,7 +155,7 @@ export const VIEWPORT_TOOLS = [
106
155
  }
107
156
  const width = clampDim('width', args['width']);
108
157
  const height = clampDim('height', args['height']);
109
- const session = deps.sessions.resolve(asString(args['sessionId']));
158
+ const session = await deps.sessions.resolveServing(asString(args['sessionId']), (s) => canResize(deps, s));
110
159
  // A lease is resized as the lease. The driven provider resizes its own page, whatever
111
160
  // session was named, so taking it first resized the wrong tab and said `applied: true`.
112
161
  const driven = viewportProvider(deps);
@@ -114,10 +163,12 @@ export const VIEWPORT_TOOLS = [
114
163
  if (provider !== undefined) {
115
164
  const applied = await provider.setViewport(session.url, { width, height });
116
165
  // The driven page takes the size; its media stay as the person's browser has them.
117
- if (applied && media === undefined)
166
+ if (applied && media === undefined) {
118
167
  return { applied: true, width, height };
119
- if (applied)
168
+ }
169
+ if (applied) {
120
170
  return { applied: true, width, height, recommendation: MEDIA_NEEDS_A_LEASE };
171
+ }
121
172
  }
122
173
  // A lease is a Playwright-owned page, so the resize was always possible — this tool just had
123
174
  // no route to it. Tried after the driven provider, on the same rule reticle_network_mock
@@ -128,16 +179,18 @@ export const VIEWPORT_TOOLS = [
128
179
  // only mounts under a breakpoint — could not be driven at a desktop viewport at all, and the
129
180
  // recommendation printed alongside the refusal asked the reader to install a second browser.
130
181
  const leased = await deps.pool?.setViewportLease(session.id, { width, height });
131
- if (true !== leased)
182
+ if (true !== leased) {
132
183
  return refused();
184
+ }
133
185
  // What the page reports after the resize, so `applied` is a reading and not a hope.
134
186
  const observed = await deps.pool?.viewportOfLease(session.id);
135
187
  if (observed !== undefined && (observed.width !== width || observed.height !== height)) {
136
188
  return { applied: false, width, height, observed, ok: false, reason: VIEWPORT_NOT_TAKEN };
137
189
  }
138
190
  const seen = observed === undefined ? {} : { observed };
139
- if (media === undefined)
191
+ if (media === undefined) {
140
192
  return { applied: true, width, height, ...seen };
193
+ }
141
194
  if (true === (await deps.pool?.emulateLease(session.id, media))) {
142
195
  return { applied: true, width, height, ...seen, ...media };
143
196
  }
@@ -147,6 +200,9 @@ export const VIEWPORT_TOOLS = [
147
200
  ];
148
201
  /** The resize went through, but the page still reports another size. */
149
202
  const VIEWPORT_NOT_TAKEN = 'the page still reports the size in `observed` after the resize, so the size was not applied';
203
+ /** Touch support rides on the browser context, so an open lease's pointer cannot be changed. */
204
+ const POINTER_IS_FIXED = "a lease's pointer is fixed when its context opens, so it cannot be changed on a live lease";
205
+ const POINTER_NEEDS_A_NEW_LEASE = 'Acquire a new lease with emulate.pointer: reticle_lease { action: "acquire", emulate: { pointer: "coarse" } }, then call this again with its sessionId.';
150
206
  /** prefers-color-scheme and prefers-reduced-motion are set on a leased page only. */
151
207
  const MEDIA_NEEDS_A_LEASE = 'Media features are set on a leased tab: open one with reticle_lease { action: "acquire", emulate }, then call this again with its sessionId.';
152
208
  function refused() {
@@ -167,8 +223,9 @@ function refused() {
167
223
  * snapshot, click and screenshot on that page read as broken (#1118).
168
224
  */
169
225
  function clampDim(key, value) {
170
- if (value === undefined)
226
+ if (value === undefined) {
171
227
  return MIN_DIM;
228
+ }
172
229
  if ('number' !== typeof value || !Number.isFinite(value)) {
173
230
  throw new Error(`${ReticleTool.VIEWPORT}: \`${key}\` must be a number of CSS px, got ${JSON.stringify(value)} — NOT applied`);
174
231
  }
@@ -9,12 +9,12 @@
9
9
  * The browser is injected via a `Launcher` so all lifecycle logic is testable without real Chromium;
10
10
  * the thin Playwright adapter that satisfies these interfaces lives separately.
11
11
  */
12
- import { type SeedStorage } from '@reticlehq/core';
13
- import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
12
+ import type { Launcher, PooledCoverage, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
14
13
  import type { RealInputArgs, RealInputResult } from '../input/real-input.js';
15
- import { type LeaseEmulation, type LeaseMedia } from './lease-emulation.js';
14
+ import { type LeaseMedia } from './lease-emulation.js';
15
+ import { type DeterminismStep } from './determinism.js';
16
16
  export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, PooledPageError, } from './pool-contract.js';
17
- import { type BrowserPoolOptions, type Lease } from './pool-lease.js';
17
+ import { type AcquireOptions, type BrowserPoolOptions, type Lease } from './pool-lease.js';
18
18
  export { DEFAULT_LEASE_TTL_MS, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, PoolAtCapacityError, } from './pool-lease.js';
19
19
  export type { Lease } from './pool-lease.js';
20
20
  /**
@@ -119,6 +119,8 @@ export declare class BrowserPool {
119
119
  * Touches the lease like any other tool call, so hovering keeps it alive.
120
120
  */
121
121
  hoverLease(sessionId: string, x: number, y: number): Promise<boolean>;
122
+ /** Reload a lease with the browser cache ignored, or false when this is not a lease or its page cannot. */
123
+ hardReloadLease(sessionId: string): Promise<boolean>;
122
124
  /** Set a live lease's media features, or false when this is not a lease or its page cannot. */
123
125
  emulateLease(sessionId: string, media: LeaseMedia): Promise<boolean>;
124
126
  /** Whether this lease's page is still loading. False for a non-lease, or a page that cannot say. */
@@ -155,6 +157,10 @@ export declare class BrowserPool {
155
157
  height: number;
156
158
  } | undefined>;
157
159
  setMocksLease(sessionId: string, rules: readonly PooledMockRule[]): Promise<boolean>;
160
+ /** Step a deterministic lease's clock by `frames` frames; undefined when it is not one. */
161
+ stepLease(sessionId: string, frames: number): Promise<DeterminismStep | undefined>;
162
+ /** The JS coverage of a lease's page, or undefined when this is not a lease or cannot measure. */
163
+ coverageOf(sessionId: string): PooledCoverage | undefined;
158
164
  /** Guard a leased page's requests, or false when this is not a lease or its page cannot. */
159
165
  guardLease(sessionId: string, decide: (method: string, url: string) => string | undefined): Promise<boolean>;
160
166
  /** What a lease was granted when it opened, or undefined for a session that is not a lease. */
@@ -207,31 +213,7 @@ export declare class BrowserPool {
207
213
  * waits FIFO until a slot frees (or until `signal` aborts, if provided), unless `whenFull` is
208
214
  * `'refuse'`: then it throws {@link PoolAtCapacityError} at once and claims nothing.
209
215
  */
210
- acquire(url: string, opts?: {
211
- signal?: AbortSignal;
212
- /**
213
- * `'wait'` (the default) queues behind the cap. `'refuse'` is for a caller that has a person
214
- * or an agent waiting on the answer and no way to see a queue: it is told the pool is full.
215
- */
216
- whenFull?: 'wait' | 'refuse';
217
- sessionId?: string;
218
- seedStorage?: SeedStorage;
219
- owner?: string;
220
- /** In a browser with a window the person can watch. */
221
- headed?: boolean;
222
- /**
223
- * JavaScript installed before any page script runs, for this lease only. A view built for a
224
- * desktop shell calls bindings the shell injects (`window.go`, `window.runtime`, a preload
225
- * object) on mount; a stub here is what lets it reach its data path in a leased tab.
226
- */
227
- initScript?: string;
228
- /** Granted on the lease's origin before the first navigation, so the first render sees them. */
229
- permissions?: readonly string[];
230
- /** Accept a self-signed or local-CA dev certificate for this lease's context only (#1255). */
231
- ignoreHTTPSErrors?: boolean;
232
- /** Media and viewport set on the context, so the first paint already has them. */
233
- emulate?: LeaseEmulation;
234
- }): Promise<Lease>;
216
+ acquire(url: string, opts?: AcquireOptions): Promise<Lease>;
235
217
  /**
236
218
  * The websocket address this lease's page reported it could not reach, if it reported one.
237
219
  *
@@ -16,7 +16,8 @@ import { seedStorageInto, targetOriginOf } from './storage-seed.js';
16
16
  import { installLeaseMarker, leaseMarkerOf } from './lease-marker-stamp.js';
17
17
  import { grantLeasePermissions, grantLocalNetworkAccess, readNotificationPermission, widenLeaseCsp, } from './context-permissions.js';
18
18
  import { readViewport } from './lease-emulation.js';
19
- import { NotHtmlDocumentError, READER_NOT_HTML } from './zero-install.js';
19
+ import { injectReaderInto } from './zero-install.js';
20
+ import { stepPage } from './determinism.js';
20
21
  import { DEFAULT_LEASE_TTL_MS, DEFAULT_NAV_TIMEOUT_MS, INIT_SCRIPT_UNSUPPORTED, MAX_REMEMBERED_REAPED_LEASES, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, navStatusOf, pageLoading, PoolAtCapacityError, } from './pool-lease.js';
21
22
  import { listenForPageErrors, reportLeaseClosed, } from './lease-page-error.js';
22
23
  export { DEFAULT_LEASE_TTL_MS, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, PoolAtCapacityError, } from './pool-lease.js';
@@ -77,7 +78,7 @@ export class BrowserPool {
77
78
  this.#navTimeout = opts.navTimeoutMs ?? DEFAULT_NAV_TIMEOUT_MS;
78
79
  this.#zeroInstall = opts.zeroInstallScript;
79
80
  this.#daemonOrigin = opts.daemonOrigin;
80
- this.#pageErrorHooks = { onPageError: opts.onPageError, onLeaseClosed: opts.onLeaseClosed };
81
+ this.#pageErrorHooks = opts;
81
82
  }
82
83
  /** The TTL configured for leases — how long an untouched lease lives before the reaper reclaims it. */
83
84
  leaseTtlMs() {
@@ -303,6 +304,21 @@ export class BrowserPool {
303
304
  return false;
304
305
  }
305
306
  }
307
+ /** Reload a lease with the browser cache ignored, or false when this is not a lease or its page cannot. */
308
+ async hardReloadLease(sessionId) {
309
+ const lease = this.#active.get(this.#leaseIdOf(sessionId));
310
+ if (lease?.page.reloadIgnoringCache === undefined) {
311
+ return false;
312
+ }
313
+ this.touch(sessionId);
314
+ try {
315
+ await lease.page.reloadIgnoringCache();
316
+ return true;
317
+ }
318
+ catch {
319
+ return false;
320
+ }
321
+ }
306
322
  /** Set a live lease's media features, or false when this is not a lease or its page cannot. */
307
323
  async emulateLease(sessionId, media) {
308
324
  const lease = this.#active.get(this.#leaseIdOf(sessionId));
@@ -395,6 +411,17 @@ export class BrowserPool {
395
411
  return false;
396
412
  }
397
413
  }
414
+ /** Step a deterministic lease's clock by `frames` frames; undefined when it is not one. */
415
+ stepLease(sessionId, frames) {
416
+ const page = this.#active.get(this.#leaseIdOf(sessionId))?.page;
417
+ this.touch(sessionId);
418
+ return page === undefined ? Promise.resolve(undefined) : stepPage(page, frames);
419
+ }
420
+ /** The JS coverage of a lease's page, or undefined when this is not a lease or cannot measure. */
421
+ coverageOf(sessionId) {
422
+ this.touch(sessionId);
423
+ return this.#active.get(this.#leaseIdOf(sessionId))?.page.jsCoverage;
424
+ }
398
425
  /** Guard a leased page's requests, or false when this is not a lease or its page cannot. */
399
426
  async guardLease(sessionId, decide) {
400
427
  const lease = this.#active.get(this.#leaseIdOf(sessionId));
@@ -513,6 +540,7 @@ export class BrowserPool {
513
540
  const browser = await this.#ensureBrowser(slot);
514
541
  const contextOpts = {
515
542
  ...(true === opts.ignoreHTTPSErrors ? { ignoreHTTPSErrors: true } : {}),
543
+ ...(true === opts.jsCoverage ? { jsCoverage: true } : {}),
516
544
  ...opts.emulate,
517
545
  };
518
546
  context = await browser.newContext(0 < Object.keys(contextOpts).length ? contextOpts : undefined);
@@ -552,9 +580,8 @@ export class BrowserPool {
552
580
  pending = dialled;
553
581
  } // logged during goto, before the lease is registered below
554
582
  });
555
- // Same "listen before goto" reasoning as onConsole above: a dialog opened during the initial
556
- // navigation (an app that confirms something in an onload handler) must still be dismissed, not
557
- // just ones opened after the lease is registered.
583
+ // Listen before goto, as onConsole does: a dialog in an onload handler is dismissed too. The SDK
584
+ // answers a dialog from its own input first; the hook records this "no" for the verdict (#1350).
558
585
  page.onDialog?.((dialog) => {
559
586
  const active = this.#active.get(sessionId);
560
587
  if (active !== undefined) {
@@ -564,9 +591,10 @@ export class BrowserPool {
564
591
  pendingDialogMessage = dialog.message;
565
592
  }
566
593
  void dialog.dismiss();
594
+ this.#pageErrorHooks?.onDialog?.(sessionId, dialog);
567
595
  });
568
596
  // Listener on BEFORE goto — an import-time throw lands during it (#1966).
569
- listenForPageErrors(page, sessionId, this.#pageErrorHooks);
597
+ await listenForPageErrors(page, sessionId, this.#pageErrorHooks);
570
598
  const marker = leaseMarkerOf(url);
571
599
  if (marker?.session === sessionId) {
572
600
  await installLeaseMarker(page, sessionId, targetOriginOf(url), marker.project);
@@ -620,17 +648,7 @@ export class BrowserPool {
620
648
  url,
621
649
  ...(navStatus !== undefined ? { navStatus } : {}),
622
650
  release: () => this.#release(sessionId),
623
- injectReader: async () => {
624
- const script = this.#zeroInstall?.(url);
625
- if (script === undefined || page.evaluate === undefined) {
626
- return false;
627
- }
628
- await page.addInitScript?.(script);
629
- if (READER_NOT_HTML === (await page.evaluate(script))) {
630
- throw new NotHtmlDocumentError();
631
- }
632
- return true;
633
- },
651
+ injectReader: () => injectReaderInto(page, this.#zeroInstall?.(url)),
634
652
  isLoading: () => pageLoading(page),
635
653
  };
636
654
  }