@reticlehq/browser 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/actions/actions-dom.js +44 -43
  2. package/dist/actions/actions-press.d.ts +10 -4
  3. package/dist/actions/actions-press.js +108 -17
  4. package/dist/actions/actions.d.ts +2 -1
  5. package/dist/actions/actions.js +25 -26
  6. package/dist/actions/danger-context.js +1 -1
  7. package/dist/commands/commands.js +2 -6
  8. package/dist/connect-options.d.ts +1 -0
  9. package/dist/dom/a11y.js +43 -11
  10. package/dist/dom/addressing/refs.js +2 -1
  11. package/dist/dom/addressing/testid-attr.d.ts +4 -0
  12. package/dist/dom/addressing/testid-attr.js +14 -0
  13. package/dist/dom/occlusion.js +5 -1
  14. package/dist/dom/query.d.ts +1 -0
  15. package/dist/dom/query.js +42 -15
  16. package/dist/dom/snapshot.js +1 -0
  17. package/dist/observers/departure.js +51 -9
  18. package/dist/observers/dom.js +3 -2
  19. package/dist/observers/field.js +5 -3
  20. package/dist/observers/health.js +63 -2
  21. package/dist/observers/network-stream.d.ts +1 -1
  22. package/dist/observers/network-stream.js +3 -3
  23. package/dist/observers/network.js +1 -1
  24. package/dist/observers/storage.js +6 -1
  25. package/dist/presenter/carousel/carousel.d.ts +5 -2
  26. package/dist/presenter/carousel/carousel.js +34 -7
  27. package/dist/presenter/carousel/offer-card.d.ts +2 -2
  28. package/dist/presenter/carousel/offer-card.js +2 -1
  29. package/dist/presenter/carousel/panel-slides.d.ts +4 -3
  30. package/dist/presenter/carousel/panel-slides.js +43 -6
  31. package/dist/presenter/carousel/talk-card.d.ts +1 -1
  32. package/dist/presenter/chrome/log-kinds.d.ts +5 -0
  33. package/dist/presenter/chrome/log-kinds.js +1 -0
  34. package/dist/presenter/chrome/log-memory.d.ts +12 -0
  35. package/dist/presenter/chrome/log-memory.js +39 -0
  36. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
  37. package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
  38. package/dist/presenter/chrome/presenter-log.d.ts +5 -4
  39. package/dist/presenter/chrome/presenter-log.js +55 -5
  40. package/dist/presenter/chrome/presenter-report-copy.d.ts +11 -1
  41. package/dist/presenter/chrome/presenter-report-copy.js +14 -1
  42. package/dist/presenter/hud-telemetry.js +31 -2
  43. package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
  44. package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
  45. package/dist/presenter/icons/presenter-icons.d.ts +2 -0
  46. package/dist/presenter/icons/presenter-icons.js +2 -0
  47. package/dist/presenter/presenter-account.d.ts +5 -4
  48. package/dist/presenter/presenter-account.js +23 -10
  49. package/dist/presenter/presenter-chat-views.d.ts +21 -0
  50. package/dist/presenter/presenter-chat-views.js +382 -0
  51. package/dist/presenter/presenter-config.d.ts +3 -2
  52. package/dist/presenter/presenter-config.js +4 -3
  53. package/dist/presenter/presenter-controls.d.ts +4 -2
  54. package/dist/presenter/presenter-controls.js +231 -51
  55. package/dist/presenter/presenter-design-tokens.d.ts +1 -0
  56. package/dist/presenter/presenter-design-tokens.js +34 -0
  57. package/dist/presenter/presenter-dock-layout.js +1 -1
  58. package/dist/presenter/presenter-frame.d.ts +12 -0
  59. package/dist/presenter/presenter-frame.js +189 -0
  60. package/dist/presenter/presenter-plan.d.ts +10 -0
  61. package/dist/presenter/presenter-plan.js +185 -0
  62. package/dist/presenter/presenter-report-styles.js +7 -0
  63. package/dist/presenter/presenter-report.d.ts +1 -1
  64. package/dist/presenter/presenter-report.js +53 -16
  65. package/dist/presenter/presenter-settings-styles.js +10 -0
  66. package/dist/presenter/presenter-settings.d.ts +5 -0
  67. package/dist/presenter/presenter-settings.js +71 -11
  68. package/dist/presenter/presenter-shell-styles.js +39 -42
  69. package/dist/presenter/presenter-shell.d.ts +8 -1
  70. package/dist/presenter/presenter-shell.js +124 -72
  71. package/dist/presenter/presenter-styles.js +9 -1
  72. package/dist/presenter/presenter.d.ts +4 -3
  73. package/dist/presenter/presenter.js +101 -13
  74. package/dist/presenter/tour/tour.d.ts +4 -1
  75. package/dist/presenter/tour/tour.js +6 -4
  76. package/dist/recorder/recorder.js +2 -2
  77. package/dist/registry/auto-testids.js +3 -4
  78. package/dist/registry/declared-platform.js +2 -0
  79. package/dist/reticle-inject.js +880 -513
  80. package/dist/reticle.d.ts +10 -1
  81. package/dist/reticle.js +73 -9
  82. package/dist/review/annotator-styles.d.ts +1 -1
  83. package/dist/review/annotator.d.ts +7 -0
  84. package/dist/review/annotator.js +20 -2
  85. package/dist/review/mark-anchor.js +2 -2
  86. package/dist/session-continuity.js +2 -1
  87. package/dist/storage-keys.d.ts +14 -0
  88. package/dist/storage-keys.js +17 -0
  89. package/package.json +3 -2
@@ -16,12 +16,32 @@ function pointerEventFor(el, type, init) {
16
16
  ? new PointerEvent(type, full)
17
17
  : new MouseEvent(type, full);
18
18
  }
19
+ const BUTTON_HELD = 1;
20
+ const BUTTON_RELEASED = 0;
21
+ function fireAt(el, type, at, buttons, related, detail = 0) {
22
+ const init = {
23
+ bubbles: !(type.endsWith('enter') || type.endsWith('leave')),
24
+ cancelable: true,
25
+ detail,
26
+ clientX: at.x,
27
+ clientY: at.y,
28
+ screenX: at.x,
29
+ screenY: at.y,
30
+ buttons,
31
+ button: 0,
32
+ ...(related !== undefined ? { relatedTarget: related } : {}),
33
+ };
34
+ return el.dispatchEvent(type.startsWith('pointer')
35
+ ? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
36
+ : mouseEventFor(el, type, init));
37
+ }
19
38
  export async function fireClickSequence(el, hold, detail) {
20
39
  const doc = el.ownerDocument;
21
40
  const from = doc.activeElement ?? doc.body;
22
- const init = { bubbles: true, cancelable: true, detail: detail ?? 0 };
23
- firePointer(el, 'pointerdown', from);
24
- asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', init)));
41
+ const at = centreOf(el);
42
+ const d = detail ?? 0;
43
+ asSyntheticInput(() => fireAt(el, 'pointerdown', at, BUTTON_HELD, from, d));
44
+ asSyntheticInput(() => fireAt(el, 'mousedown', at, BUTTON_HELD, undefined, d));
25
45
  if (el.tabIndex >= 0 && 'function' === typeof el.focus)
26
46
  el.focus();
27
47
  let heldMs = 0;
@@ -30,9 +50,9 @@ export async function fireClickSequence(el, hold, detail) {
30
50
  await hold.sleep(hold.ms);
31
51
  heldMs = hold.now() - startedAt;
32
52
  }
33
- firePointer(el, 'pointerup', from);
34
- asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', init)));
35
- const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', init)));
53
+ asSyntheticInput(() => fireAt(el, 'pointerup', at, BUTTON_RELEASED, from, d));
54
+ asSyntheticInput(() => fireAt(el, 'mouseup', at, BUTTON_RELEASED, undefined, d));
55
+ const notPrevented = asSyntheticInput(() => fireAt(el, 'click', at, BUTTON_RELEASED, undefined, d));
36
56
  return { prevented: !notPrevented, heldMs };
37
57
  }
38
58
  function isMeasurable(rect) {
@@ -86,8 +106,6 @@ function makeDataTransfer(data) {
86
106
  }
87
107
  return dt;
88
108
  }
89
- const BUTTON_HELD = 1;
90
- const BUTTON_RELEASED = 0;
91
109
  const DRAG_STEPS = 5;
92
110
  function centreOf(el) {
93
111
  const box = el.getBoundingClientRect();
@@ -100,28 +118,11 @@ export async function dragElement(source, target, data) {
100
118
  const dest = target ?? source;
101
119
  const from = centreOf(source);
102
120
  const to = centreOf(dest);
103
- const fire = (el, type, at, buttons, related) => {
104
- const bubbles = !(type.endsWith('enter') || type.endsWith('leave'));
105
- const init = {
106
- bubbles,
107
- cancelable: true,
108
- clientX: at.x,
109
- clientY: at.y,
110
- screenX: at.x,
111
- screenY: at.y,
112
- buttons,
113
- button: 0,
114
- ...(related !== undefined ? { relatedTarget: related } : {}),
115
- };
116
- el.dispatchEvent(type.startsWith('pointer')
117
- ? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
118
- : mouseEventFor(el, type, init));
119
- };
120
121
  const sourceRect = source.getBoundingClientRect();
121
122
  const destRect = target !== null ? dest.getBoundingClientRect() : null;
122
123
  const crosses = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect.top && p.y <= rect.bottom;
123
- fire(source, 'pointerdown', from, BUTTON_HELD);
124
- fire(source, 'mousedown', from, BUTTON_HELD);
124
+ fireAt(source, 'pointerdown', from, BUTTON_HELD);
125
+ fireAt(source, 'mousedown', from, BUTTON_HELD);
125
126
  await nativeFrame();
126
127
  let leftSource = false;
127
128
  let enteredDest = false;
@@ -129,30 +130,30 @@ export async function dragElement(source, target, data) {
129
130
  const at = lerp(from, to, step / DRAG_STEPS);
130
131
  if (!leftSource && !crosses(sourceRect, at)) {
131
132
  leftSource = true;
132
- fire(source, 'pointerout', at, BUTTON_HELD, dest);
133
- fire(source, 'mouseout', at, BUTTON_HELD, dest);
134
- fire(source, 'pointerleave', at, BUTTON_HELD, dest);
135
- fire(source, 'mouseleave', at, BUTTON_HELD, dest);
133
+ fireAt(source, 'pointerout', at, BUTTON_HELD, dest);
134
+ fireAt(source, 'mouseout', at, BUTTON_HELD, dest);
135
+ fireAt(source, 'pointerleave', at, BUTTON_HELD, dest);
136
+ fireAt(source, 'mouseleave', at, BUTTON_HELD, dest);
136
137
  }
137
138
  if (leftSource && !enteredDest && destRect !== null && crosses(destRect, at)) {
138
139
  enteredDest = true;
139
- fire(dest, 'pointerover', at, BUTTON_HELD, source);
140
- fire(dest, 'mouseover', at, BUTTON_HELD, source);
141
- fire(dest, 'pointerenter', at, BUTTON_HELD, source);
142
- fire(dest, 'mouseenter', at, BUTTON_HELD, source);
140
+ fireAt(dest, 'pointerover', at, BUTTON_HELD, source);
141
+ fireAt(dest, 'mouseover', at, BUTTON_HELD, source);
142
+ fireAt(dest, 'pointerenter', at, BUTTON_HELD, source);
143
+ fireAt(dest, 'mouseenter', at, BUTTON_HELD, source);
143
144
  }
144
- fire(dest, 'pointermove', at, BUTTON_HELD);
145
- fire(dest, 'mousemove', at, BUTTON_HELD);
145
+ fireAt(dest, 'pointermove', at, BUTTON_HELD);
146
+ fireAt(dest, 'mousemove', at, BUTTON_HELD);
146
147
  await nativeFrame();
147
148
  }
148
149
  if (!enteredDest && destRect !== null) {
149
- fire(dest, 'pointerover', to, BUTTON_HELD, source);
150
- fire(dest, 'mouseover', to, BUTTON_HELD, source);
151
- fire(dest, 'pointerenter', to, BUTTON_HELD, source);
152
- fire(dest, 'mouseenter', to, BUTTON_HELD, source);
150
+ fireAt(dest, 'pointerover', to, BUTTON_HELD, source);
151
+ fireAt(dest, 'mouseover', to, BUTTON_HELD, source);
152
+ fireAt(dest, 'pointerenter', to, BUTTON_HELD, source);
153
+ fireAt(dest, 'mouseenter', to, BUTTON_HELD, source);
153
154
  }
154
- fire(dest, 'pointerup', to, BUTTON_RELEASED);
155
- fire(dest, 'mouseup', to, BUTTON_RELEASED);
155
+ fireAt(dest, 'pointerup', to, BUTTON_RELEASED);
156
+ fireAt(dest, 'mouseup', to, BUTTON_RELEASED);
156
157
  let dropPrevented = false;
157
158
  if ('function' === typeof DragEvent) {
158
159
  const dataTransfer = makeDataTransfer(data);
@@ -1,14 +1,20 @@
1
1
  import { type ActionTarget } from '../dom/realm.js';
2
2
  export declare function pressKey(args: Record<string, unknown>): string;
3
- export declare function pressModifiers(args: Record<string, unknown>): {
3
+ export interface ModifierFlags {
4
4
  metaKey: boolean;
5
5
  ctrlKey: boolean;
6
6
  shiftKey: boolean;
7
7
  altKey: boolean;
8
- };
8
+ }
9
+ export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
9
10
  export declare function pressCode(args: Record<string, unknown>, key: string): string;
10
11
  export declare function focusedOrDocument(): HTMLElement;
11
12
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
12
13
  export declare function pressKeys(args: Record<string, unknown>): string[];
13
- export declare function holdKey(el: ActionTarget, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
14
- export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
14
+ export declare const legacyKeyCode: (key: string) => number;
15
+ export declare function keyboardEvent(type: string, init: KeyboardEventInit & {
16
+ key: string;
17
+ }): KeyboardEvent;
18
+ export declare function holdKey(el: ActionTarget, key: string, code: string, mods: ModifierFlags, ms: number): Promise<void>;
19
+ export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: ModifierFlags, holdMs: number): Promise<boolean>;
20
+ export declare function closeModalOnEscape(el: ActionTarget, key: string, keydownProceeded: boolean): void;
@@ -1,4 +1,4 @@
1
- import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
1
+ import { ActionType, isGlobalPress, pressKeyFromArgs, pressKeysFromArgs } from '@reticlehq/core';
2
2
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
3
  import { nativeSetTimeout } from '../timers/native/native-timers.js';
4
4
  const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
@@ -9,16 +9,38 @@ function asString(value, fallback = '') {
9
9
  export function pressKey(args) {
10
10
  return pressKeyFromArgs(args);
11
11
  }
12
+ const MODIFIER_FLAG_KEYS = [
13
+ 'metaKey',
14
+ 'ctrlKey',
15
+ 'shiftKey',
16
+ 'altKey',
17
+ ];
18
+ const MODIFIER_FLAG_ALIASES = {
19
+ meta: 'metaKey',
20
+ cmd: 'metaKey',
21
+ command: 'metaKey',
22
+ super: 'metaKey',
23
+ win: 'metaKey',
24
+ control: 'ctrlKey',
25
+ ctrl: 'ctrlKey',
26
+ shift: 'shiftKey',
27
+ alt: 'altKey',
28
+ option: 'altKey',
29
+ opt: 'altKey',
30
+ };
12
31
  export function pressModifiers(args) {
13
32
  const raw = args['modifiers'];
14
33
  const names = Array.isArray(raw) ? raw.map((m) => asString(m).toLowerCase()) : [];
15
- const has = (...aliases) => aliases.some((a) => names.includes(a));
16
- return {
17
- metaKey: has('meta', 'cmd', 'command', 'super', 'win'),
18
- ctrlKey: has('control', 'ctrl'),
19
- shiftKey: has('shift'),
20
- altKey: has('alt', 'option', 'opt'),
21
- };
34
+ const flags = { metaKey: false, ctrlKey: false, shiftKey: false, altKey: false };
35
+ for (const name of names) {
36
+ const flag = MODIFIER_FLAG_ALIASES[name];
37
+ if (flag !== undefined)
38
+ flags[flag] = true;
39
+ }
40
+ return flags;
41
+ }
42
+ function modifierFlagFor(key) {
43
+ return MODIFIER_FLAG_ALIASES[key.toLowerCase()];
22
44
  }
23
45
  export function pressCode(args, key) {
24
46
  const explicit = args['code'];
@@ -79,19 +101,45 @@ export function isReflessDocumentPress(ref, action, args) {
79
101
  return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
80
102
  }
81
103
  export function pressKeys(args) {
82
- const raw = args['keys'];
83
- if (!Array.isArray(raw))
84
- return [];
85
- return raw.map((k) => asString(k)).filter((k) => k.length > 0);
104
+ return pressKeysFromArgs(args);
86
105
  }
87
106
  const KEY_REPEAT_MS = 33;
88
107
  const KEY_REPEAT_DELAY_MS = 500;
108
+ const LEGACY_KEY_CODES = {
109
+ Backspace: 8,
110
+ Tab: 9,
111
+ Enter: 13,
112
+ Shift: 16,
113
+ Control: 17,
114
+ Alt: 18,
115
+ Escape: 27,
116
+ ' ': 32,
117
+ PageUp: 33,
118
+ PageDown: 34,
119
+ End: 35,
120
+ Home: 36,
121
+ ArrowLeft: 37,
122
+ ArrowUp: 38,
123
+ ArrowRight: 39,
124
+ ArrowDown: 40,
125
+ Delete: 46,
126
+ Meta: 91,
127
+ };
128
+ const SINGLE_ALNUM = /^[a-z0-9]$/i;
129
+ export const legacyKeyCode = (key) => LEGACY_KEY_CODES[key] ?? (SINGLE_ALNUM.test(key) ? key.toUpperCase().charCodeAt(0) : 0);
130
+ export function keyboardEvent(type, init) {
131
+ const event = new KeyboardEvent(type, init);
132
+ const code = legacyKeyCode(init.key);
133
+ for (const legacy of ['keyCode', 'which'])
134
+ Object.defineProperty(event, legacy, { get: () => code });
135
+ return event;
136
+ }
89
137
  export async function holdKey(el, key, code, mods, ms) {
90
138
  const started = Date.now();
91
139
  if (ms > KEY_REPEAT_DELAY_MS)
92
140
  await sleep(KEY_REPEAT_DELAY_MS);
93
141
  while (Date.now() - started < ms) {
94
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
142
+ asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
95
143
  key,
96
144
  code,
97
145
  bubbles: true,
@@ -103,18 +151,61 @@ export async function holdKey(el, key, code, mods, ms) {
103
151
  }
104
152
  }
105
153
  export async function pressCombo(el, keys, mods, holdMs) {
154
+ const held = { ...mods };
155
+ const dispatch = (type, key) => {
156
+ const code = pressCode({}, key);
157
+ return asSyntheticInput(() => el.dispatchEvent(keyboardEvent(type, { key, code, bubbles: true, cancelable: true, ...held })));
158
+ };
159
+ const presses = new Map();
160
+ for (const flag of MODIFIER_FLAG_KEYS) {
161
+ if (held[flag])
162
+ presses.set(flag, 1);
163
+ }
164
+ const pressFlag = (flag) => {
165
+ presses.set(flag, (presses.get(flag) ?? 0) + 1);
166
+ held[flag] = true;
167
+ };
168
+ const releaseFlag = (flag) => {
169
+ const left = (presses.get(flag) ?? 1) - 1;
170
+ presses.set(flag, left);
171
+ if (left <= 0)
172
+ held[flag] = false;
173
+ };
106
174
  let prevented = false;
107
175
  for (const key of keys) {
108
- const code = pressCode({}, key);
109
- const ok = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
176
+ const flag = modifierFlagFor(key);
177
+ if (flag !== undefined)
178
+ pressFlag(flag);
179
+ const ok = dispatch('keydown', key);
110
180
  if (!ok)
111
181
  prevented = true;
182
+ closeModalOnEscape(el, key, ok);
112
183
  }
113
184
  if (holdMs > 0)
114
185
  await sleep(holdMs);
115
186
  for (const key of [...keys].reverse()) {
116
- const code = pressCode({}, key);
117
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
187
+ const flag = modifierFlagFor(key);
188
+ if (flag !== undefined)
189
+ releaseFlag(flag);
190
+ dispatch('keyup', key);
118
191
  }
119
192
  return prevented;
120
193
  }
194
+ export function closeModalOnEscape(el, key, keydownProceeded) {
195
+ if ('Escape' !== key || !keydownProceeded)
196
+ return;
197
+ for (let dialog = el.closest('dialog'); null !== dialog; dialog = dialog.parentElement?.closest('dialog') ?? null) {
198
+ try {
199
+ if (!dialog.open || !dialog.matches(':modal'))
200
+ continue;
201
+ }
202
+ catch {
203
+ return;
204
+ }
205
+ if ('function' === typeof dialog.requestClose)
206
+ dialog.requestClose();
207
+ else if (dialog.dispatchEvent(new Event('cancel', { cancelable: true })))
208
+ dialog.close();
209
+ return;
210
+ }
211
+ }
@@ -27,6 +27,8 @@ interface ActionResult {
27
27
  component?: string;
28
28
  role?: string;
29
29
  name?: string;
30
+ nth?: number;
31
+ of?: number;
30
32
  source?: {
31
33
  file: string;
32
34
  line: number;
@@ -36,7 +38,6 @@ interface ActionResult {
36
38
  }
37
39
  export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
38
40
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
39
- export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
40
41
  export interface ActionStep {
41
42
  ref: string;
42
43
  action: string;
@@ -1,4 +1,6 @@
1
- import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
1
+ import { readTestId } from '../dom/addressing/testid-attr.js';
2
+ import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, QueryBy, SettleReason, } from '@reticlehq/core';
3
+ import { elementsMatching } from '../dom/query.js';
2
4
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
5
  import { echoRef, refs } from '../dom/addressing/refs.js';
4
6
  import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
@@ -9,7 +11,7 @@ import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/r
9
11
  import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
10
12
  import { AppearedText } from './appeared-text.js';
11
13
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
12
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
14
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
13
15
  function asString(value, fallback = '') {
14
16
  return 'string' === typeof value ? value : fallback;
15
17
  }
@@ -22,7 +24,7 @@ function requireElement(ref) {
22
24
  return el;
23
25
  }
24
26
  function anchorOf(el) {
25
- const testid = el.getAttribute('data-testid') ?? undefined;
27
+ const testid = readTestId(el) ?? undefined;
26
28
  const info = identifyComponent(el);
27
29
  const out = {};
28
30
  if (testid !== undefined)
@@ -38,6 +40,14 @@ function anchorOf(el) {
38
40
  out.role = role;
39
41
  if (name.length > 0)
40
42
  out.name = name;
43
+ if (role.length > 0 && name.length > 0) {
44
+ const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
45
+ const at = same.indexOf(el);
46
+ if (1 < same.length && 0 <= at) {
47
+ out.nth = at;
48
+ out.of = same.length;
49
+ }
50
+ }
41
51
  return out;
42
52
  }
43
53
  const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
@@ -60,6 +70,10 @@ const result = (ref, action, effect, settled, settleReason, anchor, warning) =>
60
70
  base.role = anchor.role;
61
71
  if (anchor.name !== undefined)
62
72
  base.name = anchor.name;
73
+ if (anchor.nth !== undefined)
74
+ base.nth = anchor.nth;
75
+ if (anchor.of !== undefined)
76
+ base.of = anchor.of;
63
77
  if (warning !== undefined)
64
78
  base.warning = warning;
65
79
  return base;
@@ -137,19 +151,13 @@ function activeRef(el) {
137
151
  return null;
138
152
  return refs.refFor(active);
139
153
  }
140
- const MAX_HOLD_MS = 30_000;
141
- function clampHold(raw) {
142
- if ('number' !== typeof raw || !Number.isFinite(raw) || raw <= 0)
143
- return 0;
144
- return Math.min(raw, MAX_HOLD_MS);
145
- }
146
154
  async function dispatchFor(el, action, args) {
147
155
  if (ActionType.CLICK === action) {
148
- const hold = clampHold(args['holdMs']);
156
+ const hold = clampHoldMs(args['holdMs']);
149
157
  return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
150
158
  }
151
159
  if (ActionType.TAP === action) {
152
- const hold = clampHold(args['holdMs']);
160
+ const hold = clampHoldMs(args['holdMs']);
153
161
  return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
154
162
  }
155
163
  return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
@@ -175,7 +183,7 @@ async function dispatchOther(el, action, args) {
175
183
  el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
176
184
  firePointer(el, 'pointermove', from);
177
185
  const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
178
- const holdMs = clampHold(args['holdMs']);
186
+ const holdMs = clampHoldMs(args['holdMs']);
179
187
  if (holdMs > 0)
180
188
  await sleep(holdMs);
181
189
  return !moved;
@@ -274,14 +282,15 @@ async function dispatchOther(el, action, args) {
274
282
  const mods = pressModifiers(args);
275
283
  const combo = pressKeys(args);
276
284
  if (combo.length > 0)
277
- return await pressCombo(el, combo, mods, clampHold(args['holdMs']));
285
+ return await pressCombo(el, combo, mods, clampHoldMs(args['holdMs']));
278
286
  const key = pressKey(args);
279
287
  const code = pressCode(args, key);
280
- const down = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
281
- const hold = clampHold(args['holdMs']);
288
+ const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
289
+ closeModalOnEscape(el, key, down);
290
+ const hold = clampHoldMs(args['holdMs']);
282
291
  if (hold > 0)
283
292
  await holdKey(el, key, code, mods, hold);
284
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
293
+ asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
285
294
  return !down;
286
295
  }
287
296
  case ActionType.SCROLL_INTO_VIEW:
@@ -410,16 +419,6 @@ export async function executeAction(ref, action, args = {}) {
410
419
  return result(ref, action, effect, settled, settleReason, anchor, warning);
411
420
  }
412
421
  const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
413
- export async function dispatchWebMcp(tool, params, confirmDangerous = false) {
414
- if (requiresDangerousConfirmation(tool) && !confirmDangerous) {
415
- throw new Error(`potentially destructive WebMCP tool blocked; retry with ${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
416
- }
417
- const mc = navigator.modelContext;
418
- if (mc === undefined || typeof mc.callTool !== 'function') {
419
- throw new Error('WebMCP (navigator.modelContext) not available on this page');
420
- }
421
- return await mc.callTool(tool, params);
422
- }
423
422
  export async function executeSequence(steps) {
424
423
  const effects = [];
425
424
  const stepResults = [];
@@ -33,7 +33,7 @@ export function dangerousActionContext(el) {
33
33
  const SUBMIT_CONTROL_SELECTOR = 'button[type="submit"], input[type="submit"], button:not([type]):not([type=""])';
34
34
  export function submitControlFor(el) {
35
35
  const found = el.closest('form')?.querySelector(SUBMIT_CONTROL_SELECTOR);
36
- return found instanceof HTMLElement ? found : null;
36
+ return isHtmlElement(found) ? found : null;
37
37
  }
38
38
  export function requiresDangerousConfirmation(text, role) {
39
39
  return isDangerousActionText(text, role);
@@ -1,8 +1,8 @@
1
- import { ActionType, ComponentStateReason, DANGEROUS_ACTION_CONFIRM_ARG, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
1
+ import { ComponentStateReason, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
3
  import { paintContextOf } from '../dom/paint-context.js';
4
4
  import { matchQuery, runQuery } from '../dom/query.js';
5
- import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
5
+ import { executeAction, executeSequence } from '../actions/actions.js';
6
6
  import { describe } from '../dom/a11y.js';
7
7
  import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
8
8
  import { themeReport } from '../dom/theme.js';
@@ -198,10 +198,6 @@ export function createCommandRegistry() {
198
198
  reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state'])));
199
199
  reg.set(ReticleCommand.ACT, (args) => {
200
200
  const action = str(args['action']) ?? '';
201
- if (action === ActionType.WEBMCP) {
202
- const inner = record(args['args']);
203
- return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), true === inner[DANGEROUS_ACTION_CONFIRM_ARG]);
204
- }
205
201
  return executeAction(str(args['ref']) ?? '', action, record(args['args']));
206
202
  });
207
203
  reg.set(ReticleCommand.ACT_SEQUENCE, (args) => executeSequence((Array.isArray(args['steps']) ? args['steps'] : [])));
@@ -6,6 +6,7 @@ export interface ReticleConnectOptions {
6
6
  token?: string;
7
7
  allowNonLocalhost?: boolean;
8
8
  allowInProduction?: boolean;
9
+ testIdAttribute?: string;
9
10
  overlay?: boolean;
10
11
  captureNetworkBodies?: boolean;
11
12
  networkBodyMaxChars?: number;
package/dist/dom/a11y.js CHANGED
@@ -1,12 +1,14 @@
1
- import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
1
+ import { readTestId } from './addressing/testid-attr.js';
2
+ import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
2
3
  import { isButton, isHtmlElement, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
3
4
  import { refs } from './addressing/refs.js';
5
+ import { capturedRootOf } from './shadow-registry.js';
4
6
  import { inspectChart } from './chart.js';
5
7
  import { isSensitiveKey } from '../security/serialization.js';
6
8
  import { formatSource, sourceFromDom } from './addressing/source.js';
7
9
  const HTML_DETAILS_TAG = 'details';
8
10
  const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
9
- const HTML_DIRECT_SUMMARY_SELECTOR = ':scope > summary';
11
+ const HTML_SUMMARY_TAG = 'summary';
10
12
  const NAME_FROM_CONTENT = new Set([
11
13
  'button',
12
14
  'link',
@@ -30,9 +32,11 @@ const NAME_FROM_CONTENT = new Set([
30
32
  'status',
31
33
  'alert',
32
34
  ]);
33
- const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'search', 'password', '']);
35
+ const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'password', '']);
34
36
  function inputRole(input) {
35
37
  const type = input.type.toLowerCase();
38
+ if ('search' === type)
39
+ return 'searchbox';
36
40
  if (INPUT_TEXT_TYPES.has(type))
37
41
  return 'textbox';
38
42
  if ('checkbox' === type)
@@ -173,13 +177,19 @@ function textWithoutHidden(node) {
173
177
  const alt = el.getAttribute('alt');
174
178
  return null === alt ? '' : alt;
175
179
  }
176
- const parts = [];
180
+ let result = '';
181
+ let lastWasElement = false;
177
182
  for (const child of el.childNodes) {
178
183
  const piece = textWithoutHidden(child);
179
- if (piece.length > 0)
180
- parts.push(piece);
184
+ if (0 === piece.length)
185
+ continue;
186
+ const isEl = Node.ELEMENT_NODE === child.nodeType;
187
+ if (result.length > 0 && (isEl || lastWasElement))
188
+ result += ' ';
189
+ result += piece;
190
+ lastWasElement = isEl;
181
191
  }
182
- return parts.join(' ');
192
+ return result;
183
193
  }
184
194
  export function getAccessibleName(el) {
185
195
  const labelled = labelledByText(el);
@@ -275,7 +285,8 @@ export function isSensitiveField(el) {
275
285
  const identifiers = [
276
286
  el.getAttribute('name') ?? '',
277
287
  el.id,
278
- el.getAttribute('data-testid') ?? '',
288
+ readTestId(el) ?? '',
289
+ el.getAttribute(DEFAULT_TESTID_ATTR) ?? '',
279
290
  el.getAttribute('aria-label') ?? '',
280
291
  ];
281
292
  const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
@@ -299,8 +310,13 @@ function hiddenInsideClosedDetails(el) {
299
310
  const details = parent.closest(HTML_DETAILS_TAG);
300
311
  if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE))
301
312
  return false;
302
- const summary = details.querySelector(HTML_DIRECT_SUMMARY_SELECTOR);
303
- return null === summary || !summary.contains(el);
313
+ const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
314
+ return summary === undefined || !summary.contains(el);
315
+ }
316
+ function stalledFade(el) {
317
+ if (!el.ownerDocument.hidden || !('getAnimations' in el))
318
+ return false;
319
+ return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
304
320
  }
305
321
  function selfHidden(el) {
306
322
  if ('true' === el.getAttribute('aria-hidden'))
@@ -317,12 +333,28 @@ function selfHidden(el) {
317
333
  'collapse' === style.visibility) {
318
334
  return true;
319
335
  }
320
- if (0 === Number.parseFloat(style.opacity || '1'))
336
+ if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el))
321
337
  return true;
322
338
  }
323
339
  return false;
324
340
  }
341
+ function slotInCapturedClosedRoot(el) {
342
+ const host = el.parentElement;
343
+ if (null === host || null !== host.shadowRoot)
344
+ return null;
345
+ const root = capturedRootOf(host);
346
+ if (null === root)
347
+ return null;
348
+ for (const slot of Array.from(root.querySelectorAll('slot'))) {
349
+ if (slot.assignedElements().includes(el))
350
+ return slot;
351
+ }
352
+ return null;
353
+ }
325
354
  function parentAcrossShadowBoundary(el) {
355
+ const slot = el.assignedSlot ?? slotInCapturedClosedRoot(el);
356
+ if (null !== slot)
357
+ return slot;
326
358
  if (null !== el.parentElement)
327
359
  return el.parentElement;
328
360
  const host = el.getRootNode().host;
@@ -1,4 +1,5 @@
1
1
  import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
2
+ import { ReticleStorageKey } from '../../storage-keys.js';
2
3
  export const MAX_TRACKED_REFS = 10000;
3
4
  const REF_ELISION = '…';
4
5
  const REF_PREFIX = 'e';
@@ -7,7 +8,7 @@ export function echoRef(ref) {
7
8
  return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
8
9
  }
9
10
  const SWEEP_EVERY_MINTS = 1000;
10
- const REF_BASE_KEY = '__reticle_ref_base';
11
+ const REF_BASE_KEY = ReticleStorageKey.REF_BASE;
11
12
  export const REF_BLOCK = 100;
12
13
  function readBase(store) {
13
14
  try {
@@ -0,0 +1,4 @@
1
+ export declare function getTestIdAttr(): string;
2
+ export declare function testIdSelector(): string;
3
+ export declare function readTestId(el: Element): string | null;
4
+ export declare function setTestIdAttr(name: string | undefined): void;
@@ -0,0 +1,14 @@
1
+ import { DEFAULT_TESTID_ATTR, TESTID_ATTR_PATTERN } from '@reticlehq/core';
2
+ let testIdAttr = DEFAULT_TESTID_ATTR;
3
+ export function getTestIdAttr() {
4
+ return testIdAttr;
5
+ }
6
+ export function testIdSelector() {
7
+ return `[${testIdAttr}]`;
8
+ }
9
+ export function readTestId(el) {
10
+ return el.getAttribute(testIdAttr);
11
+ }
12
+ export function setTestIdAttr(name) {
13
+ testIdAttr = undefined !== name && TESTID_ATTR_PATTERN.test(name) ? name : DEFAULT_TESTID_ATTR;
14
+ }