@reticlehq/server 2.6.0 → 2.8.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 (276) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +251 -33
  3. package/dist/bridge/bridge.d.ts +6 -1
  4. package/dist/bridge/bridge.js +27 -4
  5. package/dist/capsule/causal-summary.d.ts +20 -0
  6. package/dist/capsule/causal-summary.js +31 -6
  7. package/dist/capsule/divergence.js +7 -1
  8. package/dist/cli/chromium-hint.d.ts +68 -0
  9. package/dist/cli/chromium-hint.js +145 -0
  10. package/dist/cli/cli-doctor.js +31 -36
  11. package/dist/cli/cli-kill.d.ts +58 -0
  12. package/dist/cli/cli-kill.js +162 -0
  13. package/dist/cli/cli-launch.d.ts +18 -0
  14. package/dist/cli/cli-launch.js +32 -1
  15. package/dist/cli/cli-parse.d.ts +16 -1
  16. package/dist/cli/cli-parse.js +32 -9
  17. package/dist/cli/cli-port.d.ts +8 -0
  18. package/dist/cli/cli-port.js +36 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/doctor-daemon-line.js +10 -1
  21. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  22. package/dist/cli/doctor-sessions-line.js +66 -0
  23. package/dist/cli/port-holder.d.ts +10 -0
  24. package/dist/cli/port-holder.js +26 -2
  25. package/dist/cli.js +139 -23
  26. package/dist/crawl/crawl-tools.js +12 -2
  27. package/dist/crawl/crawl.d.ts +13 -0
  28. package/dist/crawl/crawl.js +50 -2
  29. package/dist/daemon/daemon-resilience.js +2 -2
  30. package/dist/daemon/loopback-alias.d.ts +32 -0
  31. package/dist/daemon/loopback-alias.js +80 -0
  32. package/dist/events/contradictions.d.ts +14 -0
  33. package/dist/events/contradictions.js +26 -17
  34. package/dist/events/predicate-eval.d.ts +26 -3
  35. package/dist/events/predicate-eval.js +284 -95
  36. package/dist/events/predicate-parse.d.ts +1 -1
  37. package/dist/events/predicate-parse.js +18 -4
  38. package/dist/events/predicate.js +170 -18
  39. package/dist/events/ring-buffer.d.ts +8 -0
  40. package/dist/events/ring-buffer.js +34 -1
  41. package/dist/flows/flow-classify.js +1 -1
  42. package/dist/flows/flow-tools.js +12 -4
  43. package/dist/flows/heal-run.js +1 -1
  44. package/dist/honesty/blind-spots.js +6 -2
  45. package/dist/honesty/honesty.d.ts +11 -0
  46. package/dist/honesty/honesty.js +9 -1
  47. package/dist/honesty/unsettled.d.ts +40 -0
  48. package/dist/honesty/unsettled.js +78 -0
  49. package/dist/honesty/verified.d.ts +7 -0
  50. package/dist/honesty/verified.js +27 -4
  51. package/dist/http-server.js +11 -1
  52. package/dist/index.js +29 -2
  53. package/dist/init/agent-rules.d.ts +16 -0
  54. package/dist/init/agent-rules.js +96 -17
  55. package/dist/init/astro-patch.d.ts +2 -1
  56. package/dist/init/astro-patch.js +11 -5
  57. package/dist/init/capabilities.d.ts +22 -0
  58. package/dist/init/capabilities.js +76 -0
  59. package/dist/init/confirm.d.ts +96 -0
  60. package/dist/init/confirm.js +138 -0
  61. package/dist/init/cra.js +3 -2
  62. package/dist/init/detect.d.ts +8 -0
  63. package/dist/init/detect.js +15 -0
  64. package/dist/init/non-js-project.d.ts +27 -0
  65. package/dist/init/non-js-project.js +84 -0
  66. package/dist/init/plan-framework.d.ts +9 -0
  67. package/dist/init/plan-framework.js +72 -16
  68. package/dist/init/plan.d.ts +40 -4
  69. package/dist/init/plan.js +183 -43
  70. package/dist/init/run.d.ts +24 -2
  71. package/dist/init/run.js +181 -46
  72. package/dist/init/slash-command.d.ts +1 -1
  73. package/dist/init/slash-command.js +46 -6
  74. package/dist/init/snippets.d.ts +64 -5
  75. package/dist/init/snippets.js +220 -31
  76. package/dist/init/vite-config.js +29 -3
  77. package/dist/input/network-mock-tools.d.ts +19 -0
  78. package/dist/input/network-mock-tools.js +29 -4
  79. package/dist/license/license.js +1 -1
  80. package/dist/mcp/mcp-proxy.d.ts +69 -3
  81. package/dist/mcp/mcp-proxy.js +215 -23
  82. package/dist/mcp/mcp.js +58 -7
  83. package/dist/mcp/proxy-lifecycle.d.ts +20 -1
  84. package/dist/mcp/proxy-lifecycle.js +22 -1
  85. package/dist/pool/browser-pool.d.ts +22 -2
  86. package/dist/pool/browser-pool.js +44 -4
  87. package/dist/pool/playwright-launcher.js +10 -2
  88. package/dist/project/learned-routes.d.ts +24 -0
  89. package/dist/project/learned-routes.js +63 -0
  90. package/dist/project/project-store.d.ts +6 -0
  91. package/dist/project/project-store.js +25 -1
  92. package/dist/session/live-control-tools.js +39 -2
  93. package/dist/session/live-control.d.ts +7 -9
  94. package/dist/session/live-control.js +34 -1
  95. package/dist/session/no-session-diagnosis.d.ts +23 -4
  96. package/dist/session/no-session-diagnosis.js +128 -44
  97. package/dist/session/no-session-watch.js +12 -0
  98. package/dist/session/ref-provenance.d.ts +54 -0
  99. package/dist/session/ref-provenance.js +61 -0
  100. package/dist/session/review-store.d.ts +19 -0
  101. package/dist/session/review-store.js +34 -5
  102. package/dist/session/session-lease.js +1 -1
  103. package/dist/session/session-manager.d.ts +9 -0
  104. package/dist/session/session-manager.js +11 -0
  105. package/dist/session/session.d.ts +8 -0
  106. package/dist/session/session.js +12 -0
  107. package/dist/status-payload.d.ts +13 -1
  108. package/dist/status-payload.js +6 -1
  109. package/dist/telemetry/app-instrumented.d.ts +37 -0
  110. package/dist/telemetry/app-instrumented.js +54 -0
  111. package/dist/telemetry/automation-hint.d.ts +17 -0
  112. package/dist/telemetry/automation-hint.js +71 -0
  113. package/dist/telemetry/bug-found.d.ts +0 -20
  114. package/dist/telemetry/bug-found.js +38 -21
  115. package/dist/telemetry/cli-telemetry.js +7 -1
  116. package/dist/telemetry/daemon-telemetry.d.ts +4 -4
  117. package/dist/telemetry/daemon-telemetry.js +8 -4
  118. package/dist/telemetry/feedback.js +1 -1
  119. package/dist/telemetry/init-telemetry.js +9 -1
  120. package/dist/telemetry/install-source.d.ts +36 -0
  121. package/dist/telemetry/install-source.js +40 -0
  122. package/dist/telemetry/run-telemetry.js +4 -1
  123. package/dist/telemetry/session-metrics.d.ts +3 -3
  124. package/dist/telemetry/session-metrics.js +9 -4
  125. package/dist/telemetry/telemetry.d.ts +7 -1
  126. package/dist/telemetry/telemetry.js +17 -2
  127. package/dist/telemetry/tool-refused.d.ts +19 -0
  128. package/dist/telemetry/tool-refused.js +55 -0
  129. package/dist/telemetry/verification-of.js +72 -8
  130. package/dist/temp-dir.d.ts +27 -0
  131. package/dist/temp-dir.js +46 -0
  132. package/dist/tools/act-tools.js +77 -11
  133. package/dist/tools/assert-grade.d.ts +1 -1
  134. package/dist/tools/assert-grade.js +27 -5
  135. package/dist/tools/assert-verdict.js +25 -1
  136. package/dist/tools/contract-tools.js +4 -0
  137. package/dist/tools/dynamic-tools.js +1 -1
  138. package/dist/tools/error-recovery.d.ts +10 -0
  139. package/dist/tools/error-recovery.js +70 -5
  140. package/dist/tools/feedback-invite.js +1 -1
  141. package/dist/tools/invoke-tool.js +149 -32
  142. package/dist/tools/lease-tools.d.ts +29 -1
  143. package/dist/tools/lease-tools.js +58 -5
  144. package/dist/tools/navigate-result.js +1 -1
  145. package/dist/tools/observe-tools.js +1 -1
  146. package/dist/tools/observed-nothing.js +1 -1
  147. package/dist/tools/react-grace.d.ts +48 -0
  148. package/dist/tools/react-grace.js +87 -0
  149. package/dist/tools/read-tools.js +20 -4
  150. package/dist/tools/settle-in-flight.d.ts +79 -0
  151. package/dist/tools/settle-in-flight.js +106 -0
  152. package/dist/tools/snapshot-delta.d.ts +7 -0
  153. package/dist/tools/snapshot-delta.js +130 -6
  154. package/dist/tools/tool-kit.d.ts +52 -5
  155. package/dist/tools/tool-kit.js +55 -24
  156. package/dist/tools/tool-surface.js +28 -9
  157. package/dist/tools/tools.js +38 -4
  158. package/dist/visual/capture-cleanup.d.ts +16 -0
  159. package/dist/visual/capture-cleanup.js +39 -0
  160. package/dist/visual/visual-tools.d.ts +18 -0
  161. package/dist/visual/visual-tools.js +24 -5
  162. package/docs/README.md +56 -11
  163. package/docs/actions.mdx +87 -0
  164. package/docs/agent-cheatsheet.md +102 -26
  165. package/docs/architecture.md +46 -35
  166. package/docs/benchmarks.md +76 -72
  167. package/docs/best-practices.mdx +211 -0
  168. package/docs/capabilities.mdx +387 -0
  169. package/docs/cli/affected.mdx +51 -0
  170. package/docs/cli/capsules.mdx +69 -0
  171. package/docs/cli/cloud.mdx +127 -0
  172. package/docs/cli/doctor.mdx +76 -0
  173. package/docs/cli/drive.mdx +55 -0
  174. package/docs/cli/feedback.mdx +67 -0
  175. package/docs/cli/gate.mdx +64 -0
  176. package/docs/cli/help.mdx +92 -0
  177. package/docs/cli/hunt.mdx +57 -0
  178. package/docs/cli/identify.mdx +62 -0
  179. package/docs/cli/init.mdx +94 -0
  180. package/docs/cli/kill.mdx +91 -0
  181. package/docs/cli/license.mdx +48 -0
  182. package/docs/cli/mcp.mdx +61 -0
  183. package/docs/cli/open.mdx +68 -0
  184. package/docs/cli/restart.mdx +60 -0
  185. package/docs/cli/rollback.mdx +35 -0
  186. package/docs/cli/serve.mdx +62 -0
  187. package/docs/cli/status.mdx +64 -0
  188. package/docs/cli/stop.mdx +56 -0
  189. package/docs/cli/telemetry.mdx +57 -0
  190. package/docs/cli/update.mdx +52 -0
  191. package/docs/cli/verify.mdx +57 -0
  192. package/docs/cli/version.mdx +39 -0
  193. package/docs/cli/watch.mdx +55 -0
  194. package/docs/cli.mdx +219 -0
  195. package/docs/debugging.md +13 -9
  196. package/docs/deploy-checks.md +15 -11
  197. package/docs/desktop-apps.md +49 -40
  198. package/docs/desktop.mdx +188 -0
  199. package/docs/docs.json +363 -0
  200. package/docs/enterprise.md +20 -16
  201. package/docs/faq.mdx +88 -0
  202. package/docs/fixtures.md +11 -7
  203. package/docs/flows.md +26 -22
  204. package/docs/for-agents.md +71 -0
  205. package/docs/frameworks.mdx +199 -0
  206. package/docs/gate-plan.md +45 -41
  207. package/docs/gates.md +23 -19
  208. package/docs/getting-started.md +76 -65
  209. package/docs/human-control.md +22 -18
  210. package/docs/index.mdx +101 -0
  211. package/docs/install-agentic.mdx +215 -0
  212. package/docs/install-manual.mdx +194 -0
  213. package/docs/instrumentation.mdx +232 -0
  214. package/docs/integration-patterns.md +38 -34
  215. package/docs/local-registry.md +16 -12
  216. package/docs/multi-agent-testing.md +16 -12
  217. package/docs/packages/babel-plugin.mdx +70 -0
  218. package/docs/packages/browser.mdx +111 -0
  219. package/docs/packages/core.mdx +106 -0
  220. package/docs/packages/electron.mdx +87 -0
  221. package/docs/packages/eslint-plugin.mdx +84 -0
  222. package/docs/packages/next.mdx +91 -0
  223. package/docs/packages/react.mdx +91 -0
  224. package/docs/packages/server.mdx +98 -0
  225. package/docs/packages/tauri.mdx +63 -0
  226. package/docs/packages/test.mdx +108 -0
  227. package/docs/packages/vite-plugin.mdx +96 -0
  228. package/docs/packages.mdx +217 -0
  229. package/docs/platform-integration.md +33 -26
  230. package/docs/predicates.mdx +179 -0
  231. package/docs/quickstart.mdx +211 -0
  232. package/docs/recipes.mdx +242 -0
  233. package/docs/skill-file.mdx +105 -0
  234. package/docs/state-management.mdx +197 -0
  235. package/docs/style.css +14 -0
  236. package/docs/system-map.md +47 -32
  237. package/docs/telemetry-contract.md +101 -46
  238. package/docs/telemetry-events.mdx +430 -0
  239. package/docs/telemetry.md +63 -50
  240. package/docs/testing.md +10 -6
  241. package/docs/token-efficiency.md +30 -26
  242. package/docs/tools-act-and-wait.mdx +123 -0
  243. package/docs/tools-act-sequence.mdx +120 -0
  244. package/docs/tools-act.mdx +82 -0
  245. package/docs/tools-assert.mdx +70 -0
  246. package/docs/tools-console.mdx +59 -0
  247. package/docs/tools-inspect.mdx +85 -0
  248. package/docs/tools-navigate.mdx +63 -0
  249. package/docs/tools-network.mdx +86 -0
  250. package/docs/tools-observe.mdx +89 -0
  251. package/docs/tools-overview.mdx +157 -0
  252. package/docs/tools-query.mdx +82 -0
  253. package/docs/tools-session-and-feedback.mdx +91 -0
  254. package/docs/tools-sessions.mdx +65 -0
  255. package/docs/tools-snapshot.mdx +75 -0
  256. package/docs/tools-state.mdx +81 -0
  257. package/docs/tools-tools-and-run.mdx +125 -0
  258. package/docs/tools-wait-for.mdx +66 -0
  259. package/docs/troubleshooting.mdx +193 -0
  260. package/docs/usage.md +175 -153
  261. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  262. package/docs/vs-playwright-mcp.mdx +85 -0
  263. package/docs/vs-screenshots.mdx +96 -0
  264. package/docs/what-is-reticle.mdx +63 -0
  265. package/docs/why-reticle.mdx +107 -0
  266. package/package.json +2 -2
  267. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  268. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  269. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  270. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  271. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  272. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  273. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  274. package/docs/matrix/MATRIX.md +0 -26
  275. package/docs/matrix/README.md +0 -59
  276. package/docs/mint.json +0 -61
@@ -1,6 +1,11 @@
1
- # Desktop apps: Electron & Tauri
1
+ ---
2
+ title: Wiring up Electron and Tauri
3
+ sidebarTitle: Electron and Tauri setup
4
+ description: 'Install and wire Reticle into an Electron or Tauri app, drive the IPC boundary, and read the troubleshooting for each runtime.'
5
+ icon: plug
6
+ ---
2
7
 
3
- Reticle verifies desktop apps the same way it verifies web apps — from **inside** the app, over a localhost WebSocket. There is no browser to open and no screenshot to interpret.
8
+ Reticle verifies desktop apps the same way it verifies web apps: from **inside** the app, over a localhost WebSocket. There is no browser to open and no screenshot to interpret.
4
9
 
5
10
  - [How you actually test a desktop app](#how-you-actually-test-a-desktop-app)
6
11
  - [Electron](#electron)
@@ -12,13 +17,17 @@ Reticle verifies desktop apps the same way it verifies web apps — from **insid
12
17
 
13
18
  ## How you actually test a desktop app
14
19
 
15
- The usual question is _"it's a desktop app — what URL does the agent open?"_ None. The direction is reversed from what browser tooling trains you to expect:
20
+ The usual question is _"it's a desktop app, so what URL does the agent open?"_ None. The direction is reversed from what browser tooling trains you to expect:
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ A["coding agent"]
25
+ D["reticle daemon<br/>localhost:4400"]
26
+ W["your Electron / Tauri<br/>window + the SDK"]
16
27
 
17
- ```text
18
- ┌──────────────┐ MCP ┌───────────────────┐ WebSocket ┌───────────────────────┐
19
- │ coding agent │◀────────▶│ reticle daemon │◀──────────────▶│ your Electron/Tauri │
20
- │ │ stdio │ (localhost:4400) │ the app │ window + the SDK │
21
- └──────────────┘ └───────────────────┘ dials OUT └───────────────────────┘
28
+ A <-- "MCP over stdio" --> D
29
+ W -- "the app dials OUT<br/>over WebSocket" --> D
30
+ D -- "commands" --> W
22
31
  ```
23
32
 
24
33
  Your app **connects to the daemon**, not the other way round. So the workflow is:
@@ -27,13 +36,13 @@ Your app **connects to the daemon**, not the other way round. So the workflow is
27
36
  2. Start your app exactly as you always do: `npm run dev`, `electron .`, `cargo tauri dev`.
28
37
  3. That's it. `reticle status` now lists your window as a session, and the agent drives it.
29
38
 
30
- `reticle open` has nothing to open for a desktop app and will say so, and there is no `reticle drive` for desktop — those launch a browser, which is not what you are testing. Headless works on both runtimes; see below.
39
+ `reticle open` has nothing to open for a desktop app and will say so, and there is no `reticle drive` for desktop: those launch a browser, which is not what you are testing. Headless works on both runtimes; see below.
31
40
 
32
41
  ## What works, measured
33
42
 
34
43
  Every tool below was run against both demo apps against a live daemon.
35
44
 
36
- The rows in **bold** are the ones a committed battery re-proves on every change — `pnpm test:e2e:desktop`, which starts a real Electron main process and a **packaged** Tauri binary (`tauri://localhost`, not `tauri dev`) and drives them headless. The rest were measured by hand. That distinction matters: this table used to report a hand-run score with nothing in the repo that reproduced it, so it could go stale without anything failing.
45
+ The rows in **bold** are the ones a committed battery re-proves on every change with `pnpm test:e2e:desktop`, which starts a real Electron main process and a **packaged** Tauri binary (`tauri://localhost`, not `tauri dev`) and drives them headless. The rest were measured by hand. That distinction matters: this table used to report a hand-run score with nothing in the repo that reproduced it, so it could go stale without anything failing.
37
46
 
38
47
  | Capability | Electron | Tauri | Note |
39
48
  | --- | --- | --- | --- |
@@ -42,13 +51,13 @@ The rows in **bold** are the ones a committed battery re-proves on every change
42
51
  | act (click/fill/type/select) | ✅ | ✅ | |
43
52
  | **act_and_wait, wait_for, assert** | ✅ | ✅ | signal / state / route / net / console predicates |
44
53
  | console errors | ✅ | ✅ | catches what the UI never shows |
45
- | network — HTTP | ✅ | ✅ | on `file://` a relative URL has no origin; use an absolute one |
46
- | **network — IPC** | ✅ | ✅ | `ipc://<channel>`, incl. failures. Electron: `invoke` and `sendSync` carry a verdict; a one-way `send` is recorded as `oneWay: true` with NO status, because the renderer never learns the outcome. Tauri: both the `ipc://` (macOS/Linux) and `http://ipc.localhost` (Windows) transports |
47
- | route | ✅ | ✅ | use a **hash** router — see below |
54
+ | network (HTTP) | ✅ | ✅ | on `file://` a relative URL has no origin; use an absolute one |
55
+ | **network (IPC)** | ✅ | ✅ | `ipc://<channel>`, incl. failures. Electron: `invoke` and `sendSync` carry a verdict; a one-way `send` is recorded as `oneWay: true` with NO status, because the renderer never learns the outcome. Tauri: both the `ipc://` (macOS/Linux) and `http://ipc.localhost` (Windows) transports |
56
+ | route | ✅ | ✅ | use a **hash** router; see below |
48
57
  | storage, animations, observe, explore | ✅ | ✅ | |
49
58
  | baseline (semantic), record → replay, crawl | ✅ | ✅ | `crawl` found real anomalies in both |
50
59
  | navigate (reload) | ✅ | ✅ | |
51
- | **screenshot / visual_diff** | ✅ | ✅ | one line in Electron's main process; one Rust command in Tauri — see below. Reticle's own presenter panel is hidden for the shot, so a baseline records your app and not the instrument |
60
+ | **screenshot / visual_diff** | ✅ | ✅ | one line in Electron's main process; one Rust command in Tauri (both below). Reticle's own presenter panel is hidden for the shot, so a baseline records your app and not the instrument |
52
61
  | **drivable while window occluded** | ✅ | ✅ | including minimized, app-hidden, and behind a fullscreen app on another Space |
53
62
  | **headless** | ✅ | ✅ | Electron never shows the window; Tauri shows, loads, then hides |
54
63
  | **a missing preload is DECLARED, not silent** | ✅ | n/a | without `@reticlehq/electron/preload` every IPC call is invisible, so verdicts carry `coverage: partial` naming the missing line instead of reading clean |
@@ -64,11 +73,11 @@ const win = new BrowserWindow({ ... });
64
73
  installReticleCapture(win);
65
74
  ```
66
75
 
67
- That is all — no CDP flag, no extra packages, works on a packaged `file://` renderer. `reticle_screenshot` and `reticle_visual_diff` then work on your app.
76
+ That is all: no CDP flag, no extra packages, works on a packaged `file://` renderer. `reticle_screenshot` and `reticle_visual_diff` then work on your app.
68
77
 
69
78
  Alternatively, since an Electron renderer _is_ Chromium, `--remote-debugging-port=9222` + `RETICLE_CDP_URL=http://127.0.0.1:9222` also works and additionally enables `fullPage` (the main-process route captures the window as composited, so it cannot scroll-stitch).
70
79
 
71
- **Why the main process, and not a screen capture.** `webContents.capturePage()` reads the window's own backing store. Capturing a screen _region_ instead was tried and deliberately rejected: it photographs whatever is on top, so an app window behind your editor yields a picture of the editor — saved as a visual baseline that a later diff would trust. A screenshot tool that can silently return another window's pixels manufactures exactly the false green Reticle exists to eliminate. One caveat remains: a fully occluded or minimized window is only partially composited, so parts of the capture may come back blank. Bring the window forward for a complete image — but it is never the wrong window.
80
+ **Why the main process, and not a screen capture.** `webContents.capturePage()` reads the window's own backing store. Capturing a screen _region_ instead was tried and deliberately rejected: it photographs whatever is on top, so an app window behind your editor yields a picture of the editor, saved as a visual baseline that a later diff would trust. A screenshot tool that can silently return another window's pixels manufactures exactly the false green Reticle exists to eliminate. One caveat remains: a fully occluded or minimized window is only partially composited, so parts of the capture may come back blank. Bring the window forward for a complete image. It is never the wrong window.
72
81
 
73
82
  **Tauri: one Rust command.**
74
83
 
@@ -80,19 +89,19 @@ tauri::Builder::default()
80
89
 
81
90
  Nothing on the JavaScript side: the SDK invokes the command through Tauri's own internals, because Tauri has no preload stage where a shim could be installed. `reticle_screenshot` and `reticle_visual_diff` then work on your app, including headless.
82
91
 
83
- `reticle_capture` renders the webview rather than reading the screen — like Electron's `capturePage()` — so it needs no screen-recording permission, cannot return another window's pixels, and is correct with nothing on screen at all. Each platform uses its own webview API:
92
+ `reticle_capture` renders the webview rather than reading the screen, like Electron's `capturePage()`, so it needs no screen-recording permission, cannot return another window's pixels, and is correct with nothing on screen at all. Each platform uses its own webview API:
84
93
 
85
94
  | Platform | API | Status |
86
95
  | --- | --- | --- |
87
96
  | macOS | `WKWebView.takeSnapshot` | Verified against a running app |
88
97
  | Linux, BSD | WebKitGTK `webkit_web_view_get_snapshot` | Snapshot + PNG encoding verified under `xvfb`; not yet driven through a full Tauri app |
89
- | Windows | WebView2 `CapturePreview` | **Untested — compiles, never executed** |
98
+ | Windows | WebView2 `CapturePreview` | **Untested: compiles, never executed** |
90
99
 
91
- The Windows path is written and type-checked against the real `webview2-com` API (which caught two genuine type errors), but nobody has run it on Windows. CI now re-checks it against that target on every PR (`cargo check --target x86_64-pc-windows-msvc`), so "compiles" is a gate rather than a claim — it had been asserted for months by a workflow comment while no such job existed. Executed is still a different word from compiled: it is shipped rather than withheld so it can be tried, and labelled rather than listed flatly so that trying it is a choice. If it works for you, say so and this row changes; treat a green from it as unconfirmed until then.
100
+ The Windows path is written and type-checked against the real `webview2-com` API (which caught two genuine type errors), but nobody has run it on Windows. CI now re-checks it against that target on every PR (`cargo check --target x86_64-pc-windows-msvc`), so "compiles" is a gate rather than a claim; it had been asserted for months by a workflow comment while no such job existed. Executed is still a different word from compiled: it is shipped rather than withheld so it can be tried, and labelled rather than listed flatly so that trying it is a choice. If it works for you, say so and this row changes; treat a green from it as unconfirmed until then.
92
101
 
93
102
  All three capture the visible viewport by default, so a baseline taken on a developer's Mac is comparable against the same app in Linux CI. On a platform with no webview API to call, capture reports no-provider rather than returning a plausible wrong image.
94
103
 
95
- **`{ fullPage: true }` works on Tauri/Linux only.** WebKitGTK can render the whole document offscreen; `takeSnapshot` (macOS) and `CapturePreview` (Windows) only give what is composited, and Electron's `capturePage()` is the same. Asked for a full page they cannot produce, all of them return `{ ok:false, reason:'full-page-unsupported' }` rather than quietly handing back the viewport — a baseline that omits everything below the fold, while every later diff of it reports green about a region that was never captured. No baseline is written on a refusal.
104
+ **`{ fullPage: true }` works on Tauri/Linux only.** WebKitGTK can render the whole document offscreen; `takeSnapshot` (macOS) and `CapturePreview` (Windows) only give what is composited, and Electron's `capturePage()` is the same. Asked for a full page they cannot produce, all of them return `{ ok:false, reason:'full-page-unsupported' }` rather than quietly handing back the viewport, which would be a baseline that omits everything below the fold, while every later diff of it reports green about a region that was never captured. No baseline is written on a refusal.
96
105
 
97
106
  An app that already has its own capture can expose `window.__reticleIpc.capture()` returning a PNG path instead; the SDK prefers it over the built-in command.
98
107
 
@@ -108,9 +117,9 @@ The lesson generalises past this document: four experiments agreeing does not ma
108
117
 
109
118
  ### Headless
110
119
 
111
- **Electron: yes.** `show: false` plus `backgroundThrottling: false` in `webPreferences`. The second one is load-bearing — Chromium runs an unshown window's timers in slow motion, which turns every settle wait into a flake. Screenshots still work, because `capturePage` reads the backing store rather than the screen. Verified with a full tool drive against a window that was never shown.
120
+ **Electron: yes.** `show: false` plus `backgroundThrottling: false` in `webPreferences`. The second one is load-bearing: Chromium runs an unshown window's timers in slow motion, which turns every settle wait into a flake. Screenshots still work, because `capturePage` reads the backing store rather than the screen. Verified with a full tool drive against a window that was never shown.
112
121
 
113
- **Tauri: yes — show, load, then hide.**
122
+ **Tauri: yes. Show, load, then hide.**
114
123
 
115
124
  ```rust
116
125
  .on_page_load(reticle_tauri::on_page_load) // hides the window when RETICLE_HEADLESS=1
@@ -118,7 +127,7 @@ The lesson generalises past this document: four experiments agreeing does not ma
118
127
 
119
128
  Run with `RETICLE_HEADLESS=1 pnpm tauri dev`. Nothing ends up on screen, and screenshots keep working because the capture renders the webview rather than the screen.
120
129
 
121
- The ordering is the whole trick. Hiding the window during `setup` hides it before the webview has ever been presented, and a webview that has never been presented never loads its page — which is what made headless Tauri look impossible. Hiding it after its first page load leaves everything running. Verified with a full 43-tool drive plus a screenshot and a visual diff against a window that is not on screen.
130
+ The ordering is the whole trick. Hiding the window during `setup` hides it before the webview has ever been presented, and a webview that has never been presented never loads its page, which is what made headless Tauri look impossible. Hiding it after its first page load leaves everything running. Verified with a full 43-tool drive plus a screenshot and a visual diff against a window that is not on screen.
122
131
 
123
132
  `xvfb-run -a pnpm tauri dev` also works on Linux and needs no app-side change at all.
124
133
 
@@ -126,23 +135,23 @@ The ordering is the whole trick. Hiding the window during `setup` hides it befor
126
135
 
127
136
  Both attached to the same running Electron app, same task ("archive a todo, then verify it worked"):
128
137
 
129
- | tool | ~tokens | ms | verdict |
130
- | ---------------------- | ------- | ------- | ------------------------------------------ |
131
- | reticle (lean) | **350** | 1364 | caught the failure |
132
- | playwright-mcp | 1069 | **980** | blind to it — no network/IPC in its output |
133
- | playwright-mcp → Tauri | — | — | cannot attach (no CDP in WKWebView) |
138
+ | tool | ~tokens | ms | verdict |
139
+ | ---------------------- | ------- | ------- | ----------------------------------------- |
140
+ | reticle (lean) | **350** | 1364 | caught the failure |
141
+ | playwright-mcp | 1069 | **980** | blind to it: no network/IPC in its output |
142
+ | playwright-mcp → Tauri | n/a | n/a | cannot attach (no CDP in WKWebView) |
134
143
 
135
144
  Playwright MCP is faster. It is also structurally unable to see an IPC failure, because its channel is the accessibility tree. Full method, numbers and caveats: [`bench/desktop`](../bench/desktop).
136
145
 
137
146
  ### Routing: use a hash router
138
147
 
139
- A packaged renderer runs on `file://`, where `pushState('/settings')` rewrites the URL to `file:///settings` — a path that does not exist, so the next reload lands on a blank page and the app is gone. This is why HashRouter is the standard choice for packaged Electron/Tauri apps. Reticle's route observer handles both, and a `{ kind: 'route', contains: … }` assertion matches the fragment.
148
+ A packaged renderer runs on `file://`, where `pushState('/settings')` rewrites the URL to `file:///settings`, a path that does not exist, so the next reload lands on a blank page and the app is gone. This is why HashRouter is the standard choice for packaged Electron/Tauri apps. Reticle's route observer handles both, and a `{ kind: 'route', contains: … }` assertion matches the fragment.
140
149
 
141
150
  ## Electron
142
151
 
143
152
  Two steps. The first is the ordinary web setup; the second is the only desktop-specific part.
144
153
 
145
- **1. The renderer** — one line in `vite.config.ts`, exactly like a web app:
154
+ **1. The renderer.** One line in `vite.config.ts`, exactly like a web app:
146
155
 
147
156
  ```ts
148
157
  import { reticle } from '@reticlehq/vite-plugin';
@@ -157,7 +166,7 @@ export default defineConfig({
157
166
 
158
167
  Nothing to add in your app code. (You can still call `reticle.connect()` by hand and pass `inject: false` if you want control.)
159
168
 
160
- **2. The preload** — one line, before you expose anything:
169
+ **2. The preload.** One line, before you expose anything:
161
170
 
162
171
  ```bash
163
172
  npm i -D @reticlehq/electron
@@ -175,7 +184,7 @@ contextBridge.exposeInMainWorld('api', {
175
184
 
176
185
  That line is what makes your main-process calls visible. It has to live in the preload, and it is not a stylistic choice: `contextBridge.exposeInMainWorld` hands the renderer a **deeply frozen, non-configurable** object, so nothing running in the page can instrument `window.api`. The preload is the last point where `ipcRenderer.invoke` is still an ordinary, writable function. Patching there covers every channel you go on to expose, whatever you named it.
177
186
 
178
- **Preload sandboxing.** A sandboxed preload can't resolve `node_modules`, so the bare `require` above fails. Either bundle your preload (electron-vite and Electron Forge do this by default — the require is inlined at build time and sandboxing stays on), or set `sandbox: false` in `webPreferences`.
187
+ **Preload sandboxing.** A sandboxed preload can't resolve `node_modules`, so the bare `require` above fails. Either bundle your preload (electron-vite and Electron Forge do this by default; the require is inlined at build time and sandboxing stays on), or set `sandbox: false` in `webPreferences`.
179
188
 
180
189
  **Packaged renderers.** An app that loads its renderer with `loadFile` runs on `file://`, which is a production Vite build. Pass `allowInProduction: true` to `connect()` for that mode, or keep the SDK gated behind `import.meta.env.DEV` so it never enters the shipped binary at all.
181
190
 
@@ -192,7 +201,7 @@ import { reticle } from '@reticlehq/browser';
192
201
  if (import.meta.env.DEV) reticle.connect();
193
202
  ```
194
203
 
195
- Nothing else is needed for IPC. A Tauri `invoke` travels as a real `fetch` to Tauri's `ipc://` custom protocol, so Reticle already sees it; every `invoke('load_todos')` shows up as `ipc://load_todos`. Reticle also reads Tauri's `Tauri-Response` header, because the transport answers **HTTP 200 whether the Rust command returned `Ok` or `Err`** — without that translation a failed command would be recorded as a successful request.
204
+ Nothing else is needed for IPC. A Tauri `invoke` travels as a real `fetch` to Tauri's `ipc://` custom protocol, so Reticle already sees it; every `invoke('load_todos')` shows up as `ipc://load_todos`. Reticle also reads Tauri's `Tauri-Response` header, because the transport answers **HTTP 200 whether the Rust command returned `Ok` or `Err`**. Without that translation a failed command would be recorded as a successful request.
196
205
 
197
206
  The one required step is **CSP**. Tauri ships a restrictive default that blocks the bridge WebSocket before it opens, and the failure is silent from the app's side. In `src-tauri/tauri.conf.json`:
198
207
 
@@ -206,13 +215,13 @@ The one required step is **CSP**. Tauri ships a restrictive default that blocks
206
215
  }
207
216
  ```
208
217
 
209
- Keep `ipc: http://ipc.localhost` in `connect-src` — Tauri v2 needs it for `invoke` itself. Add your dev-server origin too if you use `devUrl`. This is a dev-only config; drop the `ws://` entries from your release config.
218
+ Keep `ipc: http://ipc.localhost` in `connect-src`; Tauri v2 needs it for `invoke` itself. Add your dev-server origin too if you use `devUrl`. This is a dev-only config; drop the `ws://` entries from your release config.
210
219
 
211
220
  Working example: [`apps/tauri-smoke`](../apps/tauri-smoke).
212
221
 
213
222
  ## What IPC looks like to an agent
214
223
 
215
- A desktop app reaches its backend over IPC, not HTTP. `fetch`/`XHR` patching cannot see that, so without the IPC observer every backend call in your app is a blind spot — `reticle_network` returns nothing, `act_and_wait` has no in-flight request to settle on, and `assert { net }` is vacuously true. That is a false green by construction.
224
+ A desktop app reaches its backend over IPC, not HTTP. `fetch`/`XHR` patching cannot see that, so without the IPC observer every backend call in your app is a blind spot: `reticle_network` returns nothing, `act_and_wait` has no in-flight request to settle on, and `assert { net }` is vacuously true. That is a false green by construction.
216
225
 
217
226
  Reticle records each IPC call as an ordinary request, so the tools you already use work unchanged:
218
227
 
@@ -226,7 +235,7 @@ Reticle records each IPC call as an ordinary request, so the tools you already u
226
235
  }
227
236
  ```
228
237
 
229
- IPC has no status code; `200`/`500` are synthetic, mapped from whether the call succeeded or failed, precisely so that `reticle_network { status: 500 }` and `assert { kind: "net", status: 500 }` keep working. On Tauri you will see `status: 500` next to `statusText: "OK"` — that is not a bug: the transport really did answer 200, and the 500 is the command's own verdict. `ok` is authoritative, and on Electron `error` carries the message your main process returned:
238
+ IPC has no status code; `200`/`500` are synthetic, mapped from whether the call succeeded or failed, precisely so that `reticle_network { status: 500 }` and `assert { kind: "net", status: 500 }` keep working. On Tauri you will see `status: 500` next to `statusText: "OK"`. That is not a bug: the transport really did answer 200, and the 500 is the command's own verdict. `ok` is authoritative, and on Electron `error` carries the message your main process returned:
230
239
 
231
240
  ```jsonc
232
241
  // reticle_assert { predicate: { kind: "net", urlContains: "ipc://todos:archive", status: 500 } }
@@ -241,16 +250,16 @@ IPC has no status code; `200`/`500` are synthetic, mapped from whether the call
241
250
  }
242
251
  ```
243
252
 
244
- Both example apps ship a planted false green — an Archive button that updates the UI optimistically and swallows the rejection. The screen says "archived", a screenshot agrees, a DOM assertion agrees. Only the IPC record disagrees. That is the case desktop support exists for.
253
+ Both example apps ship a planted false green: an Archive button that updates the UI optimistically and swallows the rejection. The screen says "archived", a screenshot agrees, a DOM assertion agrees. Only the IPC record disagrees. That is the case desktop support exists for.
245
254
 
246
255
  ## Troubleshooting
247
256
 
248
- **`reticle status` shows no session.** Check the app's console (Electron: devtools, or forward `console-message` to your terminal — a desktop renderer has no visible console otherwise). A refused connect always logs why.
257
+ **`reticle status` shows no session.** Check the app's console (Electron: devtools, or forward `console-message` to your terminal, since a desktop renderer has no visible console otherwise). A refused connect always logs why.
249
258
 
250
259
  **Tauri: nothing connects and the app console shows a CSP violation.** The `connect-src` above is missing or does not include your daemon's port.
251
260
 
252
- **Electron: `module not found: @reticlehq/electron/preload`.** The preload is sandboxed. Bundle it, or set `sandbox: false` — see [Electron](#electron).
261
+ **Electron: `module not found: @reticlehq/electron/preload`.** The preload is sandboxed. Bundle it, or set `sandbox: false`; see [Electron](#electron).
253
262
 
254
263
  **IPC calls do not appear, but the app works.** Electron: the shim's `require` must run _before_ your preload captures its own reference to `ipcRenderer`. Put it on the first line. Tauri: `invoke` imported from `@tauri-apps/api/core` is observed; a hand-rolled `postMessage` protocol is not, and neither is Tauri's `postMessage` transport fallback on platforms where the `ipc://` custom protocol is unavailable.
255
264
 
256
- **Why not just patch `invoke` / `window.api` directly?** Because neither can be. Tauri defines `__TAURI_INTERNALS__.invoke` as `writable: false, configurable: false`, and Electron's `contextBridge` object is deeply frozen and installed non-configurably. Both were verified, not assumed — which is why the two runtimes use the two different mechanisms above rather than one uniform monkey-patch.
265
+ **Why not just patch `invoke` / `window.api` directly?** Because neither can be. Tauri defines `__TAURI_INTERNALS__.invoke` as `writable: false, configurable: false`, and Electron's `contextBridge` object is deeply frozen and installed non-configurably. Both were verified, not assumed, which is why the two runtimes use the two different mechanisms above rather than one uniform monkey-patch.
@@ -0,0 +1,188 @@
1
+ ---
2
+ title: Desktop apps: no browser required
3
+ sidebarTitle: Desktop apps
4
+ description: Reticle verifies Electron and Tauri apps from inside, including the IPC boundary the renderer cannot see. No browser, no screenshot, no URL to open.
5
+ icon: display
6
+ ---
7
+
8
+ Ask most browser tooling to test a desktop app and the first question is "what URL do I open?".
9
+
10
+ There isn't one. That is the whole problem, and it is why desktop verification is mostly unserved.
11
+
12
+ Reticle inverts the direction. Your app dials the local daemon, the agent drives it through the same tools it uses for the web, and the verdicts read the same. No Chromium to launch, no screenshot to interpret.
13
+
14
+ ## The thing nobody else does
15
+
16
+ A desktop app's most interesting failures happen at the **IPC boundary**. The renderer asks the main process for something, and the main process quietly fails.
17
+
18
+ The renderer cannot see that. A screenshot certainly cannot. Reticle records it as a request:
19
+
20
+ ```json
21
+ { "method": "ipc", "url": "ipc://todos:archive", "status": 500, "ms": 82 }
22
+ ```
23
+
24
+ Then it contradicts a UI that moved on regardless:
25
+
26
+ ```json
27
+ {
28
+ "kind": "signal-contradicted",
29
+ "claim": "the app fired \"todos:loaded\"",
30
+ "counter": "1 request(s) in the same window failed",
31
+ "detail": "IPC ipc://todos:archive → 500"
32
+ }
33
+ ```
34
+
35
+ The app said it loaded. One IPC call in the same window returned 500. Reticle puts both in front of the agent instead of believing the happier one.
36
+
37
+ ![Left, an app window showing three synced todo items and nothing wrong. Right, Reticle's view of the same moment: the todos:loaded signal, an ipc://todos:archive call returning 500, and a signal-contradicted finding naming both](/images/desktop-ipc.png)
38
+
39
+ <Note>
40
+ The window on the left is an illustration. The JSON on the right is real output from the desktop
41
+ battery, which starts a real Electron main process and a **packaged** Tauri binary and drives them
42
+ headless on every change.
43
+ </Note>
44
+
45
+ ## What works, and what is proven
46
+
47
+ | Capability | Electron | Tauri |
48
+ | --------------------------------------------- | :------: | :---: |
49
+ | sessions, snapshot, query, inspect | yes | yes |
50
+ | capabilities, live store state | yes | yes |
51
+ | act: click, fill, type, select | yes | yes |
52
+ | **act_and_wait, wait_for, assert** | yes | yes |
53
+ | console errors | yes | yes |
54
+ | network over HTTP | yes | yes |
55
+ | **network over IPC** | yes | yes |
56
+ | route (use a hash router) | yes | yes |
57
+ | storage, animations, observe, explore | yes | yes |
58
+ | baselines, record and replay, crawl | yes | yes |
59
+ | **screenshot and visual diff** | yes | yes |
60
+ | **drivable while occluded or minimized** | yes | yes |
61
+ | **headless** | yes | yes |
62
+ | **a missing preload is declared, not silent** | yes | n/a |
63
+ | network mocking, viewport pinning | no | no |
64
+
65
+ Bold rows are re-proven by `pnpm test:e2e:desktop` on every change. The rest were measured by hand.
66
+
67
+ That distinction is deliberate. This table used to report a hand-run score with nothing in the repo reproducing it, so it could go stale without anything failing.
68
+
69
+ ## Three things that are genuinely different
70
+
71
+ <CardGroup cols={3}>
72
+ <Card title="It works occluded" icon="eye-slash">
73
+ Minimized, app-hidden, behind a fullscreen app on another Space. The agent keeps driving.
74
+ </Card>
75
+ <Card title="Screenshots cannot lie" icon="camera">
76
+ Captured from the window's own backing store, never the screen.
77
+ </Card>
78
+ <Card title="Missing wiring is declared" icon="triangle-exclamation">
79
+ Electron without the preload reports `coverage: partial`, not a clean pass.
80
+ </Card>
81
+ </CardGroup>
82
+
83
+ ### Why screenshots read the backing store
84
+
85
+ `webContents.capturePage()` reads the window's own pixels. Capturing a screen _region_ was tried and deliberately rejected.
86
+
87
+ A region capture photographs whatever is on top. An app window behind your editor yields a picture of the editor, saved as a visual baseline that a later diff would trust.
88
+
89
+ A screenshot tool that can silently return another window's pixels manufactures exactly the false green Reticle exists to eliminate.
90
+
91
+ <Warning>
92
+ One honest caveat. A fully occluded or minimized window is only partially composited, so parts of
93
+ a capture may come back blank. Bring the window forward for a complete image. It is never the
94
+ wrong window.
95
+ </Warning>
96
+
97
+ ### Why a missing preload is not a silent pass
98
+
99
+ Without `@reticlehq/electron/preload`, every IPC call is invisible to Reticle.
100
+
101
+ A tool that just reported clean there would be telling you the IPC layer is fine when it never looked. Instead verdicts carry `coverage: partial` and name the missing line.
102
+
103
+ ## Getting started
104
+
105
+ The workflow has no browser step in it at all.
106
+
107
+ 1. **Start the daemon once.**
108
+
109
+ ```bash
110
+ npx @reticlehq/server serve
111
+ ```
112
+
113
+ 2. **Start your app exactly as you always do.**
114
+
115
+ ```bash
116
+ npm run dev # or: electron . | cargo tauri dev
117
+ ```
118
+
119
+ 3. **Confirm it connected.**
120
+
121
+ ```bash
122
+ npx @reticlehq/server status
123
+ ```
124
+
125
+ Your window appears as a session, and the agent can drive it.
126
+
127
+ ### Electron
128
+
129
+ Screenshots are one line in the main process:
130
+
131
+ ```js
132
+ const { installReticleCapture } = require('@reticlehq/electron/main');
133
+
134
+ const win = new BrowserWindow({
135
+ /* … */
136
+ });
137
+ installReticleCapture(win);
138
+ ```
139
+
140
+ No CDP flag, no extra packages, and it works on a packaged `file://` renderer.
141
+
142
+ ### Tauri
143
+
144
+ The frontend is identical to any web app. The CSP is not optional:
145
+
146
+ ```json
147
+ {
148
+ "app": {
149
+ "security": {
150
+ "csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost ws://localhost:4400 ws://127.0.0.1:4400"
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ <Warning>
157
+ Tauri's default CSP blocks the bridge WebSocket **before it opens**. Your app runs perfectly and
158
+ simply never connects, with no error to go on.
159
+
160
+ Keep `ipc: http://ipc.localhost` in `connect-src`, because Tauri v2 needs it for `invoke` itself. Drop the `ws://` entries from your release config.
161
+
162
+ </Warning>
163
+
164
+ IPC observation needs nothing on the Rust side. An `invoke('load_todos')` already reaches Reticle as `ipc://load_todos`.
165
+
166
+ Screenshots and headless mode need the crate, which is versioned independently of the npm packages:
167
+
168
+ ```toml
169
+ [dependencies]
170
+ reticle-tauri = "0.1"
171
+ ```
172
+
173
+ ```rust
174
+ tauri::Builder::default()
175
+ .invoke_handler(tauri::generate_handler![reticle_tauri::reticle_capture])
176
+ .on_page_load(reticle_tauri::on_page_load)
177
+ ```
178
+
179
+ ## Keep reading
180
+
181
+ <CardGroup cols={2}>
182
+ <Card title="Full desktop guide" icon="book" href="/desktop-apps">
183
+ Every capability in detail, what IPC looks like to an agent, and desktop troubleshooting.
184
+ </Card>
185
+ <Card title="Manual install" icon="screwdriver-wrench" href="/install-manual">
186
+ The wiring for both runtimes, step by step.
187
+ </Card>
188
+ </CardGroup>