@reticlehq/browser 2.13.1 → 3.1.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 (244) hide show
  1. package/dist/actions/actions-dom.d.ts +8 -20
  2. package/dist/actions/actions-dom.js +70 -69
  3. package/dist/actions/actions-press.d.ts +14 -0
  4. package/dist/actions/actions-press.js +123 -0
  5. package/dist/actions/actions.d.ts +1 -89
  6. package/dist/actions/actions.js +64 -365
  7. package/dist/actions/appeared-text.d.ts +0 -20
  8. package/dist/actions/appeared-text.js +6 -36
  9. package/dist/actions/danger-context.d.ts +3 -0
  10. package/dist/actions/danger-context.js +39 -0
  11. package/dist/actions/scroll.d.ts +8 -10
  12. package/dist/actions/scroll.js +28 -17
  13. package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
  14. package/dist/actions/synthetic/synthetic-input.js +13 -0
  15. package/dist/actions/value-input.d.ts +0 -30
  16. package/dist/actions/value-input.js +0 -33
  17. package/dist/commands/commands.d.ts +0 -2
  18. package/dist/commands/commands.js +10 -70
  19. package/dist/connect-options.d.ts +2 -96
  20. package/dist/dom/a11y.d.ts +0 -29
  21. package/dist/dom/a11y.js +3 -90
  22. package/dist/dom/addressing/auto-anchor.d.ts +25 -0
  23. package/dist/dom/addressing/auto-anchor.js +41 -0
  24. package/dist/dom/addressing/refs.d.ts +14 -0
  25. package/dist/dom/addressing/refs.js +114 -0
  26. package/dist/dom/addressing/source.d.ts +9 -0
  27. package/dist/dom/addressing/source.js +29 -0
  28. package/dist/dom/chart.d.ts +0 -61
  29. package/dist/dom/chart.js +0 -60
  30. package/dist/dom/desktop-capture.d.ts +0 -30
  31. package/dist/dom/desktop-capture.js +1 -73
  32. package/dist/dom/dom-ignore.d.ts +0 -30
  33. package/dist/dom/dom-ignore.js +1 -48
  34. package/dist/dom/occlusion.d.ts +0 -7
  35. package/dist/dom/occlusion.js +31 -12
  36. package/dist/dom/paint-context.d.ts +6 -0
  37. package/dist/dom/paint-context.js +38 -0
  38. package/dist/dom/query.d.ts +0 -14
  39. package/dist/dom/query.js +50 -216
  40. package/dist/dom/realm.d.ts +0 -20
  41. package/dist/dom/realm.js +0 -28
  42. package/dist/dom/shadow-registry.d.ts +0 -33
  43. package/dist/dom/shadow-registry.js +2 -72
  44. package/dist/dom/snapshot.d.ts +2 -43
  45. package/dist/dom/snapshot.js +27 -152
  46. package/dist/dom/theme.d.ts +0 -37
  47. package/dist/dom/theme.js +2 -52
  48. package/dist/dom/virtualized.d.ts +0 -17
  49. package/dist/dom/virtualized.js +0 -27
  50. package/dist/edit-epoch.d.ts +1 -23
  51. package/dist/edit-epoch.js +1 -53
  52. package/dist/index.d.ts +4 -5
  53. package/dist/index.js +4 -28
  54. package/dist/observers/animation.d.ts +0 -1
  55. package/dist/observers/animation.js +1 -4
  56. package/dist/observers/blind-spots.d.ts +0 -26
  57. package/dist/observers/blind-spots.js +3 -62
  58. package/dist/observers/console.d.ts +1 -2
  59. package/dist/observers/console.js +41 -30
  60. package/dist/observers/context-open.d.ts +1 -11
  61. package/dist/observers/context-open.js +10 -14
  62. package/dist/observers/departure.d.ts +3 -0
  63. package/dist/observers/departure.js +52 -0
  64. package/dist/observers/dialogs.d.ts +2 -0
  65. package/dist/observers/dialogs.js +39 -0
  66. package/dist/observers/dom.js +28 -52
  67. package/dist/observers/download.d.ts +1 -2
  68. package/dist/observers/download.js +39 -56
  69. package/dist/observers/focus.d.ts +0 -6
  70. package/dist/observers/focus.js +0 -7
  71. package/dist/observers/frames.d.ts +0 -9
  72. package/dist/observers/frames.js +1 -28
  73. package/dist/observers/health.d.ts +0 -5
  74. package/dist/observers/health.js +2 -34
  75. package/dist/observers/install-all.d.ts +2 -0
  76. package/dist/observers/install-all.js +26 -27
  77. package/dist/observers/ipc.d.ts +0 -25
  78. package/dist/observers/ipc.js +0 -65
  79. package/dist/observers/navigation.d.ts +2 -0
  80. package/dist/observers/navigation.js +33 -0
  81. package/dist/observers/net-detail/network-body.d.ts +12 -0
  82. package/dist/observers/net-detail/network-body.js +71 -0
  83. package/dist/observers/net-detail/network-redact.d.ts +1 -0
  84. package/dist/observers/net-detail/network-redact.js +1 -0
  85. package/dist/observers/network-stream.d.ts +0 -21
  86. package/dist/observers/network-stream.js +3 -46
  87. package/dist/observers/network.d.ts +3 -27
  88. package/dist/observers/network.js +178 -295
  89. package/dist/observers/perf.d.ts +0 -10
  90. package/dist/observers/perf.js +0 -16
  91. package/dist/observers/route.d.ts +1 -2
  92. package/dist/observers/route.js +13 -19
  93. package/dist/observers/scroll.d.ts +0 -1
  94. package/dist/observers/scroll.js +2 -11
  95. package/dist/observers/sdk-failure.d.ts +1 -9
  96. package/dist/observers/sdk-failure.js +1 -30
  97. package/dist/observers/state.d.ts +1 -11
  98. package/dist/observers/state.js +13 -41
  99. package/dist/observers/storage.d.ts +0 -14
  100. package/dist/observers/storage.js +10 -34
  101. package/dist/observers/types.d.ts +0 -15
  102. package/dist/observers/types.js +0 -14
  103. package/dist/patching/capture-method.d.ts +0 -24
  104. package/dist/patching/capture-method.js +0 -26
  105. package/dist/presenter/chrome/layers.d.ts +2 -0
  106. package/dist/presenter/chrome/layers.js +2 -0
  107. package/dist/presenter/chrome/log-kinds.d.ts +12 -0
  108. package/dist/presenter/chrome/log-kinds.js +7 -0
  109. package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
  110. package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
  111. package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
  112. package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
  113. package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
  114. package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
  115. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
  116. package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
  117. package/dist/presenter/chrome/presenter-log.d.ts +17 -0
  118. package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
  119. package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
  120. package/dist/presenter/chrome/presenter-report-copy.js +95 -0
  121. package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
  122. package/dist/presenter/chrome/presenter-safe-html.js +16 -0
  123. package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
  124. package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
  125. package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
  126. package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
  127. package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
  128. package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
  129. package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
  130. package/dist/presenter/presenter-account-styles.d.ts +1 -0
  131. package/dist/presenter/presenter-account-styles.js +99 -0
  132. package/dist/presenter/presenter-account.d.ts +40 -0
  133. package/dist/presenter/presenter-account.js +178 -0
  134. package/dist/presenter/presenter-config.d.ts +10 -107
  135. package/dist/presenter/presenter-config.js +5 -92
  136. package/dist/presenter/presenter-controls.d.ts +1 -35
  137. package/dist/presenter/presenter-controls.js +10 -133
  138. package/dist/presenter/presenter-dock-layout.d.ts +0 -3
  139. package/dist/presenter/presenter-dock-layout.js +1 -5
  140. package/dist/presenter/presenter-drag.d.ts +2 -42
  141. package/dist/presenter/presenter-drag.js +3 -76
  142. package/dist/presenter/presenter-glow.d.ts +0 -21
  143. package/dist/presenter/presenter-glow.js +2 -28
  144. package/dist/presenter/presenter-hud-position.d.ts +18 -0
  145. package/dist/presenter/presenter-hud-position.js +50 -0
  146. package/dist/presenter/presenter-report-styles.d.ts +0 -6
  147. package/dist/presenter/presenter-report-styles.js +22 -7
  148. package/dist/presenter/presenter-report.d.ts +5 -7
  149. package/dist/presenter/presenter-report.js +44 -95
  150. package/dist/presenter/presenter-run-state.d.ts +1 -8
  151. package/dist/presenter/presenter-run-state.js +1 -6
  152. package/dist/presenter/presenter-settings-styles.d.ts +0 -1
  153. package/dist/presenter/presenter-settings-styles.js +2 -2
  154. package/dist/presenter/presenter-settings.d.ts +5 -23
  155. package/dist/presenter/presenter-settings.js +21 -33
  156. package/dist/presenter/presenter-shell-styles.js +23 -20
  157. package/dist/presenter/presenter-shell.d.ts +6 -18
  158. package/dist/presenter/presenter-shell.js +26 -77
  159. package/dist/presenter/presenter-styles.d.ts +2 -10
  160. package/dist/presenter/presenter-styles.js +6 -12
  161. package/dist/presenter/presenter-workspace.d.ts +4 -4
  162. package/dist/presenter/presenter-workspace.js +30 -10
  163. package/dist/presenter/presenter.d.ts +3 -63
  164. package/dist/presenter/presenter.js +29 -132
  165. package/dist/presenter/tour/tour-view.d.ts +33 -0
  166. package/dist/presenter/tour/tour-view.js +188 -0
  167. package/dist/presenter/tour/tour.d.ts +25 -0
  168. package/dist/presenter/tour/tour.js +245 -0
  169. package/dist/recorder/recorder-styles.d.ts +0 -6
  170. package/dist/recorder/recorder-styles.js +0 -6
  171. package/dist/recorder/recorder.d.ts +0 -34
  172. package/dist/recorder/recorder.js +6 -49
  173. package/dist/registry/auto-testids.d.ts +0 -19
  174. package/dist/registry/auto-testids.js +1 -21
  175. package/dist/registry/capabilities.d.ts +0 -31
  176. package/dist/registry/capabilities.js +1 -46
  177. package/dist/registry/commit-and-signal.d.ts +0 -6
  178. package/dist/registry/commit-and-signal.js +1 -7
  179. package/dist/registry/declared-channels.d.ts +2 -0
  180. package/dist/registry/declared-channels.js +14 -0
  181. package/dist/registry/declared-platform.d.ts +4 -0
  182. package/dist/registry/declared-platform.js +34 -0
  183. package/dist/registry/domains.d.ts +0 -8
  184. package/dist/registry/domains.js +0 -7
  185. package/dist/registry/emitter.d.ts +0 -14
  186. package/dist/registry/emitter.js +1 -11
  187. package/dist/registry/store-adapters.d.ts +0 -186
  188. package/dist/registry/store-adapters.js +1 -164
  189. package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
  190. package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
  191. package/dist/registry/stores.d.ts +0 -33
  192. package/dist/registry/stores.js +1 -60
  193. package/dist/reticle-presenter-helpers.d.ts +0 -13
  194. package/dist/reticle-presenter-helpers.js +10 -20
  195. package/dist/reticle-singleton.d.ts +2 -0
  196. package/dist/reticle-singleton.js +3 -0
  197. package/dist/reticle.d.ts +1 -52
  198. package/dist/reticle.js +115 -180
  199. package/dist/review/annotator-styles.js +1 -8
  200. package/dist/review/annotator.d.ts +2 -13
  201. package/dist/review/annotator.js +2 -42
  202. package/dist/review/mark-anchor.d.ts +0 -11
  203. package/dist/review/mark-anchor.js +3 -11
  204. package/dist/security/serialization.d.ts +0 -20
  205. package/dist/security/serialization.js +1 -66
  206. package/dist/session-continuity.d.ts +0 -16
  207. package/dist/session-continuity.js +1 -20
  208. package/dist/timers/clock.d.ts +1 -14
  209. package/dist/timers/clock.js +59 -60
  210. package/dist/timers/console/native-console.js +5 -0
  211. package/dist/timers/native/native-timers.d.ts +11 -0
  212. package/dist/timers/native/native-timers.js +48 -0
  213. package/dist/transport/transport.d.ts +0 -51
  214. package/dist/transport/transport.js +5 -131
  215. package/dist/transport/unreachable-message.d.ts +0 -17
  216. package/dist/transport/unreachable-message.js +0 -17
  217. package/dist/util/captured-method.d.ts +0 -35
  218. package/dist/util/captured-method.js +0 -35
  219. package/package.json +5 -4
  220. package/dist/actions/synthetic-input.d.ts +0 -9
  221. package/dist/actions/synthetic-input.js +0 -40
  222. package/dist/dom/auto-anchor.d.ts +0 -65
  223. package/dist/dom/auto-anchor.js +0 -74
  224. package/dist/dom/refs.d.ts +0 -90
  225. package/dist/dom/refs.js +0 -245
  226. package/dist/dom/source.d.ts +0 -53
  227. package/dist/dom/source.js +0 -74
  228. package/dist/observers/network-body.d.ts +0 -35
  229. package/dist/observers/network-body.js +0 -168
  230. package/dist/observers/network-redact.d.ts +0 -20
  231. package/dist/observers/network-redact.js +0 -80
  232. package/dist/presenter/presenter-brand.d.ts +0 -22
  233. package/dist/presenter/presenter-effects.d.ts +0 -13
  234. package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
  235. package/dist/presenter/presenter-log.d.ts +0 -64
  236. package/dist/presenter/presenter-report-copy.d.ts +0 -112
  237. package/dist/presenter/presenter-report-copy.js +0 -156
  238. package/dist/presenter/presenter-test-helpers.d.ts +0 -16
  239. package/dist/presenter/presenter-test-helpers.js +0 -41
  240. package/dist/timers/native-console.js +0 -9
  241. package/dist/timers/native-timers.d.ts +0 -36
  242. package/dist/timers/native-timers.js +0 -78
  243. /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
  244. /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
@@ -1,37 +1,2 @@
1
- /**
2
- * Read a host object's method as a STORED VALUE, for code that has to put it back.
3
- *
4
- * Every observer here patches a global and must be able to undo it: `console.error`, `window.fetch`,
5
- * `window.setTimeout`, `Date.now`. Undoing means holding the true original — the same function
6
- * object, so teardown restores exactly what was there and a later install can recognise it by
7
- * identity — and calling through a separately bound copy, because a host method invoked with the
8
- * wrong receiver throws (`fetch` gives "Illegal invocation").
9
- *
10
- * Written as `const original = window.fetch`, that is a bare unbound method reference, and
11
- * `@typescript-eslint/unbound-method` is right to flag the SHAPE even though this use is deliberate:
12
- * the rule cannot see that the next line binds it, nor that the raw value only ever goes back into
13
- * the slot it came from.
14
- *
15
- * The two obvious ways out are both wrong here. An `eslint-disable` is against this repo's standing
16
- * rule and hides the shape from every future reader. Annotating `this: void` asserts the function
17
- * does not use its receiver — which for a console method or `fetch` is simply false, and is the very
18
- * reason the binding exists.
19
- *
20
- * So the intent is expressed instead of suppressed. A property descriptor's `value` is a stored
21
- * value, not a method being detached from its object, which is exactly the distinction the rule is
22
- * drawing and exactly what this code wants. It is also strictly more accurate for the restore case:
23
- * it reads what the slot HOLDS rather than what calling the property would do, so an accessor
24
- * defined on the object cannot be silently flattened into its result.
25
- *
26
- * Returns undefined when the property is absent or inherited rather than own — a caller that needs a
27
- * fallback to the prototype chain should say so at its own call site rather than have it hidden here.
28
- */
29
1
  export declare function capturedMethod<T>(target: object, key: PropertyKey): T | undefined;
30
- /**
31
- * The same, for a property this code knows is present — the globals every observer patches.
32
- *
33
- * Throws rather than returning a broken teardown. A missing `window.fetch` is not a condition any
34
- * caller here can recover from, and silently installing a patch that cannot be undone is worse than
35
- * refusing: the page keeps the wrapper for the rest of its life with nothing able to remove it.
36
- */
37
2
  export declare function requireCapturedMethod<T>(target: object, key: PropertyKey): T;
@@ -1,41 +1,6 @@
1
- /**
2
- * Read a host object's method as a STORED VALUE, for code that has to put it back.
3
- *
4
- * Every observer here patches a global and must be able to undo it: `console.error`, `window.fetch`,
5
- * `window.setTimeout`, `Date.now`. Undoing means holding the true original — the same function
6
- * object, so teardown restores exactly what was there and a later install can recognise it by
7
- * identity — and calling through a separately bound copy, because a host method invoked with the
8
- * wrong receiver throws (`fetch` gives "Illegal invocation").
9
- *
10
- * Written as `const original = window.fetch`, that is a bare unbound method reference, and
11
- * `@typescript-eslint/unbound-method` is right to flag the SHAPE even though this use is deliberate:
12
- * the rule cannot see that the next line binds it, nor that the raw value only ever goes back into
13
- * the slot it came from.
14
- *
15
- * The two obvious ways out are both wrong here. An `eslint-disable` is against this repo's standing
16
- * rule and hides the shape from every future reader. Annotating `this: void` asserts the function
17
- * does not use its receiver — which for a console method or `fetch` is simply false, and is the very
18
- * reason the binding exists.
19
- *
20
- * So the intent is expressed instead of suppressed. A property descriptor's `value` is a stored
21
- * value, not a method being detached from its object, which is exactly the distinction the rule is
22
- * drawing and exactly what this code wants. It is also strictly more accurate for the restore case:
23
- * it reads what the slot HOLDS rather than what calling the property would do, so an accessor
24
- * defined on the object cannot be silently flattened into its result.
25
- *
26
- * Returns undefined when the property is absent or inherited rather than own — a caller that needs a
27
- * fallback to the prototype chain should say so at its own call site rather than have it hidden here.
28
- */
29
1
  export function capturedMethod(target, key) {
30
2
  return Object.getOwnPropertyDescriptor(target, key)?.value;
31
3
  }
32
- /**
33
- * The same, for a property this code knows is present — the globals every observer patches.
34
- *
35
- * Throws rather than returning a broken teardown. A missing `window.fetch` is not a condition any
36
- * caller here can recover from, and silently installing a patch that cannot be undone is worse than
37
- * refusing: the page keeps the wrapper for the rest of its life with nothing able to remove it.
38
- */
39
4
  export function requireCapturedMethod(target, key) {
40
5
  const value = capturedMethod(target, key);
41
6
  if (value === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/browser",
3
- "version": "2.13.1",
3
+ "version": "3.1.0",
4
4
  "description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,12 +13,13 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "dependencies": {
16
- "@reticlehq/core": "2.13.1"
16
+ "@reticlehq/core": "3.1.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@iconify-json/heroicons-outline": "^1.2.1",
20
20
  "@iconify-json/heroicons-solid": "^1.2.1",
21
21
  "@tanstack/query-core": "^5.101.4",
22
+ "esbuild": "^0.28.2",
22
23
  "jotai": "^2.20.2",
23
24
  "jsdom": "^30.0.1",
24
25
  "mobx": "^7.0.3",
@@ -35,7 +36,7 @@
35
36
  "repository": {
36
37
  "type": "git",
37
38
  "url": "git+https://github.com/reticlehq/reticle.git",
38
- "directory": "packages/browser"
39
+ "directory": "adapters/realm/browser"
39
40
  },
40
41
  "homepage": "https://github.com/reticlehq/reticle#readme",
41
42
  "bugs": "https://github.com/reticlehq/reticle/issues",
@@ -59,7 +60,7 @@
59
60
  "node": ">=20.0.0"
60
61
  },
61
62
  "scripts": {
62
- "build": "node scripts/embed-heroicons.mjs && tsc -b",
63
+ "build": "node scripts/embed-heroicons.mjs && tsc -b && node ../../../scripts/alias-dist.mjs",
63
64
  "typecheck": "tsc -b",
64
65
  "lint": "eslint src",
65
66
  "test:unit": "vitest run src --passWithNoTests",
@@ -1,9 +0,0 @@
1
- /** True while a Reticle-dispatched input event is being delivered. */
2
- export declare function isSyntheticInput(): boolean;
3
- /**
4
- * Run `dispatch` marked as Reticle's own.
5
- *
6
- * `finally` rather than a trailing decrement: a listener that throws must not leave the flag stuck
7
- * on, which would silently disable annotation for the rest of the page's life.
8
- */
9
- export declare function asSyntheticInput<T>(dispatch: () => T): T;
@@ -1,40 +0,0 @@
1
- /**
2
- * Is the input event currently being dispatched one that RETICLE made?
3
- *
4
- * The annotator listens for clicks in the capture phase and cancels them, because a click in
5
- * annotate mode is a request to place a mark rather than a request to press a button. That is right
6
- * for a person and wrong for the agent: `reticle_act` dispatches its clicks into the same document,
7
- * so with the HUD expanded every agent click was swallowed while the action still reported
8
- * `dispatched: true`. Measured on this repo's own fixture — the same click that produced
9
- * `POST /api/login -> 200` with annotate off produced no network at all with it on.
10
- *
11
- * `isTrusted` looks like the answer and is not usable: it is false for everything `dispatchEvent`
12
- * creates, which is also how every test in this package drives the annotator, so gating on it would
13
- * make annotation untestable and would still not distinguish Reticle's clicks from the page's own
14
- * synthetic ones.
15
- *
16
- * So Reticle marks its own. Dispatch is synchronous, so a plain depth counter held across the call
17
- * is exact: any listener running inside it — capture phase included — is running because of us.
18
- * Nested actions are counted rather than flagged, so an inner action finishing does not clear the
19
- * mark for the outer one still in flight.
20
- */
21
- let depth = 0;
22
- /** True while a Reticle-dispatched input event is being delivered. */
23
- export function isSyntheticInput() {
24
- return depth > 0;
25
- }
26
- /**
27
- * Run `dispatch` marked as Reticle's own.
28
- *
29
- * `finally` rather than a trailing decrement: a listener that throws must not leave the flag stuck
30
- * on, which would silently disable annotation for the rest of the page's life.
31
- */
32
- export function asSyntheticInput(dispatch) {
33
- depth += 1;
34
- try {
35
- return dispatch();
36
- }
37
- finally {
38
- depth -= 1;
39
- }
40
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Auto-anchor synthesis — derive a STABLE way to address any element WITHOUT a hand-added
3
- * data-testid. This is the brain of zero-config integration: given what the framework already knows
4
- * about an element (its component name + source location from the fiber, its role + accessible name,
5
- * its position), pick the most durable anchor so a flow can re-find the element across refactors.
6
- *
7
- * Pure and framework-agnostic: it takes a plain descriptor (the React adapter, or a future Vue/Svelte
8
- * adapter, fills it in) and returns the chosen anchor + whether it is stable. No DOM, no fiber here.
9
- *
10
- * Priority (most → least durable). Each tier is only used when the one above is unavailable:
11
- * 1. testid — explicit, author-chosen, the gold standard. Stable.
12
- * 2. component@source — component display name + source file:line. Survives text/markup/style
13
- * changes; uniquely identifies WHERE in the source the element comes from.
14
- * Stable (a move that changes the source line is a real change worth re-pinning).
15
- * 3. component+role/name — component name plus role/accessible-name when no source is available.
16
- * Stable enough: the component identity anchors it, role/name disambiguate.
17
- * 4. role+name — semantic but content-dependent: an accessible name can change with copy.
18
- * NOT marked stable (a healed locator could drift to a renamed element).
19
- * 5. role / position — last resort. NOT stable; surfaced so the element is still addressable, but
20
- * a flow built on it should be flagged (the recorder marks the step degraded).
21
- *
22
- * The point: tiers 1–3 are "stable" (a flow anchored on them can be trusted to re-resolve to the
23
- * same element); tiers 4–5 are best-effort. Today most elements without a testid fall to role/text
24
- * (degraded). With component+source, the SAME elements become tier-2 stable anchors — that is how
25
- * integration becomes universal without the app author adding a single testid.
26
- */
27
- /** Which signal produced the anchor — also its durability tier. */
28
- export declare const AnchorStrategy: {
29
- readonly TESTID: "testid";
30
- readonly COMPONENT: "component";
31
- readonly ROLE: "role";
32
- readonly POSITION: "position";
33
- };
34
- export type AnchorStrategy = (typeof AnchorStrategy)[keyof typeof AnchorStrategy];
35
- /** What an adapter knows about an element — any subset; richer input yields a more durable anchor. */
36
- export interface AnchorInput {
37
- /** Explicit data-testid, if the author added one. */
38
- testid?: string;
39
- /** Nearest component display name (React fiber / Vue instance / …). */
40
- component?: string;
41
- /** Source location stamped by the framework's compiler/plugin. */
42
- source?: {
43
- file: string;
44
- line: number;
45
- };
46
- /** ARIA role. */
47
- role?: string;
48
- /** Accessible name (label / text). */
49
- name?: string;
50
- /** Index among same-component-or-role siblings — a positional tiebreaker. */
51
- nth?: number;
52
- }
53
- interface SynthesizedAnchor {
54
- strategy: AnchorStrategy;
55
- /** A compact, human-legible, re-resolvable anchor string. */
56
- value: string;
57
- /** True when a flow can trust this anchor to re-resolve to the same element (tiers 1–3). */
58
- stable: boolean;
59
- }
60
- /**
61
- * Choose the most durable anchor for an element from whatever the adapter could observe. Always
62
- * returns something addressable (falls through to position), with `stable` marking tiers 1–3.
63
- */
64
- export declare function synthesizeAnchor(input: AnchorInput): SynthesizedAnchor;
65
- export {};
@@ -1,74 +0,0 @@
1
- /**
2
- * Auto-anchor synthesis — derive a STABLE way to address any element WITHOUT a hand-added
3
- * data-testid. This is the brain of zero-config integration: given what the framework already knows
4
- * about an element (its component name + source location from the fiber, its role + accessible name,
5
- * its position), pick the most durable anchor so a flow can re-find the element across refactors.
6
- *
7
- * Pure and framework-agnostic: it takes a plain descriptor (the React adapter, or a future Vue/Svelte
8
- * adapter, fills it in) and returns the chosen anchor + whether it is stable. No DOM, no fiber here.
9
- *
10
- * Priority (most → least durable). Each tier is only used when the one above is unavailable:
11
- * 1. testid — explicit, author-chosen, the gold standard. Stable.
12
- * 2. component@source — component display name + source file:line. Survives text/markup/style
13
- * changes; uniquely identifies WHERE in the source the element comes from.
14
- * Stable (a move that changes the source line is a real change worth re-pinning).
15
- * 3. component+role/name — component name plus role/accessible-name when no source is available.
16
- * Stable enough: the component identity anchors it, role/name disambiguate.
17
- * 4. role+name — semantic but content-dependent: an accessible name can change with copy.
18
- * NOT marked stable (a healed locator could drift to a renamed element).
19
- * 5. role / position — last resort. NOT stable; surfaced so the element is still addressable, but
20
- * a flow built on it should be flagged (the recorder marks the step degraded).
21
- *
22
- * The point: tiers 1–3 are "stable" (a flow anchored on them can be trusted to re-resolve to the
23
- * same element); tiers 4–5 are best-effort. Today most elements without a testid fall to role/text
24
- * (degraded). With component+source, the SAME elements become tier-2 stable anchors — that is how
25
- * integration becomes universal without the app author adding a single testid.
26
- */
27
- /** Which signal produced the anchor — also its durability tier. */
28
- export const AnchorStrategy = {
29
- TESTID: 'testid',
30
- COMPONENT: 'component',
31
- ROLE: 'role',
32
- POSITION: 'position',
33
- };
34
- function nonEmpty(s) {
35
- return 'string' === typeof s && s.length > 0;
36
- }
37
- /** Source as a compact `file:line` suffix; basename only to stay terse and path-independent. */
38
- function sourceTag(source) {
39
- const base = source.file.split('/').pop() ?? source.file;
40
- return `${base}:${source.line}`;
41
- }
42
- /**
43
- * Choose the most durable anchor for an element from whatever the adapter could observe. Always
44
- * returns something addressable (falls through to position), with `stable` marking tiers 1–3.
45
- */
46
- export function synthesizeAnchor(input) {
47
- if (nonEmpty(input.testid)) {
48
- return { strategy: AnchorStrategy.TESTID, value: input.testid, stable: true };
49
- }
50
- if (nonEmpty(input.component) && input.source !== undefined) {
51
- return {
52
- strategy: AnchorStrategy.COMPONENT,
53
- value: `${input.component}@${sourceTag(input.source)}`,
54
- stable: true,
55
- };
56
- }
57
- if (nonEmpty(input.component) && (nonEmpty(input.role) || nonEmpty(input.name))) {
58
- const qualifier = nonEmpty(input.name) ? input.name : input.role;
59
- return {
60
- strategy: AnchorStrategy.COMPONENT,
61
- value: `${input.component}[${qualifier ?? ''}]`,
62
- stable: true,
63
- };
64
- }
65
- if (nonEmpty(input.role) && nonEmpty(input.name)) {
66
- return { strategy: AnchorStrategy.ROLE, value: `${input.role}:${input.name}`, stable: false };
67
- }
68
- if (nonEmpty(input.role)) {
69
- const suffix = input.nth !== undefined ? `#${input.nth}` : '';
70
- return { strategy: AnchorStrategy.ROLE, value: `${input.role}${suffix}`, stable: false };
71
- }
72
- // Last resort: position only — addressable but never trusted.
73
- return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
74
- }
@@ -1,90 +0,0 @@
1
- import { type Ref } from '@reticlehq/core';
2
- /**
3
- * Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
4
- * element keeps its ref across snapshots, and a ref re-resolves to its element as long as
5
- * the element is still in the document.
6
- */
7
- /**
8
- * How many ref -> element entries are retained.
9
- *
10
- * The forward map is weak, so elements are always collectable; this bounds the BOOKKEEPING, which was
11
- * not. Refs are minted far more often than an agent asks about them — every meaningful DOM addition,
12
- * every transitionend, every scroll reveal — and the only eviction used to be "the agent happened to
13
- * resolve this exact dead ref". A busy app over a long session therefore accumulated entries for
14
- * elements that had been garbage for hours.
15
- *
16
- * Sized generously because eviction is not free of consequence (see refFor): the useful lifetime of a
17
- * ref is one agent turn, and this is far more than any one turn produces.
18
- */
19
- export declare const MAX_TRACKED_REFS = 10000;
20
- /**
21
- * A ref, made safe to interpolate into an error message.
22
- *
23
- * A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
24
- * an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
25
- * value directly, so the caller's own bad argument came back as a 100KB error.
26
- *
27
- * The server caps error messages centrally, but it cannot help here: the browser's transport
28
- * serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
29
- * exact substring the server's recovery table matches on. A stale ref then arrives classified as a
30
- * possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
31
- * the message.
32
- */
33
- export declare function echoRef(ref: string): string;
34
- /**
35
- * How many numbers a document claims at once.
36
- *
37
- * The reservation is a synchronous storage write, so it cannot happen per mint — mints are frequent
38
- * (every meaningful DOM addition, every transitionend, every scroll reveal). One write per this many
39
- * mints is free.
40
- *
41
- * Sized to keep refs SHORT, and the first attempt at that reasoned about the wrong axis. It bounded
42
- * how many navigations it took to reach seven digits and concluded "a hundred" — but the cost is not
43
- * paid at seven digits, it is paid at every digit, on every ref, in every snapshot and every error
44
- * message, on every turn. At 10,000 the SECOND document already mints five-digit refs.
45
- *
46
- * Measured on one unchanged three-element page across a session: 61 tokens on the first document,
47
- * 64 after a reload, 65 later — +6.5% for the same page, entirely in ref width. The benchmark saw the
48
- * same thing as a ~4% rise in average observation cost, which is what a benchmark is for.
49
- *
50
- * So the block is small. A document claims another one when it outmints this, which costs one more
51
- * storage write and is exactly proportional: a page with thousands of elements has a long snapshot
52
- * anyway, and a small page — the common case, and the one an agent drives repeatedly — keeps
53
- * three-digit refs across dozens of navigations.
54
- */
55
- export declare const REF_BLOCK = 100;
56
- export declare class RefRegistry {
57
- #private;
58
- /**
59
- * Storage is injected rather than read from the global for the same reason the clock is: it is an
60
- * ambient dependency that decides behaviour, and the cross-document property is untestable if the
61
- * only way to get a second document is to actually navigate.
62
- */
63
- constructor(storage?: () => Storage | undefined);
64
- /** How many reverse entries are currently retained. Exposed so the bound can be asserted. */
65
- get size(): number;
66
- /**
67
- * A hot update just replaced modules in this document: every ref handed out so far was minted
68
- * against the DOM the previous version of that code produced.
69
- *
70
- * Only the boundary is recorded. Nothing is evicted — a hot update is not a navigation, and most
71
- * of the page survives one, so refs minted before it very often still resolve. This exists so the
72
- * ones that DON'T can say why.
73
- */
74
- markEdited(): void;
75
- /** Was this ref handed out before the last hot update? False while no update has been observed. */
76
- mintedBeforeLastEdit(ref: string): boolean;
77
- /** Get the existing ref for an element, or mint a new one. */
78
- refFor(el: Element): Ref;
79
- /**
80
- * Resolve a ref back to its element, or null if it's gone/detached.
81
- *
82
- * Takes a plain `string` ON PURPOSE: this is the untrusted-input path — the ref comes from the agent
83
- * over the wire, and a miss is answered with null rather than an error. Requiring a branded Ref here
84
- * would force a meaningless cast at every wire boundary and buy nothing; the brand's value is on the
85
- * MINT (refFor) so our own code cannot pass, say, a sessionId where a handle is expected.
86
- */
87
- resolve(ref: string): Element | null;
88
- }
89
- /** Process-wide registry shared by snapshot, query, and the action executor. */
90
- export declare const refs: RefRegistry;
package/dist/dom/refs.js DELETED
@@ -1,245 +0,0 @@
1
- import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
2
- /**
3
- * Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
4
- * element keeps its ref across snapshots, and a ref re-resolves to its element as long as
5
- * the element is still in the document.
6
- */
7
- /**
8
- * How many ref -> element entries are retained.
9
- *
10
- * The forward map is weak, so elements are always collectable; this bounds the BOOKKEEPING, which was
11
- * not. Refs are minted far more often than an agent asks about them — every meaningful DOM addition,
12
- * every transitionend, every scroll reveal — and the only eviction used to be "the agent happened to
13
- * resolve this exact dead ref". A busy app over a long session therefore accumulated entries for
14
- * elements that had been garbage for hours.
15
- *
16
- * Sized generously because eviction is not free of consequence (see refFor): the useful lifetime of a
17
- * ref is one agent turn, and this is far more than any one turn produces.
18
- */
19
- export const MAX_TRACKED_REFS = 10000;
20
- /** Marks a ref that was too long to echo in full. */
21
- const REF_ELISION = '…';
22
- /** Every ref Reticle mints is this letter followed by its sequence number. */
23
- const REF_PREFIX = 'e';
24
- /**
25
- * A ref, made safe to interpolate into an error message.
26
- *
27
- * A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
28
- * an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
29
- * value directly, so the caller's own bad argument came back as a 100KB error.
30
- *
31
- * The server caps error messages centrally, but it cannot help here: the browser's transport
32
- * serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
33
- * exact substring the server's recovery table matches on. A stale ref then arrives classified as a
34
- * possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
35
- * the message.
36
- */
37
- export function echoRef(ref) {
38
- const max = TRANSPORT_LIMITS.MAX_REF_LENGTH;
39
- return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
40
- }
41
- /**
42
- * Full-sweep cadence. The deref-sweep is O(retained), and a page legitimately holding ≥ MAX_TRACKED_REFS
43
- * live elements (a 10k-node dashboard) can never get under the cap that way, so running it on EVERY mint
44
- * over the cap turned each mint into a 10k-entry scan — measured 264µs vs 0.20µs (1,300x) on the app's
45
- * main thread, ~2.6% of it forever on a churning page. The cap is instead held by an O(1) oldest-drop per
46
- * over-cap mint, and the expensive dead-entry sweep runs once per this many mints (amortized O(1)).
47
- */
48
- const SWEEP_EVERY_MINTS = 1000;
49
- /**
50
- * Where a document records the ref numbers it has claimed, so the next one starts past them.
51
- *
52
- * Refs are per-DOCUMENT: a reload or a cross-page link tears this module down and the next document
53
- * used to start minting from `e1` again. So `e7` from page A was a valid, resolvable, DIFFERENT
54
- * element on page B — nothing refused, the wrong element was acted on, and the result came back `ok`.
55
- * The SPA case was always safe (WeakRef liveness + isConnected); this was the other half, and the
56
- * more dangerous one, because it produced a false green instead of an error.
57
- *
58
- * Deliberately NOT a change to the ref grammar. Stamping refs `e7@3` would have worked too, but every
59
- * agent passes refs back verbatim and every saved flow on disk already holds bare ones, so it buys a
60
- * wire-format migration for a property the sequence itself can carry. A number that never restarts
61
- * means a stale ref simply misses the map, and the refusal that already exists — already worded for
62
- * exactly this case — fires unchanged.
63
- *
64
- * Namespaced like the session key it sits beside; `sessionStorage` is the right scope for the same
65
- * reason session continuity uses it (survives reloads and same-tab navigations, not shared with
66
- * another tab, which is a different session with its own registry anyway).
67
- */
68
- const REF_BASE_KEY = '__reticle_ref_base';
69
- /**
70
- * How many numbers a document claims at once.
71
- *
72
- * The reservation is a synchronous storage write, so it cannot happen per mint — mints are frequent
73
- * (every meaningful DOM addition, every transitionend, every scroll reveal). One write per this many
74
- * mints is free.
75
- *
76
- * Sized to keep refs SHORT, and the first attempt at that reasoned about the wrong axis. It bounded
77
- * how many navigations it took to reach seven digits and concluded "a hundred" — but the cost is not
78
- * paid at seven digits, it is paid at every digit, on every ref, in every snapshot and every error
79
- * message, on every turn. At 10,000 the SECOND document already mints five-digit refs.
80
- *
81
- * Measured on one unchanged three-element page across a session: 61 tokens on the first document,
82
- * 64 after a reload, 65 later — +6.5% for the same page, entirely in ref width. The benchmark saw the
83
- * same thing as a ~4% rise in average observation cost, which is what a benchmark is for.
84
- *
85
- * So the block is small. A document claims another one when it outmints this, which costs one more
86
- * storage write and is exactly proportional: a page with thousands of elements has a long snapshot
87
- * anyway, and a small page — the common case, and the one an agent drives repeatedly — keeps
88
- * three-digit refs across dozens of navigations.
89
- */
90
- export const REF_BLOCK = 100;
91
- /** The reserved-block high-water mark, or 0 when storage is unavailable or its value is junk. */
92
- function readBase(store) {
93
- try {
94
- const raw = store?.getItem(REF_BASE_KEY);
95
- const value = null === raw || undefined === raw ? Number.NaN : Number(raw);
96
- return Number.isSafeInteger(value) && value > 0 ? value : 0;
97
- }
98
- catch {
99
- return 0; // a sandboxed iframe throws on access; a session must still start
100
- }
101
- }
102
- export class RefRegistry {
103
- #toRef = new WeakMap();
104
- #fromRef = new Map();
105
- #storage;
106
- #seq = 0;
107
- /** The end of the block currently reserved. Minting past it claims the next one. */
108
- #reserved = 0;
109
- #mintsSinceSweep = 0;
110
- /**
111
- * The highest number handed out before the last hot update was applied, or 0 while none has been.
112
- *
113
- * A watermark rather than a per-ref stamp: the sequence never restarts within a document, so one
114
- * number separates "minted before the code changed" from "minted after it" for every ref at once.
115
- * The alternative — an epoch on every map entry — is bookkeeping paid on every mint for an answer
116
- * needed only on the rare refusal.
117
- */
118
- #editedAtSeq = 0;
119
- /**
120
- * Storage is injected rather than read from the global for the same reason the clock is: it is an
121
- * ambient dependency that decides behaviour, and the cross-document property is untestable if the
122
- * only way to get a second document is to actually navigate.
123
- */
124
- constructor(storage = () => globalThis.sessionStorage) {
125
- this.#storage = storage;
126
- }
127
- /** Read the tab's high-water mark and claim the next block, BEFORE any of it is handed out. */
128
- #reserve() {
129
- let store;
130
- try {
131
- store = this.#storage();
132
- }
133
- catch {
134
- store = undefined; // hostile storage: no cross-document guarantee, but the session still starts
135
- }
136
- const base = Math.max(readBase(store), this.#seq);
137
- this.#reserved = base + REF_BLOCK;
138
- try {
139
- store?.setItem(REF_BASE_KEY, String(this.#reserved));
140
- }
141
- catch {
142
- /* same: never block a mint on storage */
143
- }
144
- // Claimed on the FIRST mint, not at unload — a page that crashes, is killed, or navigates away
145
- // without firing unload has still permanently spent its numbers.
146
- this.#seq = base;
147
- }
148
- /** How many reverse entries are currently retained. Exposed so the bound can be asserted. */
149
- get size() {
150
- return this.#fromRef.size;
151
- }
152
- /** O(1): drop the least-recently-minted reverse entry. Map iterates in insertion order, so the front
153
- * is the oldest. A still-live element whose entry is dropped re-registers on its next refFor. */
154
- #evictOldest() {
155
- const oldest = this.#fromRef.keys().next();
156
- if (oldest.done !== true)
157
- this.#fromRef.delete(oldest.value);
158
- }
159
- /** Full pass: drop every entry whose element has been collected or detached (frees genuinely dead
160
- * bookkeeping), then any remaining excess by age. Amortized — called once per SWEEP_EVERY_MINTS. */
161
- #sweep() {
162
- for (const [ref, weak] of this.#fromRef) {
163
- const el = weak.deref();
164
- if (el === undefined || !el.isConnected)
165
- this.#fromRef.delete(ref);
166
- }
167
- while (this.#fromRef.size > MAX_TRACKED_REFS)
168
- this.#evictOldest();
169
- }
170
- /**
171
- * A hot update just replaced modules in this document: every ref handed out so far was minted
172
- * against the DOM the previous version of that code produced.
173
- *
174
- * Only the boundary is recorded. Nothing is evicted — a hot update is not a navigation, and most
175
- * of the page survives one, so refs minted before it very often still resolve. This exists so the
176
- * ones that DON'T can say why.
177
- */
178
- markEdited() {
179
- this.#editedAtSeq = this.#seq;
180
- }
181
- /** Was this ref handed out before the last hot update? False while no update has been observed. */
182
- mintedBeforeLastEdit(ref) {
183
- if (0 === this.#editedAtSeq || !ref.startsWith(REF_PREFIX))
184
- return false;
185
- const n = Number(ref.slice(REF_PREFIX.length));
186
- return Number.isSafeInteger(n) && n > 0 && n <= this.#editedAtSeq;
187
- }
188
- /** Get the existing ref for an element, or mint a new one. */
189
- refFor(el) {
190
- const existing = this.#toRef.get(el);
191
- if (existing !== undefined) {
192
- // Re-register if this element's entry was evicted while it was off the agent's radar. Without
193
- // this, an element that is still on the page could hold a ref that no longer resolves — the
194
- // eviction would surface as "element not found", which is a wrong answer rather than a slow one.
195
- // Either way, TOUCH it to the recent end (LRU): an element being surfaced right now must not be
196
- // the oldest-drop victim of a mint storm before the agent gets to act on it.
197
- const weak = this.#fromRef.get(existing);
198
- this.#fromRef.delete(existing);
199
- this.#fromRef.set(existing, weak ?? new WeakRef(el));
200
- return existing;
201
- }
202
- if (this.#seq >= this.#reserved)
203
- this.#reserve();
204
- this.#seq += 1;
205
- const ref = asRef(`${REF_PREFIX}${String(this.#seq)}`);
206
- this.#toRef.set(el, ref);
207
- this.#fromRef.set(ref, new WeakRef(el));
208
- // Keep bounded cheaply on every mint; run the full dead-entry sweep only periodically.
209
- this.#mintsSinceSweep += 1;
210
- if (this.#mintsSinceSweep >= SWEEP_EVERY_MINTS) {
211
- this.#mintsSinceSweep = 0;
212
- this.#sweep();
213
- }
214
- else if (this.#fromRef.size > MAX_TRACKED_REFS) {
215
- this.#evictOldest();
216
- }
217
- return ref;
218
- }
219
- /**
220
- * Resolve a ref back to its element, or null if it's gone/detached.
221
- *
222
- * Takes a plain `string` ON PURPOSE: this is the untrusted-input path — the ref comes from the agent
223
- * over the wire, and a miss is answered with null rather than an error. Requiring a branded Ref here
224
- * would force a meaningless cast at every wire boundary and buy nothing; the brand's value is on the
225
- * MINT (refFor) so our own code cannot pass, say, a sessionId where a handle is expected.
226
- */
227
- resolve(ref) {
228
- const weak = this.#fromRef.get(ref);
229
- if (weak === undefined)
230
- return null;
231
- const el = weak.deref();
232
- if (el === undefined || !el.isConnected) {
233
- this.#fromRef.delete(ref);
234
- return null;
235
- }
236
- // LRU touch: a ref the agent just resolved is in ACTIVE use — move it to the most-recent end so an
237
- // oldest-drop eviction between this resolve and the follow-up act can't reclaim it and turn a live
238
- // element into a false "ref no longer resolves". O(1) — Map preserves insertion order.
239
- this.#fromRef.delete(ref);
240
- this.#fromRef.set(ref, weak);
241
- return el;
242
- }
243
- }
244
- /** Process-wide registry shared by snapshot, query, and the action executor. */
245
- export const refs = new RefRegistry();