@reticlehq/server 3.5.0 → 3.6.0

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 (328) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -11,7 +11,7 @@ import { tokensMatch } from './token-auth.js';
11
11
  import { pairingTokenSource } from './pairing-token.js';
12
12
  import { log } from '../../log.js';
13
13
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
14
- import { sessionReplacedReason } from '../session/facts/session-replaced.js';
14
+ import { SESSION_PARKED_REASON, sessionReplacedReason, } from '../session/facts/session-replaced.js';
15
15
  import { describeSkew, sdkFix, SkewPair } from '../../command/version/version-skew.js';
16
16
  import { noteVersionSkew } from '../../command/version/version-nudge.js';
17
17
  import { protocolSkewReason } from './protocol-skew.js';
@@ -147,6 +147,10 @@ function harnessRequest(event) {
147
147
  return false;
148
148
  return undefined;
149
149
  }
150
+ /** True when this event is the panel's Sign in. Pure boundary narrowing, like the above. */
151
+ function isSigninRequest(event) {
152
+ return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SIGNIN;
153
+ }
150
154
  /** True when this event is the panel's "sync now" button. Pure boundary narrowing, like the above. */
151
155
  function isSyncRequest(event) {
152
156
  return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SYNC;
@@ -273,6 +277,7 @@ export class Bridge {
273
277
  /** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
274
278
  #onSyncRequest;
275
279
  #onHarnessRequest;
280
+ #onSigninRequest;
276
281
  constructor(options) {
277
282
  const host = options.host ?? LOOPBACK_HOST;
278
283
  if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
@@ -543,6 +548,11 @@ export class Bridge {
543
548
  socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
544
549
  return;
545
550
  }
551
+ // A window the agent detached stays detached across its own reloads. See SessionManager.park.
552
+ if (this.sessions.isParked(parsed.sessionId)) {
553
+ socket.close(1008, SESSION_PARKED_REASON);
554
+ return;
555
+ }
546
556
  const existing = this.sessions.get(parsed.sessionId);
547
557
  if (existing === undefined && this.sessions.count() >= this.#maxSessions) {
548
558
  socket.close(...WS_CLOSE.SESSION_LIMIT);
@@ -695,7 +705,10 @@ export class Bridge {
695
705
  // The switch is written through to the platform by the daemon, for the same reason: the
696
706
  // Session has no cloud credential and should not grow one.
697
707
  else if (harness !== undefined)
698
- this.#onHarnessRequest?.(harness);
708
+ this.#onHarnessRequest?.(harness, session);
709
+ // Signing in writes `~/.reticle`, which only the daemon may do; the panel only asks.
710
+ else if (isSigninRequest(parsed.event))
711
+ this.#onSigninRequest?.();
699
712
  // Pass the raw frame's byte length so the buffer doesn't re-serialize every event for accounting.
700
713
  else
701
714
  session.pushEvent(parsed.event, Buffer.byteLength(text, 'utf8'));
@@ -838,6 +851,10 @@ export class Bridge {
838
851
  attachHarnessRequest(handler) {
839
852
  this.#onHarnessRequest = handler;
840
853
  }
854
+ /** Register a handler for the panel's Sign in. Optional, like the above: without one it is ignored. */
855
+ attachSigninRequest(handler) {
856
+ this.#onSigninRequest = handler;
857
+ }
841
858
  /** Register a handler to run when a session connects. Additive — every handler runs. */
842
859
  attachSessionCreate(handler) {
843
860
  this.#onSessionCreate.push(handler);
@@ -33,6 +33,15 @@ export declare function pairingTokenSource(input: {
33
33
  explicit: boolean;
34
34
  env: NodeJS.ProcessEnv;
35
35
  }): 'explicit' | 'env:RETICLE_TOKEN' | 'env:RETICLE_PAIRING_TOKEN_DIR' | 'default';
36
+ /**
37
+ * The pairing token this machine was told to use, or undefined when it was told nothing.
38
+ *
39
+ * `RETICLE_TOKEN` first, then the token file in `RETICLE_PAIRING_TOKEN_DIR`, the precedence
40
+ * `pairingTokenSource` names. A command that mints its own one-shot token for a hosted preview asks
41
+ * this first: in a container the app's plugin bakes the directory token in, and a fresh token made
42
+ * the daemon refuse the only app it was started to verify (#1251).
43
+ */
44
+ export declare function configuredPairingToken(env: NodeJS.ProcessEnv, deps: PairingTokenDeps): Promise<string | undefined>;
36
45
  export declare function pairingTokenPath(dir: string): string;
37
46
  /** Production IO adapter: 0600 file + 0700 dir so only the owner can read the secret. */
38
47
  export declare function nodePairingTokenDeps(): PairingTokenDeps;
@@ -41,6 +41,23 @@ export function pairingTokenSource(input) {
41
41
  return `env:${PAIRING_TOKEN_DIR_ENV}`;
42
42
  return 'default';
43
43
  }
44
+ /**
45
+ * The pairing token this machine was told to use, or undefined when it was told nothing.
46
+ *
47
+ * `RETICLE_TOKEN` first, then the token file in `RETICLE_PAIRING_TOKEN_DIR`, the precedence
48
+ * `pairingTokenSource` names. A command that mints its own one-shot token for a hosted preview asks
49
+ * this first: in a container the app's plugin bakes the directory token in, and a fresh token made
50
+ * the daemon refuse the only app it was started to verify (#1251).
51
+ */
52
+ export async function configuredPairingToken(env, deps) {
53
+ const token = env[ReticleEnv.TOKEN]?.trim();
54
+ if (token !== undefined && 0 < token.length)
55
+ return token;
56
+ const dir = env[PAIRING_TOKEN_DIR_ENV];
57
+ if (dir === undefined || 0 === dir.length)
58
+ return undefined;
59
+ return readOrCreatePairingToken(dir, deps);
60
+ }
44
61
  export function pairingTokenPath(dir) {
45
62
  return join(dir, ReticleDir.PAIRING_TOKEN_FILE);
46
63
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { type ScriptCoverage } from './js-coverage.js';
13
13
  import type { Browser, Page } from 'playwright';
14
- import { ActionType, DriveErrorCode } from '@reticlehq/core';
14
+ import { ActionType, DriveErrorCode, InputMode, InputModeReason } from '@reticlehq/core';
15
15
  import { type MockRule } from './network-mock.js';
16
16
  import { type NetworkDetail } from './network-detail.js';
17
17
  /** Viewport CSS-px box as returned by the INSPECT command (getBoundingClientRect). */
@@ -21,28 +21,72 @@ export interface ElementBox {
21
21
  width: number;
22
22
  height: number;
23
23
  }
24
- /** Args forwarded from reticle_act (fill value, type text, drag drop-target box). */
25
- export interface RealInputArgs {
24
+ /**
25
+ * Args forwarded from reticle_act (fill value, type text, press key, drag drop-target box).
26
+ *
27
+ * A `type`, not an `interface`: only the alias form gets an implicit index signature, so the core
28
+ * arg readers take it as a plain record without a cast at every call site.
29
+ */
30
+ export type RealInputArgs = {
26
31
  value?: string;
27
32
  text?: string;
33
+ /** For press: the key name, e.g. Escape or Tab. `text` also carries it; `key` is the fallback. */
34
+ key?: string;
35
+ /** For press: modifier names (Meta/Control/Shift/Alt; aliases like `cmd` accepted). */
36
+ modifiers?: string[];
37
+ /**
38
+ * For press: several keys held down TOGETHER, pressed in order and released in reverse. Read but
39
+ * never forwarded — a chord with a non-modifier key has no `page.keyboard.press` spelling, so the
40
+ * caller routes such a press to the synthetic dispatcher before it reaches here.
41
+ */
42
+ keys?: string[];
43
+ /**
44
+ * For press: how long to keep the key down between keydown and keyup. The same argument the
45
+ * synthetic path has always honoured, so a hold-to-confirm key driven through the driver holds
46
+ * for exactly as long as one driven in the page.
47
+ */
48
+ holdMs?: number;
28
49
  /** For drag: the resolved box of the drop-target ref (toRef). */
29
50
  toBox?: ElementBox;
30
51
  steps?: number;
31
- }
32
- interface RealInputResult {
52
+ };
53
+ /** What a provider reports for one driven gesture. Exported so a fake can build it by name. */
54
+ export interface RealInputResult {
33
55
  /** True if a native gesture was actually driven. */
34
56
  performed: boolean;
35
- /** Center used, for diagnostics/tests. */
36
- center: {
57
+ /** Center used, for diagnostics/tests. ABSENT for a key press: see `undriven`. */
58
+ center?: {
37
59
  cx: number;
38
60
  cy: number;
39
61
  };
62
+ /**
63
+ * Which path this result came from. `real` whenever a gesture was actually driven, so a caller
64
+ * reading it on a `performed: true` result learns nothing it did not already know — it is here
65
+ * for the `performed: false` case, where the caller is about to take the synthetic path and
66
+ * saying `real` would describe a gesture that never ran.
67
+ */
68
+ inputMode: InputMode;
69
+ /**
70
+ * How long the key was ACTUALLY held, for a real press that was asked to hold. Absent for every
71
+ * other gesture, and for a press with no `holdMs` — the same rule the synthetic path uses, where
72
+ * an absent `heldMs` means "this action does not hold" rather than "it held for no time".
73
+ *
74
+ * Measured rather than echoed back: `holdMs: 1200` against a 1200ms animation is a race by
75
+ * construction, and a throttled tab stretches the wait. A caller needs to tell "held 1200" from
76
+ * "held 1204", and can only do that with the achieved number.
77
+ */
78
+ heldMs?: number;
40
79
  }
41
80
  /** Options for a page screenshot — full-page scroll capture and/or a clip box. */
42
81
  export interface ScreenshotOpts {
43
82
  fullPage?: boolean;
44
83
  /** Restrict the capture to one element/region (viewport CSS px). */
45
84
  clip?: ElementBox;
85
+ /**
86
+ * A live picture, as a JPEG at this quality: what a person is watching, taken as it is. The
87
+ * baseline tweaks (hiding the HUD, stopping animations) would make their window flicker each second.
88
+ */
89
+ jpegQuality?: number;
46
90
  }
47
91
  /** The capability surface reticle_act depends on. A FAKE implementing this is injected in tests. */
48
92
  export interface RealInputProvider {
@@ -117,17 +161,40 @@ export declare function boxCenter(box: ElementBox): {
117
161
  cy: number;
118
162
  };
119
163
  /**
120
- * Which actions are driven by native pointer input. fill/type stay synthetic unless a
121
- * provider explicitly runs them.
164
+ * Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
165
+ * pointer, and widening this would change what every existing caller of it means.
122
166
  */
123
167
  export declare function isPointerAction(action: ActionType): boolean;
168
+ /**
169
+ * The routing question `tryRealInput` asks: does this action have a real-input path at all? Wider
170
+ * than `isPointerAction` by `press`, which needs no coordinates.
171
+ */
172
+ export declare function isRealInputAction(action: ActionType): boolean;
124
173
  type SleepFn = (ms: number) => Promise<void>;
125
174
  type ConnectFn = (url: string) => Promise<Browser>;
175
+ /**
176
+ * The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
177
+ *
178
+ * `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
179
+ * That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
180
+ * — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
181
+ * presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
182
+ * `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
183
+ * routed to the synthetic path by the caller, which reports the reason rather than pretending a
184
+ * different gesture happened.
185
+ *
186
+ * Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
187
+ * the target. Two can apply at once, and the earlier one is the more unusual request — the reason
188
+ * should name that, not the generic "you passed a ref".
189
+ */
190
+ export declare function unspellablePressReason(ref: string, args: Record<string, unknown>): InputModeReason | undefined;
126
191
  /**
127
192
  * Shared gesture executor: drive a native gesture on an already-resolved Page. Used by both the
128
193
  * CDP-attached and the launched (drive) providers so the pointer logic lives in one place.
129
194
  */
130
- export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn): Promise<RealInputResult>;
195
+ export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn,
196
+ /** Injected so a test can assert the achieved hold deterministically; defaults to the real clock. */
197
+ now?: () => number): Promise<RealInputResult>;
131
198
  /**
132
199
  * Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
133
200
  * screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
@@ -137,6 +204,8 @@ interface CdpProviderOptions {
137
204
  cdpUrl: string;
138
205
  /** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
139
206
  sleep?: SleepFn;
207
+ /** Injected so a measured hold is deterministic in tests; defaults to the real clock. */
208
+ now?: () => number;
140
209
  /** Injected connector so unit tests can stub Playwright without import. */
141
210
  connect?: ConnectFn;
142
211
  /**
@@ -193,6 +262,8 @@ export interface LaunchedProviderOptions {
193
262
  headless: boolean;
194
263
  /** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
195
264
  sleep?: SleepFn;
265
+ /** Injected so a measured hold is deterministic in tests; defaults to the real clock. */
266
+ now?: () => number;
196
267
  /** Injected launcher so unit tests can stub Playwright; defaults to dynamic import('playwright'). */
197
268
  launch?: LaunchFn;
198
269
  /** When set, re-invoke the page's reticle.connect with these after load (drive-a-hosted-preview). */
@@ -12,11 +12,11 @@
12
12
  import { takeJsCoverage } from './js-coverage.js';
13
13
  import { stampedDriveUrl } from './drive-url-stamp.js';
14
14
  import { launchChromium } from '../../launch-chromium.js';
15
- import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
15
+ import { HIDE_RETICLE_CHROME_CSS, chromiumLaunchHint, gotoOptions, } from '../pool/playwright-launcher.js';
16
16
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
17
17
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
18
18
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
19
- import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG } from '@reticlehq/core';
19
+ import { ActionType, clampHoldMs, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, explicitCodeFromArgs, InputMode, InputModeReason, pressKeyFromArgs, pressKeysFromArgs, pressModifiersFromArgs, } from '@reticlehq/core';
20
20
  import { installNetworkMocks } from './network-mock.js';
21
21
  import { attachNetworkDetail } from './network-detail.js';
22
22
  import { injectedConnectArgs } from '../pool/zero-install.js';
@@ -34,8 +34,8 @@ export function boxCenter(box) {
34
34
  return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
35
35
  }
36
36
  /**
37
- * Which actions are driven by native pointer input. fill/type stay synthetic unless a
38
- * provider explicitly runs them.
37
+ * Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
38
+ * pointer, and widening this would change what every existing caller of it means.
39
39
  */
40
40
  export function isPointerAction(action) {
41
41
  return (action === ActionType.HOVER ||
@@ -43,37 +43,217 @@ export function isPointerAction(action) {
43
43
  action === ActionType.DBLCLICK ||
44
44
  action === ActionType.DRAG);
45
45
  }
46
+ /**
47
+ * The routing question `tryRealInput` asks: does this action have a real-input path at all? Wider
48
+ * than `isPointerAction` by `press`, which needs no coordinates.
49
+ */
50
+ export function isRealInputAction(action) {
51
+ return isPointerAction(action) || action === ActionType.PRESS;
52
+ }
53
+ /**
54
+ * A gesture that was NOT driven. No center for a key press — a zero would read as a real point.
55
+ *
56
+ * `inputMode` is SYNTHETIC: no real gesture ran, so this is the path the caller is about to take.
57
+ */
58
+ function undriven(action, box) {
59
+ const base = { performed: false, inputMode: InputMode.SYNTHETIC };
60
+ return action === ActionType.PRESS ? base : { ...base, center: boxCenter(box) };
61
+ }
46
62
  /** Settle delay after a native gesture so the reaction can begin to flush (named, not free). */
47
63
  const REAL_INPUT_SETTLE_MS = 16;
48
64
  /** Default number of interpolation steps for a native drag. */
49
65
  const DEFAULT_DRAG_STEPS = 8;
66
+ /**
67
+ * Drive a `press` with a real keyboard — coordinate-free; the key goes to whatever holds focus.
68
+ * Key and modifiers come from the same core helpers the synthetic dispatcher uses, so the two paths
69
+ * cannot read the same args differently.
70
+ *
71
+ * A `holdMs` splits the chord into down / wait / up, the same shape the page uses for a held key.
72
+ * `keyboard.press` would send it instantly instead, reporting a hold-to-confirm that never held.
73
+ * The one thing this cannot reproduce is the auto-repeat a held key emits: the driver sends a single
74
+ * keydown, where the in-page path sends the repeats a browser would. Named rather than papered over
75
+ * — an app that counts repeats needs the synthetic path, and a caller that sees a held key do
76
+ * nothing now has a reason to try it.
77
+ */
78
+ async function pressViaKeyboard(page, args, sleep, now) {
79
+ const key = pressKeyFromArgs(args);
80
+ const modifiers = pressModifiersFromArgs(args);
81
+ const chord = 0 < modifiers.length ? `${modifiers.join('+')}+${key}` : key;
82
+ const holdMs = clampHoldMs(args.holdMs);
83
+ if (0 === holdMs) {
84
+ // `press` DOES take a chord string ("Control+k"), so the tap path needs no splitting.
85
+ await page.keyboard.press(chord);
86
+ return 0;
87
+ }
88
+ /*
89
+ * `down`/`up` take ONE key each, unlike `press`: `down('Control+k')` throws
90
+ * `Unknown key: "Control+k"` — measured on a real Chromium, not assumed. The chord string the
91
+ * tap path uses therefore cannot express a hold, so the chord is split here: modifiers down in
92
+ * chord order, the key last, and released in reverse (key first, modifiers reversed), which is
93
+ * what a hand does and what an app's keyup bookkeeping expects.
94
+ *
95
+ * Without the split a held shortcut threw before a single key moved, the caller fell back to
96
+ * the synthetic path, and a real hold was impossible for any press with modifiers — the exact
97
+ * gesture `holdMs` exists for.
98
+ *
99
+ * ## Why every key that went down is tracked, and every exit path cleans up
100
+ *
101
+ * The down calls are the ones that MOVE the keyboard, and they can fail partway: `Control` goes
102
+ * down, the key after it rejects, and the driver's copy of `Control` is still held. That path
103
+ * used to fall straight to the caller's synthetic replay — which presses the same chord again on
104
+ * a keyboard that is already holding a modifier. Tracking what actually went down is what makes
105
+ * the cleanup below able to release the right keys, and only those: releasing a key that never
106
+ * went down is its own lie, and it would turn a clean failure into an unnecessary refusal.
107
+ */
108
+ const pressed = [];
109
+ const pressDown = async (k) => {
110
+ await page.keyboard.down(k);
111
+ pressed.push(k);
112
+ };
113
+ try {
114
+ for (const modifier of modifiers)
115
+ await pressDown(modifier);
116
+ await pressDown(key);
117
+ }
118
+ catch (error) {
119
+ // The chord is PARTIALLY down. Release what is held before reporting the failure; if that
120
+ // cannot be proved, `releaseKeys` throws RELEASE_FAILED and the caller refuses the replay —
121
+ // the same rule as a failed release on the ordinary path, for the same reason. Only when the
122
+ // cleanup DOES succeed is the original error safe to hand back.
123
+ await releaseKeys(page, pressed);
124
+ throw error;
125
+ }
126
+ const startedAt = now();
127
+ try {
128
+ await sleep(holdMs);
129
+ }
130
+ catch (error) {
131
+ // The wait failed, and the caller is about to run the synthetic path — which presses this SAME
132
+ // key a second time. If the driver's copy stays down, a held modifier then colours every later
133
+ // action for the rest of the run. Best-effort on purpose: whatever went wrong with the wait is
134
+ // the error worth reporting. A cleanup that cannot be proved is the exception — that one is a
135
+ // stuck key, and it must reach the caller as a refusal rather than be swallowed.
136
+ try {
137
+ await releaseKeys(page, pressed);
138
+ }
139
+ catch {
140
+ throw new DriveError(DriveErrorCode.RELEASE_FAILED, 'the wait failed and the keys it pressed could not be released, so they may still be held');
141
+ }
142
+ throw error;
143
+ }
144
+ // NOT best-effort: on the ordinary path a failed release is a real failure the caller must hear
145
+ // about, and swallowing it would report a press whose key is still down.
146
+ await releaseKeys(page, pressed);
147
+ return now() - startedAt;
148
+ }
149
+ /**
150
+ * Release the given keys, in reverse press order, and report a failure as one that leaves the
151
+ * keyboard UNKNOWN rather than as an ordinary provider error.
152
+ *
153
+ * Every release is attempted even after one fails: a stuck `Control` is worse than a stuck letter,
154
+ * so a failure on one key must not skip the rest. The first failure is what is thrown, since it is
155
+ * the one that happened first, and it is wrapped in `DriveError` so the caller can tell "this
156
+ * gesture failed" from "this gesture may have left a key down" — the difference between a safe
157
+ * synthetic replay and an unsafe one.
158
+ */
159
+ async function releaseKeys(page, pressed) {
160
+ let failure;
161
+ for (const k of [...pressed].reverse()) {
162
+ try {
163
+ await page.keyboard.up(k);
164
+ }
165
+ catch (error) {
166
+ failure ??= error;
167
+ }
168
+ }
169
+ if (failure !== undefined) {
170
+ throw new DriveError(DriveErrorCode.RELEASE_FAILED, `the key was pressed but could not be released, so it may still be held: ${describeFailure(failure)}`);
171
+ }
172
+ }
173
+ /**
174
+ * A thrown value as a line a reader can act on.
175
+ *
176
+ * Not `String(error)`: a non-Error thrown value (a rejected plain object, an SDK that throws a
177
+ * record) stringifies to `[object Object]`, which names nothing and hides the one fact the message
178
+ * exists to carry. Anything that is neither an Error nor a string says so rather than pretending.
179
+ */
180
+ function describeFailure(error) {
181
+ if (error instanceof Error)
182
+ return error.message;
183
+ return 'string' === typeof error ? error : 'an unknown error';
184
+ }
185
+ /**
186
+ * The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
187
+ *
188
+ * `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
189
+ * That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
190
+ * — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
191
+ * presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
192
+ * `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
193
+ * routed to the synthetic path by the caller, which reports the reason rather than pretending a
194
+ * different gesture happened.
195
+ *
196
+ * Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
197
+ * the target. Two can apply at once, and the earlier one is the more unusual request — the reason
198
+ * should name that, not the generic "you passed a ref".
199
+ */
200
+ export function unspellablePressReason(ref, args) {
201
+ if (0 < pressKeysFromArgs(args).length) {
202
+ return InputModeReason.SYNTHETIC_MULTI_KEY_PRESS_PREFERRED;
203
+ }
204
+ if (explicitCodeFromArgs(args) !== undefined) {
205
+ return InputModeReason.SYNTHETIC_KEY_CODE_PRESS_PREFERRED;
206
+ }
207
+ if (0 < ref.length)
208
+ return InputModeReason.SYNTHETIC_ELEMENT_PRESS_PREFERRED;
209
+ return undefined;
210
+ }
50
211
  /**
51
212
  * Shared gesture executor: drive a native gesture on an already-resolved Page. Used by both the
52
213
  * CDP-attached and the launched (drive) providers so the pointer logic lives in one place.
53
214
  */
54
- export async function performGesture(page, action, box, args, sleep) {
215
+ export async function performGesture(page, action, box, args, sleep,
216
+ /** Injected so a test can assert the achieved hold deterministically; defaults to the real clock. */
217
+ now = Date.now) {
218
+ // Ahead of the box, not after it: a press addresses focus rather than a point, and the
219
+ // placeholder box its caller passes would otherwise be turned into a (0,0) that reads like a real
220
+ // location. The result reports NO `center` for the same reason.
221
+ if (action === ActionType.PRESS) {
222
+ const heldMs = await pressViaKeyboard(page, args, sleep, now);
223
+ return {
224
+ performed: true,
225
+ inputMode: InputMode.REAL,
226
+ // Omitted rather than 0 when there was no hold — see `RealInputResult.heldMs`.
227
+ ...(heldMs > 0 ? { heldMs } : {}),
228
+ };
229
+ }
55
230
  const center = boxCenter(box);
56
231
  const { cx, cy } = center;
232
+ const real = (performed) => ({
233
+ performed,
234
+ center,
235
+ inputMode: InputMode.REAL,
236
+ });
57
237
  if (action === ActionType.HOVER) {
58
238
  await page.mouse.move(cx, cy);
59
239
  await page.mouse.move(cx + 1, cy);
60
240
  await page.mouse.move(cx, cy);
61
241
  await sleep(REAL_INPUT_SETTLE_MS);
62
- return { performed: true, center };
242
+ return real(true);
63
243
  }
64
244
  if (action === ActionType.CLICK) {
65
245
  await page.mouse.move(cx, cy);
66
246
  await page.mouse.click(cx, cy);
67
- return { performed: true, center };
247
+ return real(true);
68
248
  }
69
249
  if (action === ActionType.DBLCLICK) {
70
250
  await page.mouse.move(cx, cy);
71
251
  await page.mouse.dblclick(cx, cy);
72
- return { performed: true, center };
252
+ return real(true);
73
253
  }
74
254
  if (action === ActionType.DRAG) {
75
255
  if (args.toBox === undefined)
76
- return { performed: false, center };
256
+ return real(false);
77
257
  const dst = boxCenter(args.toBox);
78
258
  const steps = args.steps ?? DEFAULT_DRAG_STEPS;
79
259
  await page.mouse.move(cx, cy);
@@ -84,14 +264,14 @@ export async function performGesture(page, action, box, args, sleep) {
84
264
  await page.mouse.move(px, py, { steps: 1 });
85
265
  }
86
266
  await page.mouse.up();
87
- return { performed: true, center };
267
+ return real(true);
88
268
  }
89
269
  if (action === ActionType.FILL || action === ActionType.TYPE) {
90
270
  await page.mouse.click(cx, cy);
91
271
  await page.keyboard.type(args.value ?? args.text ?? '');
92
- return { performed: true, center };
272
+ return real(true);
93
273
  }
94
- return { performed: false, center };
274
+ return real(false);
95
275
  }
96
276
  /**
97
277
  * Reticle paints its own dev overlay (presenter HUD + border glow) into the page. That chrome is
@@ -100,18 +280,19 @@ export async function performGesture(page, action, box, args, sleep) {
100
280
  * (Playwright applies this stylesheet only for the shot, then reverts) so visual baselines reflect
101
281
  * the app, not Reticle. Disabling animations settles any remaining transitions for determinism.
102
282
  */
103
- const HIDE_RETICLE_CHROME_CSS = '[data-reticle-overlay]{display:none !important}';
104
283
  const SCREENSHOT_DETERMINISM = { style: HIDE_RETICLE_CHROME_CSS, animations: 'disabled' };
105
284
  /**
106
285
  * Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
107
286
  * screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
108
287
  */
109
288
  export async function capturePage(page, opts) {
110
- const buf = await page.screenshot(opts.clip !== undefined
111
- ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip }
112
- : true === opts.fullPage
113
- ? { ...SCREENSHOT_DETERMINISM, fullPage: true }
114
- : { ...SCREENSHOT_DETERMINISM });
289
+ const buf = await page.screenshot(opts.jpegQuality !== undefined
290
+ ? { type: 'jpeg', quality: opts.jpegQuality }
291
+ : opts.clip !== undefined
292
+ ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip }
293
+ : true === opts.fullPage
294
+ ? { ...SCREENSHOT_DETERMINISM, fullPage: true }
295
+ : { ...SCREENSHOT_DETERMINISM });
115
296
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
116
297
  }
117
298
  const nodeSleep = (ms) => new Promise((resolve) => {
@@ -138,6 +319,7 @@ const cdpConnect = async (url) => {
138
319
  export class CdpRealInputProvider {
139
320
  #cdpUrl;
140
321
  #sleep;
322
+ #now;
141
323
  #connect;
142
324
  #onNetworkDetail;
143
325
  /** Pages already listening. #pageFor resolves on EVERY call, so without this each action would add
@@ -149,6 +331,7 @@ export class CdpRealInputProvider {
149
331
  constructor(options) {
150
332
  this.#cdpUrl = options.cdpUrl;
151
333
  this.#sleep = options.sleep ?? nodeSleep;
334
+ this.#now = options.now ?? Date.now;
152
335
  this.#connect = options.connect ?? cdpConnect;
153
336
  this.#onNetworkDetail = options.onNetworkDetail;
154
337
  }
@@ -205,8 +388,8 @@ export class CdpRealInputProvider {
205
388
  async perform(sessionUrl, action, box, args) {
206
389
  const page = await this.#pageFor(sessionUrl);
207
390
  if (page === undefined)
208
- return { performed: false, center: boxCenter(box) };
209
- return performGesture(page, action, box, args, this.#sleep);
391
+ return undriven(action, box);
392
+ return performGesture(page, action, box, args, this.#sleep, this.#now);
210
393
  }
211
394
  /** PNG of the correlated page, or undefined if none matches. */
212
395
  async screenshot(sessionUrl, opts) {
@@ -309,6 +492,7 @@ export class LaunchedRealInputProvider {
309
492
  #driveUrl;
310
493
  #headless;
311
494
  #sleep;
495
+ #now;
312
496
  #launch;
313
497
  #injectConnect;
314
498
  #storageState;
@@ -321,6 +505,7 @@ export class LaunchedRealInputProvider {
321
505
  this.#driveUrl = options.driveUrl;
322
506
  this.#headless = options.headless;
323
507
  this.#sleep = options.sleep ?? nodeSleep;
508
+ this.#now = options.now ?? Date.now;
324
509
  this.#launch = options.launch ?? launchedChromium;
325
510
  this.#injectConnect = options.injectConnect;
326
511
  this.#storageState = options.storageState;
@@ -438,8 +623,8 @@ export class LaunchedRealInputProvider {
438
623
  perform(_sessionUrl, action, box, args) {
439
624
  const page = this.#livePage();
440
625
  if (page === undefined)
441
- return Promise.resolve({ performed: false, center: boxCenter(box) });
442
- return performGesture(page, action, box, args, this.#sleep);
626
+ return Promise.resolve(undriven(action, box));
627
+ return performGesture(page, action, box, args, this.#sleep, this.#now);
443
628
  }
444
629
  /** PNG of the owned page, or undefined before navigate / after dispose. */
445
630
  screenshot(_sessionUrl, opts) {
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { CDP_NO_PROVIDER_REASON, CDP_NO_PROVIDER_RECOMMENDATION } from '@reticlehq/core';
2
+ import { CDP_NO_PROVIDER_REASON, VIEWPORT_NO_PROVIDER_RECOMMENDATION } from '@reticlehq/core';
3
3
  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';
@@ -16,7 +16,7 @@ function viewportProvider(deps) {
16
16
  export const VIEWPORT_TOOLS = [
17
17
  {
18
18
  name: ReticleTool.VIEWPORT,
19
- description: 'Pin the DRIVEN page (needs `reticle drive`) to a fixed viewport size so a screenshot baseline is ' +
19
+ description: 'Pin the DRIVEN or LEASED page (needs `reticle drive`, or `reticle_lease` acquire when there is no CDP provider) to a fixed viewport size so a screenshot baseline is ' +
20
20
  'reproducible across machines — the missing piece of CI-stable visual regression, alongside ' +
21
21
  'reticle_visual_diff `masks` and a frozen clock (reticle_clock). Set it once before reticle_screenshot / ' +
22
22
  'reticle_visual_diff. Returns { applied, width, height } or the no-provider recommendation.',
@@ -60,7 +60,7 @@ export const VIEWPORT_TOOLS = [
60
60
  height: 0,
61
61
  ok: false,
62
62
  reason: CDP_NO_PROVIDER_REASON,
63
- recommendation: CDP_NO_PROVIDER_RECOMMENDATION,
63
+ recommendation: VIEWPORT_NO_PROVIDER_RECOMMENDATION,
64
64
  };
65
65
  },
66
66
  },