@reticlehq/browser 2.2.1 → 2.3.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 (271) hide show
  1. package/dist/actions/actions-dom.d.ts +0 -1
  2. package/dist/actions/actions-dom.js +0 -1
  3. package/dist/actions/actions.d.ts +3 -1
  4. package/dist/actions/actions.js +55 -36
  5. package/dist/actions/scroll.d.ts +0 -1
  6. package/dist/actions/scroll.js +0 -1
  7. package/dist/actions/value-input.d.ts +33 -0
  8. package/dist/actions/value-input.js +63 -0
  9. package/dist/commands/commands.d.ts +0 -1
  10. package/dist/commands/commands.js +15 -7
  11. package/dist/dom/a11y.d.ts +0 -2
  12. package/dist/dom/a11y.js +54 -24
  13. package/dist/dom/auto-anchor.d.ts +0 -1
  14. package/dist/dom/auto-anchor.js +0 -1
  15. package/dist/dom/blind-spots.d.ts +0 -1
  16. package/dist/dom/blind-spots.js +0 -1
  17. package/dist/dom/chart.d.ts +0 -1
  18. package/dist/dom/chart.js +0 -1
  19. package/dist/dom/desktop-capture.d.ts +36 -0
  20. package/dist/dom/desktop-capture.js +113 -0
  21. package/dist/dom/dom-ignore.d.ts +7 -1
  22. package/dist/dom/dom-ignore.js +1 -2
  23. package/dist/dom/occlusion.d.ts +0 -1
  24. package/dist/dom/occlusion.js +0 -1
  25. package/dist/dom/query.d.ts +0 -1
  26. package/dist/dom/query.js +61 -13
  27. package/dist/dom/realm.d.ts +30 -0
  28. package/dist/dom/realm.js +58 -0
  29. package/dist/dom/refs.d.ts +0 -1
  30. package/dist/dom/refs.js +0 -1
  31. package/dist/dom/shadow-registry.d.ts +40 -0
  32. package/dist/dom/shadow-registry.js +165 -0
  33. package/dist/dom/snapshot.d.ts +2 -2
  34. package/dist/dom/snapshot.js +21 -4
  35. package/dist/dom/source.d.ts +0 -1
  36. package/dist/dom/source.js +0 -1
  37. package/dist/dom/theme.d.ts +0 -1
  38. package/dist/dom/theme.js +0 -1
  39. package/dist/dom/virtualized.d.ts +19 -0
  40. package/dist/dom/virtualized.js +80 -0
  41. package/dist/index.d.ts +0 -1
  42. package/dist/index.js +0 -1
  43. package/dist/observers/animation.d.ts +0 -1
  44. package/dist/observers/animation.js +0 -1
  45. package/dist/observers/blind-spots.d.ts +16 -4
  46. package/dist/observers/blind-spots.js +65 -11
  47. package/dist/observers/console.d.ts +0 -1
  48. package/dist/observers/console.js +0 -1
  49. package/dist/observers/dom.d.ts +0 -2
  50. package/dist/observers/dom.js +67 -9
  51. package/dist/observers/download.d.ts +6 -0
  52. package/dist/observers/download.js +103 -0
  53. package/dist/observers/focus.d.ts +0 -1
  54. package/dist/observers/focus.js +0 -1
  55. package/dist/observers/frames.d.ts +12 -0
  56. package/dist/observers/frames.js +67 -0
  57. package/dist/observers/health.d.ts +0 -1
  58. package/dist/observers/health.js +30 -2
  59. package/dist/observers/install-all.d.ts +5 -0
  60. package/dist/observers/install-all.js +63 -0
  61. package/dist/observers/ipc.d.ts +22 -0
  62. package/dist/observers/ipc.js +121 -0
  63. package/dist/observers/network-body.d.ts +17 -1
  64. package/dist/observers/network-body.js +26 -1
  65. package/dist/observers/network-redact.d.ts +0 -1
  66. package/dist/observers/network-redact.js +6 -3
  67. package/dist/observers/network-stream.d.ts +23 -0
  68. package/dist/observers/network-stream.js +102 -0
  69. package/dist/observers/network.d.ts +11 -1
  70. package/dist/observers/network.js +7 -1
  71. package/dist/observers/perf.d.ts +0 -1
  72. package/dist/observers/perf.js +0 -1
  73. package/dist/observers/route.d.ts +0 -1
  74. package/dist/observers/route.js +0 -1
  75. package/dist/observers/scroll.d.ts +0 -1
  76. package/dist/observers/scroll.js +0 -1
  77. package/dist/observers/sdk-failure.d.ts +28 -0
  78. package/dist/observers/sdk-failure.js +60 -0
  79. package/dist/observers/state.d.ts +0 -1
  80. package/dist/observers/state.js +0 -1
  81. package/dist/observers/storage.d.ts +0 -1
  82. package/dist/observers/storage.js +0 -1
  83. package/dist/observers/types.d.ts +0 -1
  84. package/dist/observers/types.js +0 -1
  85. package/dist/patching/capture-method.d.ts +0 -1
  86. package/dist/patching/capture-method.js +0 -1
  87. package/dist/presenter/overlay.d.ts +0 -1
  88. package/dist/presenter/overlay.js +0 -1
  89. package/dist/presenter/presenter-config.d.ts +0 -1
  90. package/dist/presenter/presenter-config.js +0 -1
  91. package/dist/presenter/presenter-controls.d.ts +0 -1
  92. package/dist/presenter/presenter-controls.js +0 -1
  93. package/dist/presenter/presenter-effects.d.ts +0 -1
  94. package/dist/presenter/presenter-effects.js +0 -1
  95. package/dist/presenter/presenter-glow.d.ts +0 -1
  96. package/dist/presenter/presenter-glow.js +0 -1
  97. package/dist/presenter/presenter-log.d.ts +0 -1
  98. package/dist/presenter/presenter-log.js +0 -1
  99. package/dist/presenter/presenter-run-state.d.ts +0 -1
  100. package/dist/presenter/presenter-run-state.js +0 -1
  101. package/dist/presenter/presenter-styles.d.ts +0 -1
  102. package/dist/presenter/presenter-styles.js +0 -1
  103. package/dist/presenter/presenter-tally.d.ts +0 -1
  104. package/dist/presenter/presenter-tally.js +0 -1
  105. package/dist/presenter/presenter-test-helpers.d.ts +0 -1
  106. package/dist/presenter/presenter-test-helpers.js +0 -1
  107. package/dist/presenter/presenter-verbs.d.ts +0 -1
  108. package/dist/presenter/presenter-verbs.js +0 -1
  109. package/dist/presenter/presenter.d.ts +0 -1
  110. package/dist/presenter/presenter.js +0 -1
  111. package/dist/recorder/recorder-styles.d.ts +0 -1
  112. package/dist/recorder/recorder-styles.js +0 -1
  113. package/dist/recorder/recorder.d.ts +0 -1
  114. package/dist/recorder/recorder.js +0 -1
  115. package/dist/registry/adapters.d.ts +0 -1
  116. package/dist/registry/adapters.js +0 -1
  117. package/dist/registry/capabilities.d.ts +0 -1
  118. package/dist/registry/capabilities.js +0 -1
  119. package/dist/registry/commit-and-signal.d.ts +0 -1
  120. package/dist/registry/commit-and-signal.js +0 -1
  121. package/dist/registry/domains.d.ts +0 -1
  122. package/dist/registry/domains.js +0 -1
  123. package/dist/registry/emitter.d.ts +0 -1
  124. package/dist/registry/emitter.js +0 -1
  125. package/dist/registry/store-adapters.d.ts +14 -6
  126. package/dist/registry/store-adapters.js +0 -1
  127. package/dist/registry/stores.d.ts +0 -1
  128. package/dist/registry/stores.js +8 -1
  129. package/dist/reticle-presenter-helpers.d.ts +0 -1
  130. package/dist/reticle-presenter-helpers.js +0 -1
  131. package/dist/reticle.d.ts +3 -2
  132. package/dist/reticle.js +10 -31
  133. package/dist/review/annotator.d.ts +0 -1
  134. package/dist/review/annotator.js +0 -1
  135. package/dist/review/mark-anchor.d.ts +0 -1
  136. package/dist/review/mark-anchor.js +0 -1
  137. package/dist/security/serialization.d.ts +0 -1
  138. package/dist/security/serialization.js +0 -1
  139. package/dist/timers/clock.d.ts +0 -1
  140. package/dist/timers/clock.js +0 -1
  141. package/dist/timers/native-console.d.ts +0 -1
  142. package/dist/timers/native-console.js +0 -1
  143. package/dist/timers/native-timers.d.ts +0 -1
  144. package/dist/timers/native-timers.js +0 -1
  145. package/dist/transport/transport.d.ts +2 -6
  146. package/dist/transport/transport.js +93 -17
  147. package/package.json +8 -3
  148. package/dist/actions/actions-dom.d.ts.map +0 -1
  149. package/dist/actions/actions-dom.js.map +0 -1
  150. package/dist/actions/actions.d.ts.map +0 -1
  151. package/dist/actions/actions.js.map +0 -1
  152. package/dist/actions/scroll.d.ts.map +0 -1
  153. package/dist/actions/scroll.js.map +0 -1
  154. package/dist/commands/commands.d.ts.map +0 -1
  155. package/dist/commands/commands.js.map +0 -1
  156. package/dist/dom/a11y.d.ts.map +0 -1
  157. package/dist/dom/a11y.js.map +0 -1
  158. package/dist/dom/auto-anchor.d.ts.map +0 -1
  159. package/dist/dom/auto-anchor.js.map +0 -1
  160. package/dist/dom/blind-spots.d.ts.map +0 -1
  161. package/dist/dom/blind-spots.js.map +0 -1
  162. package/dist/dom/chart.d.ts.map +0 -1
  163. package/dist/dom/chart.js.map +0 -1
  164. package/dist/dom/dom-ignore.d.ts.map +0 -1
  165. package/dist/dom/dom-ignore.js.map +0 -1
  166. package/dist/dom/occlusion.d.ts.map +0 -1
  167. package/dist/dom/occlusion.js.map +0 -1
  168. package/dist/dom/query.d.ts.map +0 -1
  169. package/dist/dom/query.js.map +0 -1
  170. package/dist/dom/refs.d.ts.map +0 -1
  171. package/dist/dom/refs.js.map +0 -1
  172. package/dist/dom/snapshot.d.ts.map +0 -1
  173. package/dist/dom/snapshot.js.map +0 -1
  174. package/dist/dom/source.d.ts.map +0 -1
  175. package/dist/dom/source.js.map +0 -1
  176. package/dist/dom/theme.d.ts.map +0 -1
  177. package/dist/dom/theme.js.map +0 -1
  178. package/dist/index.d.ts.map +0 -1
  179. package/dist/index.js.map +0 -1
  180. package/dist/observers/animation.d.ts.map +0 -1
  181. package/dist/observers/animation.js.map +0 -1
  182. package/dist/observers/blind-spots.d.ts.map +0 -1
  183. package/dist/observers/blind-spots.js.map +0 -1
  184. package/dist/observers/console.d.ts.map +0 -1
  185. package/dist/observers/console.js.map +0 -1
  186. package/dist/observers/dom.d.ts.map +0 -1
  187. package/dist/observers/dom.js.map +0 -1
  188. package/dist/observers/focus.d.ts.map +0 -1
  189. package/dist/observers/focus.js.map +0 -1
  190. package/dist/observers/health.d.ts.map +0 -1
  191. package/dist/observers/health.js.map +0 -1
  192. package/dist/observers/network-body.d.ts.map +0 -1
  193. package/dist/observers/network-body.js.map +0 -1
  194. package/dist/observers/network-redact.d.ts.map +0 -1
  195. package/dist/observers/network-redact.js.map +0 -1
  196. package/dist/observers/network.d.ts.map +0 -1
  197. package/dist/observers/network.js.map +0 -1
  198. package/dist/observers/perf.d.ts.map +0 -1
  199. package/dist/observers/perf.js.map +0 -1
  200. package/dist/observers/route.d.ts.map +0 -1
  201. package/dist/observers/route.js.map +0 -1
  202. package/dist/observers/scroll.d.ts.map +0 -1
  203. package/dist/observers/scroll.js.map +0 -1
  204. package/dist/observers/state.d.ts.map +0 -1
  205. package/dist/observers/state.js.map +0 -1
  206. package/dist/observers/storage.d.ts.map +0 -1
  207. package/dist/observers/storage.js.map +0 -1
  208. package/dist/observers/types.d.ts.map +0 -1
  209. package/dist/observers/types.js.map +0 -1
  210. package/dist/patching/capture-method.d.ts.map +0 -1
  211. package/dist/patching/capture-method.js.map +0 -1
  212. package/dist/presenter/overlay.d.ts.map +0 -1
  213. package/dist/presenter/overlay.js.map +0 -1
  214. package/dist/presenter/presenter-config.d.ts.map +0 -1
  215. package/dist/presenter/presenter-config.js.map +0 -1
  216. package/dist/presenter/presenter-controls.d.ts.map +0 -1
  217. package/dist/presenter/presenter-controls.js.map +0 -1
  218. package/dist/presenter/presenter-effects.d.ts.map +0 -1
  219. package/dist/presenter/presenter-effects.js.map +0 -1
  220. package/dist/presenter/presenter-glow.d.ts.map +0 -1
  221. package/dist/presenter/presenter-glow.js.map +0 -1
  222. package/dist/presenter/presenter-log.d.ts.map +0 -1
  223. package/dist/presenter/presenter-log.js.map +0 -1
  224. package/dist/presenter/presenter-run-state.d.ts.map +0 -1
  225. package/dist/presenter/presenter-run-state.js.map +0 -1
  226. package/dist/presenter/presenter-styles.d.ts.map +0 -1
  227. package/dist/presenter/presenter-styles.js.map +0 -1
  228. package/dist/presenter/presenter-tally.d.ts.map +0 -1
  229. package/dist/presenter/presenter-tally.js.map +0 -1
  230. package/dist/presenter/presenter-test-helpers.d.ts.map +0 -1
  231. package/dist/presenter/presenter-test-helpers.js.map +0 -1
  232. package/dist/presenter/presenter-verbs.d.ts.map +0 -1
  233. package/dist/presenter/presenter-verbs.js.map +0 -1
  234. package/dist/presenter/presenter.d.ts.map +0 -1
  235. package/dist/presenter/presenter.js.map +0 -1
  236. package/dist/recorder/recorder-styles.d.ts.map +0 -1
  237. package/dist/recorder/recorder-styles.js.map +0 -1
  238. package/dist/recorder/recorder.d.ts.map +0 -1
  239. package/dist/recorder/recorder.js.map +0 -1
  240. package/dist/registry/adapters.d.ts.map +0 -1
  241. package/dist/registry/adapters.js.map +0 -1
  242. package/dist/registry/capabilities.d.ts.map +0 -1
  243. package/dist/registry/capabilities.js.map +0 -1
  244. package/dist/registry/commit-and-signal.d.ts.map +0 -1
  245. package/dist/registry/commit-and-signal.js.map +0 -1
  246. package/dist/registry/domains.d.ts.map +0 -1
  247. package/dist/registry/domains.js.map +0 -1
  248. package/dist/registry/emitter.d.ts.map +0 -1
  249. package/dist/registry/emitter.js.map +0 -1
  250. package/dist/registry/store-adapters.d.ts.map +0 -1
  251. package/dist/registry/store-adapters.js.map +0 -1
  252. package/dist/registry/stores.d.ts.map +0 -1
  253. package/dist/registry/stores.js.map +0 -1
  254. package/dist/reticle-presenter-helpers.d.ts.map +0 -1
  255. package/dist/reticle-presenter-helpers.js.map +0 -1
  256. package/dist/reticle.d.ts.map +0 -1
  257. package/dist/reticle.js.map +0 -1
  258. package/dist/review/annotator.d.ts.map +0 -1
  259. package/dist/review/annotator.js.map +0 -1
  260. package/dist/review/mark-anchor.d.ts.map +0 -1
  261. package/dist/review/mark-anchor.js.map +0 -1
  262. package/dist/security/serialization.d.ts.map +0 -1
  263. package/dist/security/serialization.js.map +0 -1
  264. package/dist/timers/clock.d.ts.map +0 -1
  265. package/dist/timers/clock.js.map +0 -1
  266. package/dist/timers/native-console.d.ts.map +0 -1
  267. package/dist/timers/native-console.js.map +0 -1
  268. package/dist/timers/native-timers.d.ts.map +0 -1
  269. package/dist/timers/native-timers.js.map +0 -1
  270. package/dist/transport/transport.d.ts.map +0 -1
  271. package/dist/transport/transport.js.map +0 -1
@@ -26,4 +26,3 @@ export declare function firePointerNonBubbling(el: Element, type: string, relate
26
26
  */
27
27
  export declare function dragElement(source: HTMLElement, target: HTMLElement | null, data: unknown): Promise<boolean>;
28
28
  export {};
29
- //# sourceMappingURL=actions-dom.d.ts.map
@@ -133,4 +133,3 @@ export async function dragElement(source, target, data) {
133
133
  }
134
134
  return dropPrevented;
135
135
  }
136
- //# sourceMappingURL=actions-dom.js.map
@@ -51,6 +51,9 @@ interface ActionResult {
51
51
  * degraded role one — addressing the element across replays with zero hand-added testids.
52
52
  */
53
53
  component?: string;
54
+ /** Role + accessible name — the anchor that identifies an INSTANCE, not a JSX site. */
55
+ role?: string;
56
+ name?: string;
54
57
  source?: {
55
58
  file: string;
56
59
  line: number;
@@ -89,4 +92,3 @@ export declare function executeSequence(steps: ActionStep[]): Promise<{
89
92
  steps: SequenceStepResult[];
90
93
  }>;
91
94
  export {};
92
- //# sourceMappingURL=actions.d.ts.map
@@ -1,26 +1,10 @@
1
1
  import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, isDangerousActionText, SettleReason, } from '@reticlehq/core';
2
2
  import { refs } from '../dom/refs.js';
3
- import { getAccessibleName, isVisible, getStates } from '../dom/a11y.js';
3
+ import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
4
+ import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
4
5
  import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
5
- import { captureValueSetter } from '../patching/capture-method.js';
6
+ import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
6
7
  import { nativeSetTimeout, settle } from '../timers/native-timers.js';
7
- /**
8
- * Set a value on a controlled input the way React expects (native setter + input event).
9
- * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
10
- */
11
- function setNativeValue(el, value) {
12
- const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
13
- const setter = captureValueSetter(proto);
14
- if (setter !== undefined) {
15
- setter.call(el, value);
16
- }
17
- else {
18
- el.value = value;
19
- }
20
- const notPrevented = el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
21
- el.dispatchEvent(new Event('change', { bubbles: true }));
22
- return !notPrevented;
23
- }
24
8
  function asString(value, fallback = '') {
25
9
  return typeof value === 'string' ? value : fallback;
26
10
  }
@@ -28,7 +12,7 @@ function requireElement(ref) {
28
12
  const el = refs.resolve(ref);
29
13
  if (el === null)
30
14
  throw new Error(`ref '${ref}' no longer resolves to an element`);
31
- if (!(el instanceof HTMLElement))
15
+ if (!isHtmlElement(el))
32
16
  throw new Error(`ref '${ref}' is not an HTMLElement`);
33
17
  return el;
34
18
  }
@@ -55,6 +39,12 @@ function anchorOf(el) {
55
39
  out.component = component;
56
40
  if (info?.source !== undefined)
57
41
  out.source = info.source;
42
+ const role = getRole(el);
43
+ const name = getAccessibleName(el);
44
+ if (role.length > 0 && name.length > 0) {
45
+ out.role = role;
46
+ out.name = name;
47
+ }
58
48
  return out;
59
49
  }
60
50
  const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
@@ -77,6 +67,10 @@ const result = (ref, action, effect, settled, settleReason, anchor, warning) =>
77
67
  base.component = anchor.component;
78
68
  if (anchor.source !== undefined)
79
69
  base.source = anchor.source;
70
+ if (anchor.role !== undefined)
71
+ base.role = anchor.role;
72
+ if (anchor.name !== undefined)
73
+ base.name = anchor.name;
80
74
  if (warning !== undefined)
81
75
  base.warning = warning;
82
76
  return base;
@@ -116,7 +110,7 @@ function assertActionAllowed(el, action, args) {
116
110
  action === ActionType.SUBMIT ||
117
111
  (action === ActionType.PRESS && asString(args['key'], 'Enter') === 'Enter');
118
112
  const dragTarget = action === ActionType.DRAG ? refs.resolve(asString(args['toRef'])) : null;
119
- const context = dragTarget instanceof HTMLElement
113
+ const context = isHtmlElement(dragTarget)
120
114
  ? `${dangerousActionContext(el)} ${dangerousActionContext(dragTarget)}`
121
115
  : dangerousActionContext(el);
122
116
  if (canTrigger &&
@@ -133,9 +127,7 @@ function enabledOf(el) {
133
127
  }
134
128
  /** Current value for inputs/textareas/selects, else undefined. */
135
129
  function valueOf(el) {
136
- if (el instanceof HTMLInputElement ||
137
- el instanceof HTMLTextAreaElement ||
138
- el instanceof HTMLSelectElement) {
130
+ if (isInput(el) || isTextArea(el) || isSelect(el)) {
139
131
  return el.value;
140
132
  }
141
133
  return undefined;
@@ -190,41 +182,69 @@ async function dispatchFor(el, action, args) {
190
182
  el.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
191
183
  return false;
192
184
  case ActionType.FILL:
193
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
185
+ if (isInput(el) || isTextArea(el)) {
186
+ // A MISSING value is a malformed call, not a request to empty the field. `asString` defaults
187
+ // to '', which made such a fill indistinguishable from `clear`: it wiped what was there,
188
+ // dispatched a real input event so React committed the empty string to state, and reported
189
+ // ok:true with nothing flagged. The value is nested (`args:{value}`) at the tool boundary, so
190
+ // passing it at the top level lands here — measured on bench-app's login form, where
191
+ // "admin@reticle.dev" was destroyed by a fill that reported success. An explicit '' still
192
+ // clears, because that is someone asking for it.
193
+ if (typeof args['value'] !== 'string') {
194
+ throw new Error("fill requires a string `value` — pass it nested, as args: { value: '…' }. " +
195
+ 'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
196
+ }
197
+ assertEditable(el, 'fill');
194
198
  el.focus(); // focus first so a later blur commits (onBlur editors)
195
- return setNativeValue(el, asString(args['value']));
199
+ return setNativeValue(el, args['value']);
196
200
  }
201
+ assertNotRichText(el, 'fill');
197
202
  throw new Error(`cannot fill a <${el.tagName.toLowerCase()}>`);
198
203
  case ActionType.TYPE:
199
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
204
+ if (isInput(el) || isTextArea(el)) {
205
+ // Same family as the valueless FILL: a missing `text` appended '' and reported ok:true, so an
206
+ // agent believed it had typed into a field nothing was written to.
207
+ if (typeof args['text'] !== 'string') {
208
+ throw new Error("type requires a string `text` — pass it nested, as args: { text: '…' }.");
209
+ }
210
+ assertEditable(el, 'type');
200
211
  el.focus();
201
- return setNativeValue(el, el.value + asString(args['text']));
212
+ return setNativeValue(el, el.value + args['text']);
202
213
  }
214
+ assertNotRichText(el, 'type into');
203
215
  throw new Error(`cannot type into a <${el.tagName.toLowerCase()}>`);
204
216
  case ActionType.CLEAR:
205
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
217
+ if (isInput(el) || isTextArea(el)) {
206
218
  return setNativeValue(el, '');
207
219
  }
208
220
  // Consistent with TYPE/SELECT/CHECK: clearing a non-field is not a silent success. Throwing
209
221
  // surfaces the mismatch instead of reporting ok:true for an action that did nothing.
210
222
  throw new Error(`cannot clear a <${el.tagName.toLowerCase()}>`);
211
223
  case ActionType.SELECT:
212
- if (el instanceof HTMLSelectElement) {
213
- el.value = asString(args['value']);
224
+ if (isSelect(el)) {
225
+ // A MISSING value assigned '' to the <select>. No option carries that, so the browser sets
226
+ // selectedIndex to -1 — deselecting everything — from a call that was simply malformed.
227
+ // (Selecting a value that no option carries is a different case and is deliberately NOT
228
+ // refused: `actions.effect.test.ts` pins it as a detectable no-op, where the reported
229
+ // valueChanged delta is what tells the agent the option never took.)
230
+ if (typeof args['value'] !== 'string') {
231
+ throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
232
+ }
233
+ el.value = args['value'];
214
234
  el.dispatchEvent(new Event('change', { bubbles: true }));
215
235
  return false; // change is not cancelable.
216
236
  }
217
237
  throw new Error(`cannot select on a <${el.tagName.toLowerCase()}>`);
218
238
  case ActionType.CHECK:
219
239
  case ActionType.UNCHECK:
220
- if (el instanceof HTMLInputElement) {
240
+ if (isInput(el)) {
221
241
  el.checked = action === ActionType.CHECK;
222
242
  el.dispatchEvent(new Event('change', { bubbles: true }));
223
243
  return false;
224
244
  }
225
245
  throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
226
246
  case ActionType.SUBMIT: {
227
- const form = el instanceof HTMLFormElement ? el : el.closest('form');
247
+ const form = isForm(el) ? el : el.closest('form');
228
248
  if (form === null)
229
249
  throw new Error('no form to submit');
230
250
  form.requestSubmit();
@@ -240,7 +260,7 @@ async function dispatchFor(el, action, args) {
240
260
  el.scrollIntoView();
241
261
  return false;
242
262
  case ActionType.UPLOAD: {
243
- if (!(el instanceof HTMLInputElement) || el.type !== 'file') {
263
+ if (!isInput(el) || el.type !== 'file') {
244
264
  throw new Error('upload target must be a <input type="file">');
245
265
  }
246
266
  const file = new File([asString(args['content'], 'reticle test file')], asString(args['name'], 'file.txt'), {
@@ -258,7 +278,7 @@ async function dispatchFor(el, action, args) {
258
278
  // asString falls back to '' (never undefined), so the old `!== undefined` was always true and
259
279
  // handed '' to refs.resolve. A drag with no target ref is a free drag — resolve nothing.
260
280
  const resolved = toRef !== '' ? refs.resolve(toRef) : null;
261
- return await dragElement(el, resolved instanceof HTMLElement ? resolved : null, args['data']);
281
+ return await dragElement(el, isHtmlElement(resolved) ? resolved : null, args['data']);
262
282
  }
263
283
  default:
264
284
  throw new Error(`unknown action '${action}'`);
@@ -363,4 +383,3 @@ export async function executeSequence(steps) {
363
383
  }
364
384
  return { ok: true, count: steps.length, effects, steps: stepResults };
365
385
  }
366
- //# sourceMappingURL=actions.js.map
@@ -16,4 +16,3 @@ interface ScrollResult {
16
16
  */
17
17
  export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number): ScrollResult;
18
18
  export {};
19
- //# sourceMappingURL=scroll.d.ts.map
@@ -41,4 +41,3 @@ export function scrollContainer(ref, dy, fraction) {
41
41
  atEnd: after + target.clientHeight >= target.scrollHeight - 1,
42
42
  };
43
43
  }
44
- //# sourceMappingURL=scroll.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Writing a value into a field, and the two preconditions that make it honest.
3
+ *
4
+ * Lifted out of actions.ts when that file hit the 600-line cap. It is a cohesive unit: every export
5
+ * here exists so that a value write either lands as a real user's edit would, or is refused with a
6
+ * reason — never forced through while reporting success.
7
+ */
8
+ /**
9
+ * Set a value on a controlled input the way React expects (native setter + input event).
10
+ * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
11
+ */
12
+ export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
13
+ /**
14
+ * Refuse to write to a field a person could not edit.
15
+ *
16
+ * `readonly` and `disabled` block USER input, not scripted assignment, so the prototype value setter
17
+ * writes straight through both. Measured: filling a readonly input reported `ok:true` and
18
+ * `valueChanged:true` with nothing in the effect block marking it read-only. That leaves the app in a
19
+ * state no user could reach, and every conclusion drawn from what follows is about software nobody
20
+ * can operate — the same reason the click path reports `occluded`, and the same choice Playwright
21
+ * makes when it refuses to fill a non-editable element.
22
+ */
23
+ /**
24
+ * A rich-text target is UNSUPPORTED, not a mistake by the caller.
25
+ *
26
+ * Every rich-text editor (TipTap, Quill, ProseMirror, Slate, Lexical) is `[contenteditable]`, and
27
+ * these actions handle only input/textarea. `cannot fill a <div>` reads as "you picked the wrong
28
+ * element" and sends the reader hunting for a better selector that does not exist. Support is not
29
+ * faked: assigning textContent would update the DOM while the editor's own model kept the old value,
30
+ * so the tool would report success for content the app will never submit.
31
+ */
32
+ export declare function assertNotRichText(el: HTMLElement, action: string): void;
33
+ export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
@@ -0,0 +1,63 @@
1
+ import { captureValueSetter } from '../patching/capture-method.js';
2
+ import { valuePrototypeOf } from '../dom/realm.js';
3
+ /**
4
+ * Writing a value into a field, and the two preconditions that make it honest.
5
+ *
6
+ * Lifted out of actions.ts when that file hit the 600-line cap. It is a cohesive unit: every export
7
+ * here exists so that a value write either lands as a real user's edit would, or is refused with a
8
+ * reason — never forced through while reporting success.
9
+ */
10
+ /**
11
+ * Set a value on a controlled input the way React expects (native setter + input event).
12
+ * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
13
+ */
14
+ export function setNativeValue(el, value) {
15
+ // Realm-aware: a controlled input inside a same-origin frame has its own prototype.
16
+ const proto = valuePrototypeOf(el);
17
+ const setter = proto === undefined ? undefined : captureValueSetter(proto);
18
+ if (setter !== undefined) {
19
+ setter.call(el, value);
20
+ }
21
+ else {
22
+ el.value = value;
23
+ }
24
+ const notPrevented = el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
25
+ el.dispatchEvent(new Event('change', { bubbles: true }));
26
+ return !notPrevented;
27
+ }
28
+ /**
29
+ * Refuse to write to a field a person could not edit.
30
+ *
31
+ * `readonly` and `disabled` block USER input, not scripted assignment, so the prototype value setter
32
+ * writes straight through both. Measured: filling a readonly input reported `ok:true` and
33
+ * `valueChanged:true` with nothing in the effect block marking it read-only. That leaves the app in a
34
+ * state no user could reach, and every conclusion drawn from what follows is about software nobody
35
+ * can operate — the same reason the click path reports `occluded`, and the same choice Playwright
36
+ * makes when it refuses to fill a non-editable element.
37
+ */
38
+ /**
39
+ * A rich-text target is UNSUPPORTED, not a mistake by the caller.
40
+ *
41
+ * Every rich-text editor (TipTap, Quill, ProseMirror, Slate, Lexical) is `[contenteditable]`, and
42
+ * these actions handle only input/textarea. `cannot fill a <div>` reads as "you picked the wrong
43
+ * element" and sends the reader hunting for a better selector that does not exist. Support is not
44
+ * faked: assigning textContent would update the DOM while the editor's own model kept the old value,
45
+ * so the tool would report success for content the app will never submit.
46
+ */
47
+ export function assertNotRichText(el, action) {
48
+ // The ATTRIBUTE, not `isContentEditable`: jsdom does not implement the property, so a test would
49
+ // pass against a guard that never fires in the environment the unit suite runs in.
50
+ const flag = el.getAttribute('contenteditable');
51
+ if (el.isContentEditable || (flag !== null && flag !== 'false')) {
52
+ throw new Error(`cannot ${action} a contenteditable element — rich-text editors keep their own document model, ` +
53
+ 'so writing to the DOM here would look right and submit the old content. This surface is not supported yet.');
54
+ }
55
+ }
56
+ export function assertEditable(el, action) {
57
+ if (el.disabled) {
58
+ throw new Error(`cannot ${action} a disabled <${el.tagName.toLowerCase()}> — a user could not edit it, so forcing the value would put the app in a state nobody can reach`);
59
+ }
60
+ if (el.readOnly) {
61
+ throw new Error(`cannot ${action} a readonly <${el.tagName.toLowerCase()}> — a user could not edit it, so forcing the value would put the app in a state nobody can reach`);
62
+ }
63
+ }
@@ -4,4 +4,3 @@ export declare const RELOAD_CACHE_BUST_PARAM = "_reticle_reload";
4
4
  export declare function resolveNavigationUrl(rawUrl: string, baseUrl: string): string | null;
5
5
  /** Map browser command names to handlers. Used by the transport on each COMMAND. */
6
6
  export declare function createCommandRegistry(): Map<string, CommandHandler>;
7
- //# sourceMappingURL=commands.d.ts.map
@@ -5,8 +5,10 @@ import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/acti
5
5
  import { describe } from '../dom/a11y.js';
6
6
  import { themeReport } from '../dom/theme.js';
7
7
  import { refs } from '../dom/refs.js';
8
+ import { isButton, isInput } from '../dom/realm.js';
8
9
  import { hitTestOccluder } from '../dom/occlusion.js';
9
10
  import { readStorage } from '../observers/storage.js';
11
+ import { captureDesktopWindow } from '../dom/desktop-capture.js';
10
12
  import { identifyComponent, readComponentState } from '../registry/adapters.js';
11
13
  import { readStoresWithTruncation, readStoresRaw, storeNames } from '../registry/stores.js';
12
14
  import { sanitizeWithReport } from '../security/serialization.js';
@@ -24,6 +26,14 @@ function num(value) {
24
26
  function record(value) {
25
27
  return typeof value === 'object' && value !== null ? value : {};
26
28
  }
29
+ function sourceLocation(value) {
30
+ if (typeof value !== 'object' || value === null)
31
+ return undefined;
32
+ const obj = value;
33
+ if (typeof obj['file'] !== 'string' || typeof obj['line'] !== 'number')
34
+ return undefined;
35
+ return { file: obj['file'], line: obj['line'], column: num(obj['column']) };
36
+ }
27
37
  function queryFromArgs(args) {
28
38
  return {
29
39
  by: str(args['by']),
@@ -35,10 +45,12 @@ function queryFromArgs(args) {
35
45
  placeholder: str(args['placeholder']),
36
46
  testid: str(args['testid']),
37
47
  alt: str(args['alt']),
48
+ component: str(args['component']),
38
49
  scope: str(args['scope']),
39
50
  attrs: Array.isArray(args['attrs'])
40
51
  ? args['attrs'].filter((a) => typeof a === 'string')
41
52
  : undefined,
53
+ source: sourceLocation(args['source']),
42
54
  };
43
55
  }
44
56
  function inspect(ref) {
@@ -66,12 +78,8 @@ function inspect(ref) {
66
78
  ...describe(el),
67
79
  tag: el.tagName.toLowerCase(),
68
80
  href: el.getAttribute('href') ?? undefined,
69
- formAction: el instanceof HTMLButtonElement || el instanceof HTMLInputElement
70
- ? (el.form?.getAttribute('action') ?? undefined)
71
- : undefined,
72
- formText: el instanceof HTMLButtonElement || el instanceof HTMLInputElement
73
- ? (el.form?.textContent ?? undefined)
74
- : undefined,
81
+ formAction: isButton(el) || isInput(el) ? (el.form?.getAttribute('action') ?? undefined) : undefined,
82
+ formText: isButton(el) || isInput(el) ? (el.form?.textContent ?? undefined) : undefined,
75
83
  box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
76
84
  // True when another element sits over this one's center point — the click would hit the overlay,
77
85
  // not this control (a z-index/overlay bug the DOM tree cannot show).
@@ -213,6 +221,7 @@ export function createCommandRegistry() {
213
221
  reg.set(ReticleCommand.STATE_READ, (args) => readState(str(args['ref']), str(args['store']), str(args['path']), num(args['depth'])));
214
222
  reg.set(ReticleCommand.STORAGE_READ, (args) => readStorage(str(args['area'])));
215
223
  reg.set(ReticleCommand.CAPABILITIES, () => getCapabilities());
224
+ reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(args['fullPage'] === true));
216
225
  reg.set(ReticleCommand.SCROLL, (args) => {
217
226
  const dy = args['dy'];
218
227
  const fraction = args['fraction'];
@@ -242,4 +251,3 @@ export function createCommandRegistry() {
242
251
  });
243
252
  return reg;
244
253
  }
245
- //# sourceMappingURL=commands.js.map
@@ -1,7 +1,6 @@
1
1
  import { ElementState, type ElementDescriptor } from '@reticlehq/core';
2
2
  /** Compute the ARIA role (explicit wins, else implicit from the tag). */
3
3
  export declare function getRole(el: Element): string;
4
- /** Accessible name via a practical subset of the accname algorithm. */
5
4
  export declare function getAccessibleName(el: Element): string;
6
5
  /**
7
6
  * The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
@@ -28,4 +27,3 @@ export declare function isVisible(el: Element, memo?: Map<Element, boolean>): bo
28
27
  /** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
29
28
  * visibility cache with the query's state filter so ancestors aren't re-walked per element. */
30
29
  export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;
31
- //# sourceMappingURL=a11y.d.ts.map
package/dist/dom/a11y.js CHANGED
@@ -1,9 +1,22 @@
1
1
  import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
2
+ import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
2
3
  import { refs } from './refs.js';
3
4
  import { inspectChart } from './chart.js';
4
5
  import { isSensitiveKey } from '../security/serialization.js';
5
6
  import { formatSource, sourceFromDom } from './source.js';
6
- /** Roles whose accessible name comes from their text content. */
7
+ /**
8
+ * Roles whose accessible name comes from their text content (ARIA's `nameFrom: author content`).
9
+ *
10
+ * `radio`, `checkbox`, `row` and `tooltip` were missing, and the gap is not cosmetic: a segmented
11
+ * filter written as `<button role="radio">held</button>` — an extremely ordinary design-system
12
+ * control — reported as a nameless `radio`, so `by: role` + name could not address it at all and an
13
+ * agent had to fall back to a testid the app has no reason to carry. Measured on a shipments console:
14
+ * six filters, six nameless radios, none reachable by name.
15
+ *
16
+ * `listitem`, `status` and `alert` are NOT in the spec's list. They are kept as a deliberate
17
+ * over-approximation — a name computed from content is more useful than no name — and removing them
18
+ * has no measured symptom to justify the risk.
19
+ */
7
20
  const NAME_FROM_CONTENT = new Set([
8
21
  'button',
9
22
  'link',
@@ -11,9 +24,13 @@ const NAME_FROM_CONTENT = new Set([
11
24
  'option',
12
25
  'listitem',
13
26
  'cell',
27
+ 'checkbox',
14
28
  'columnheader',
29
+ 'radio',
30
+ 'row',
15
31
  'rowheader',
16
32
  'tab',
33
+ 'tooltip',
17
34
  'menuitem',
18
35
  'menuitemcheckbox',
19
36
  'menuitemradio',
@@ -104,12 +121,36 @@ function labelledByText(el) {
104
121
  for (const id of ids.split(/\s+/)) {
105
122
  const ref = el.ownerDocument.getElementById(id);
106
123
  if (ref !== null)
107
- parts.push(collapse(ref.textContent ?? ''));
124
+ parts.push(collapse(textWithoutHidden(ref)));
108
125
  }
109
126
  const joined = parts.join(' ').trim();
110
127
  return joined.length > 0 ? joined : null;
111
128
  }
112
129
  /** Accessible name via a practical subset of the accname algorithm. */
130
+ /** Accessible name via a practical subset of the accname algorithm. */
131
+ /**
132
+ * Text content with `aria-hidden` subtrees removed.
133
+ *
134
+ * The accessible-name spec excludes them, and — decisively — so does dom-accessibility-api, which is
135
+ * what `by: role` + name MATCHES against via testing-library. Reading raw `textContent` here made the
136
+ * name we REPORT differ from the name that can be SELECTED: Material UI renders its required-field
137
+ * marker as `<span aria-hidden="true"> *</span>`, so a login field was reported as `"Username *"` and
138
+ * addressable only as `"Username"`. Reporting a name the agent cannot use defeats the purpose of
139
+ * reporting it at all, so both must come from the same rule.
140
+ */
141
+ function textWithoutHidden(node) {
142
+ if (node.nodeType === Node.TEXT_NODE)
143
+ return node.textContent ?? '';
144
+ if (node.nodeType !== Node.ELEMENT_NODE)
145
+ return '';
146
+ const el = node;
147
+ if (el.getAttribute('aria-hidden') === 'true')
148
+ return '';
149
+ let out = '';
150
+ for (const child of el.childNodes)
151
+ out += textWithoutHidden(child);
152
+ return out;
153
+ }
113
154
  export function getAccessibleName(el) {
114
155
  const labelled = labelledByText(el);
115
156
  if (labelled !== null)
@@ -117,31 +158,29 @@ export function getAccessibleName(el) {
117
158
  const ariaLabel = el.getAttribute('aria-label');
118
159
  if (ariaLabel !== null && ariaLabel.trim().length > 0)
119
160
  return ariaLabel.trim();
120
- if (el instanceof HTMLImageElement) {
161
+ if (isImage(el)) {
121
162
  const alt = el.getAttribute('alt');
122
163
  if (alt !== null)
123
164
  return alt.trim();
124
165
  }
125
- if (el instanceof HTMLInputElement ||
126
- el instanceof HTMLTextAreaElement ||
127
- el instanceof HTMLSelectElement) {
166
+ if (isInput(el) || isTextArea(el) || isSelect(el)) {
128
167
  const labels = el.labels;
129
168
  if (labels !== null && labels.length > 0) {
130
169
  const text = [...labels]
131
- .map((l) => collapse(l.textContent ?? ''))
170
+ .map((l) => collapse(textWithoutHidden(l)))
132
171
  .join(' ')
133
172
  .trim();
134
173
  if (text.length > 0)
135
174
  return text;
136
175
  }
137
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
176
+ if (isInput(el) || isTextArea(el)) {
138
177
  const placeholder = el.getAttribute('placeholder');
139
178
  if (placeholder !== null && placeholder.trim().length > 0)
140
179
  return placeholder.trim();
141
180
  }
142
181
  }
143
182
  if (NAME_FROM_CONTENT.has(getRole(el))) {
144
- const text = collapse(el.textContent ?? '');
183
+ const text = collapse(textWithoutHidden(el));
145
184
  if (text.length > 0)
146
185
  return text;
147
186
  }
@@ -163,14 +202,10 @@ function ariaBool(el, attr) {
163
202
  export function getStates(el, visible = isVisible(el)) {
164
203
  const states = [ElementState.PRESENT];
165
204
  states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
166
- const disabledProp = (el instanceof HTMLButtonElement ||
167
- el instanceof HTMLInputElement ||
168
- el instanceof HTMLSelectElement ||
169
- el instanceof HTMLTextAreaElement) &&
170
- el.disabled;
205
+ const disabledProp = (isButton(el) || isInput(el) || isSelect(el) || isTextArea(el)) && el.disabled;
171
206
  const disabled = disabledProp || ariaBool(el, 'aria-disabled') === true;
172
207
  states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
173
- const checkedProp = el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio') && el.checked;
208
+ const checkedProp = isInput(el) && (el.type === 'checkbox' || el.type === 'radio') && el.checked;
174
209
  if (checkedProp || ariaBool(el, 'aria-checked') === true)
175
210
  states.push(ElementState.CHECKED);
176
211
  if (ariaBool(el, 'aria-expanded') === true)
@@ -186,9 +221,7 @@ export function getStates(el, visible = isVisible(el)) {
186
221
  * flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
187
222
  */
188
223
  export function isSensitiveField(el) {
189
- if (!(el instanceof HTMLInputElement) &&
190
- !(el instanceof HTMLTextAreaElement) &&
191
- !(el instanceof HTMLSelectElement)) {
224
+ if (!isInput(el) && !isTextArea(el) && !isSelect(el)) {
192
225
  return false;
193
226
  }
194
227
  const autocomplete = el.getAttribute('autocomplete') ?? '';
@@ -199,14 +232,12 @@ export function isSensitiveField(el) {
199
232
  el.getAttribute('aria-label') ?? '',
200
233
  ];
201
234
  const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
202
- return ((el instanceof HTMLInputElement && el.type.toLowerCase() === 'password') ||
235
+ return ((isInput(el) && el.type.toLowerCase() === 'password') ||
203
236
  sensitiveAutocomplete ||
204
237
  identifiers.some(isSensitiveKey));
205
238
  }
206
239
  export function getValue(el) {
207
- if (el instanceof HTMLInputElement ||
208
- el instanceof HTMLTextAreaElement ||
209
- el instanceof HTMLSelectElement) {
240
+ if (isInput(el) || isTextArea(el) || isSelect(el)) {
210
241
  if (isSensitiveField(el))
211
242
  return REDACTED_VALUE;
212
243
  return el.value;
@@ -218,7 +249,7 @@ export function getValue(el) {
218
249
  function selfHidden(el) {
219
250
  if (el.getAttribute('aria-hidden') === 'true')
220
251
  return true;
221
- if (el instanceof HTMLElement && el.hidden)
252
+ if (isHtmlElement(el) && el.hidden)
222
253
  return true;
223
254
  const view = el.ownerDocument.defaultView;
224
255
  if (view !== null) {
@@ -296,4 +327,3 @@ export function describe(el, memo) {
296
327
  }
297
328
  return base;
298
329
  }
299
- //# sourceMappingURL=a11y.js.map
@@ -63,4 +63,3 @@ interface SynthesizedAnchor {
63
63
  */
64
64
  export declare function synthesizeAnchor(input: AnchorInput): SynthesizedAnchor;
65
65
  export {};
66
- //# sourceMappingURL=auto-anchor.d.ts.map
@@ -72,4 +72,3 @@ export function synthesizeAnchor(input) {
72
72
  // Last resort: position only — addressable but never trusted.
73
73
  return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
74
74
  }
75
- //# sourceMappingURL=auto-anchor.js.map
@@ -11,4 +11,3 @@ export declare function classifyCrossOriginFrames(frames: ReadonlyArray<{
11
11
  }>): number;
12
12
  /** Probe the live document: count iframes whose document is unreachable (cross-origin). */
13
13
  export declare function countCrossOriginFrames(): number;
14
- //# sourceMappingURL=blind-spots.d.ts.map
@@ -27,4 +27,3 @@ export function countCrossOriginFrames() {
27
27
  }
28
28
  return classifyCrossOriginFrames(frames);
29
29
  }
30
- //# sourceMappingURL=blind-spots.js.map
@@ -90,4 +90,3 @@ export declare function canvasChartData(canvas: Element, globals: {
90
90
  } | undefined;
91
91
  };
92
92
  }): CanvasChartData | null;
93
- //# sourceMappingURL=chart.d.ts.map
package/dist/dom/chart.js CHANGED
@@ -116,4 +116,3 @@ export function canvasChartData(canvas, globals) {
116
116
  }
117
117
  return null;
118
118
  }
119
- //# sourceMappingURL=chart.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Desktop screenshots, asked of the runtime that owns the window.
3
+ *
4
+ * A browser tab is captured through CDP. A desktop webview has no CDP endpoint, so the pixels have
5
+ * to come from the shell — and the shell is the only party that can produce them CORRECTLY.
6
+ *
7
+ * Capturing a screen *region* instead was tried and rejected: it photographs the glass, so an app
8
+ * window sitting behind the editor yields a picture of the editor, saved as a visual baseline that a
9
+ * later diff would trust. A screenshot tool that can silently return another window's pixels is
10
+ * worse than one that returns nothing — it manufactures exactly the false green Reticle exists to
11
+ * eliminate. Electron's `webContents.capturePage()` reads the window's own backing store instead:
12
+ * correct while occluded, correct while backgrounded, and needing no screen-recording permission.
13
+ *
14
+ * Both desktop runtimes can do this, each through its own shell API — `webContents.capturePage()` on
15
+ * Electron, and on Tauri the platform's own webview snapshot (`WKWebView.takeSnapshot`, WebView2
16
+ * `CapturePreview`, or WebKitGTK's snapshot). None of them reads the screen, so all stay correct with
17
+ * the window occluded, hidden, or running headless with nothing on screen at all.
18
+ *
19
+ * The app opts in with one line in its main process (`@reticlehq/electron/main`) or one Rust command
20
+ * (`reticle-tauri`). Absent that, this returns `{ ok: false }` and the tool reports no-provider
21
+ * rather than guessing.
22
+ */
23
+ export interface CaptureResult {
24
+ ok: boolean;
25
+ /**
26
+ * Filesystem path of the captured PNG, written by the desktop shell.
27
+ *
28
+ * A path, not the image bytes: the SDK's transport sanitizer caps every string at 64KB, so a
29
+ * base64 PNG came back SILENTLY TRUNCATED and was saved as a "successful" screenshot that no
30
+ * decoder could read. The daemon and the app always share a machine here (the bridge is loopback),
31
+ * so handing over a path keeps the image off the event wire entirely.
32
+ */
33
+ path?: string;
34
+ reason?: string;
35
+ }
36
+ export declare function captureDesktopWindow(fullPage?: boolean): Promise<CaptureResult>;