@reticlehq/browser 2.8.0 → 2.9.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.
- package/NOTICE +7 -0
- package/dist/actions/actions-dom.js +23 -15
- package/dist/actions/actions.d.ts +17 -0
- package/dist/actions/actions.js +132 -29
- package/dist/actions/synthetic-input.d.ts +9 -0
- package/dist/actions/synthetic-input.js +40 -0
- package/dist/commands/commands.js +11 -1
- package/dist/connect-options.d.ts +9 -9
- package/dist/dom/a11y.d.ts +9 -0
- package/dist/dom/a11y.js +19 -0
- package/dist/dom/dom-ignore.d.ts +19 -6
- package/dist/dom/dom-ignore.js +20 -13
- package/dist/dom/query.d.ts +2 -2
- package/dist/dom/query.js +85 -45
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +34 -0
- package/dist/dom/source.d.ts +14 -0
- package/dist/dom/source.js +16 -0
- package/dist/observers/install-all.js +4 -1
- package/dist/observers/ipc.d.ts +16 -0
- package/dist/observers/ipc.js +19 -1
- package/dist/observers/network.d.ts +8 -0
- package/dist/observers/network.js +24 -3
- package/dist/observers/state.js +21 -2
- package/dist/presenter/presenter-brand.d.ts +24 -7
- package/dist/presenter/presenter-brand.js +23 -6
- package/dist/presenter/presenter-config.d.ts +102 -9
- package/dist/presenter/presenter-config.js +94 -8
- package/dist/presenter/presenter-controls.d.ts +10 -10
- package/dist/presenter/presenter-controls.js +175 -85
- package/dist/presenter/presenter-dock-layout.d.ts +6 -0
- package/dist/presenter/presenter-dock-layout.js +170 -0
- package/dist/presenter/presenter-drag.d.ts +48 -0
- package/dist/presenter/presenter-drag.js +198 -0
- package/dist/presenter/presenter-effects.d.ts +1 -1
- package/dist/presenter/presenter-effects.js +1 -1
- package/dist/presenter/presenter-glow.d.ts +5 -5
- package/dist/presenter/presenter-glow.js +7 -7
- package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
- package/dist/presenter/presenter-heroicons-data.js +38 -0
- package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
- package/dist/presenter/presenter-hud-chrome.js +41 -0
- package/dist/presenter/presenter-icons.d.ts +44 -0
- package/dist/presenter/presenter-icons.js +100 -0
- package/dist/presenter/presenter-log.d.ts +27 -5
- package/dist/presenter/presenter-log.js +114 -28
- package/dist/presenter/presenter-report-copy.d.ts +83 -0
- package/dist/presenter/presenter-report-copy.js +127 -0
- package/dist/presenter/presenter-report-styles.d.ts +7 -0
- package/dist/presenter/presenter-report-styles.js +102 -0
- package/dist/presenter/presenter-report.d.ts +23 -0
- package/dist/presenter/presenter-report.js +217 -0
- package/dist/presenter/presenter-run-state.d.ts +1 -1
- package/dist/presenter/presenter-settings-styles.d.ts +2 -0
- package/dist/presenter/presenter-settings-styles.js +153 -0
- package/dist/presenter/presenter-settings.d.ts +82 -0
- package/dist/presenter/presenter-settings.js +494 -0
- package/dist/presenter/presenter-shell-styles.d.ts +1 -0
- package/dist/presenter/presenter-shell-styles.js +375 -0
- package/dist/presenter/presenter-shell.d.ts +48 -0
- package/dist/presenter/presenter-shell.js +408 -0
- package/dist/presenter/presenter-styles.d.ts +9 -4
- package/dist/presenter/presenter-styles.js +61 -125
- package/dist/presenter/presenter-tally.d.ts +8 -11
- package/dist/presenter/presenter-tally.js +48 -23
- package/dist/presenter/presenter-test-helpers.d.ts +1 -1
- package/dist/presenter/presenter-test-helpers.js +1 -1
- package/dist/presenter/presenter-verbs.js +2 -2
- package/dist/presenter/presenter-workspace.d.ts +15 -0
- package/dist/presenter/presenter-workspace.js +180 -0
- package/dist/presenter/presenter.d.ts +15 -10
- package/dist/presenter/presenter.js +194 -46
- package/dist/reticle.d.ts +2 -2
- package/dist/reticle.js +23 -19
- package/dist/review/annotator-styles.d.ts +6 -0
- package/dist/review/annotator-styles.js +81 -0
- package/dist/review/annotator.d.ts +29 -17
- package/dist/review/annotator.js +477 -146
- package/package.json +7 -4
package/NOTICE
CHANGED
|
@@ -3,3 +3,10 @@ Copyright 2026 Reticle Labs
|
|
|
3
3
|
|
|
4
4
|
This product includes software developed at Reticle Labs.
|
|
5
5
|
Licensed under the Apache License, Version 2.0.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Heroicons (presenter HUD)
|
|
10
|
+
Copyright (c) Tailwind Labs, Inc.
|
|
11
|
+
Licensed under the MIT License.
|
|
12
|
+
https://github.com/tailwindlabs/heroicons
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { refs } from '../dom/refs.js';
|
|
2
2
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
3
3
|
import { nativeFrame } from '../timers/native-timers.js';
|
|
4
|
+
import { asSyntheticInput } from './synthetic-input.js';
|
|
4
5
|
export const NO_GEOMETRY = {
|
|
5
6
|
occluded: false,
|
|
6
7
|
occludedBy: null,
|
|
@@ -16,7 +17,7 @@ export async function fireClickSequence(el, hold) {
|
|
|
16
17
|
const doc = el.ownerDocument;
|
|
17
18
|
const from = doc.activeElement ?? doc.body;
|
|
18
19
|
firePointer(el, 'pointerdown', from);
|
|
19
|
-
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
20
|
+
asSyntheticInput(() => el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })));
|
|
20
21
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
21
22
|
el.focus();
|
|
22
23
|
// The gap that makes hold-to-confirm driveable. Everything between down and up used to be
|
|
@@ -34,8 +35,11 @@ export async function fireClickSequence(el, hold) {
|
|
|
34
35
|
heldMs = hold.now() - startedAt;
|
|
35
36
|
}
|
|
36
37
|
firePointer(el, 'pointerup', from);
|
|
37
|
-
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
38
|
-
|
|
38
|
+
asSyntheticInput(() => el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })));
|
|
39
|
+
// Marked as ours so the annotator's capture-phase listener lets it through. Only the CHECK branch
|
|
40
|
+
// used to do this, so the ordinary click action — which is this function — was swallowed whole in
|
|
41
|
+
// annotate mode while still reporting `dispatched: true`.
|
|
42
|
+
const notPrevented = asSyntheticInput(() => el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })));
|
|
39
43
|
return { prevented: !notPrevented, heldMs };
|
|
40
44
|
}
|
|
41
45
|
/** A box has layout we can reason about (jsdom returns an all-zero box — nothing to hit-test). */
|
|
@@ -78,21 +82,25 @@ export function clickGeometry(el) {
|
|
|
78
82
|
return { ...hitTest(el, rect), scrolledIntoView };
|
|
79
83
|
}
|
|
80
84
|
export function firePointer(el, type, relatedTarget = null) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
asSyntheticInput(() => {
|
|
86
|
+
if ('function' === typeof PointerEvent) {
|
|
87
|
+
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
87
93
|
}
|
|
88
94
|
/** Enter/leave pointer events are non-bubbling per spec; keep them so to avoid double-firing. */
|
|
89
95
|
export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
asSyntheticInput(() => {
|
|
97
|
+
if ('function' === typeof PointerEvent) {
|
|
98
|
+
el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
el.dispatchEvent(new MouseEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
96
104
|
}
|
|
97
105
|
function makeDataTransfer(data) {
|
|
98
106
|
if (typeof DataTransfer !== 'function')
|
|
@@ -51,6 +51,23 @@ interface ActionEffect {
|
|
|
51
51
|
* the achieved hold can legitimately be much longer than the one requested.
|
|
52
52
|
*/
|
|
53
53
|
heldMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* check/uncheck only: the control ALREADY read as the requested state, so NOTHING was dispatched.
|
|
56
|
+
* OMITTED otherwise (the uninformative default).
|
|
57
|
+
*
|
|
58
|
+
* Not clicking is right — clicking an already-checked box would uncheck it. But the branch that
|
|
59
|
+
* skips the click reads the DOM PROPERTY, and that is not evidence about what the APPLICATION
|
|
60
|
+
* holds. They come apart exactly when it matters: a default-checked input the app has not
|
|
61
|
+
* committed, a property written earlier by something else, a control read mid-render. Then `check`
|
|
62
|
+
* no-ops, the framework never hears, and every later read agrees because every later read is also
|
|
63
|
+
* reading the DOM — reported from the field as a form whose flag was unset in the database while
|
|
64
|
+
* Reticle confirmed it checked.
|
|
65
|
+
*
|
|
66
|
+
* So this is a caveat, not a failure: the requested state is what the DOM shows, and Reticle did
|
|
67
|
+
* not put it there. Assert the app's own state (a signal, a request, a derived control) rather than
|
|
68
|
+
* the box.
|
|
69
|
+
*/
|
|
70
|
+
alreadyAtValue?: true;
|
|
54
71
|
}
|
|
55
72
|
interface ActionResult {
|
|
56
73
|
ok: true;
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, isDangerousActionText, SettleReason, } from '@reticlehq/core';
|
|
1
|
+
import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, isDangerousActionText, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
|
+
import { asSyntheticInput } from './synthetic-input.js';
|
|
2
3
|
import { echoRef, refs } from '../dom/refs.js';
|
|
3
4
|
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
4
5
|
import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
@@ -95,7 +96,40 @@ const FILL_LIKE = new Set([
|
|
|
95
96
|
]);
|
|
96
97
|
const isFillLike = (action) => FILL_LIKE.has(action);
|
|
97
98
|
/** Actions that resolve to a point and so benefit from off-viewport scroll + occlusion hit-test. */
|
|
98
|
-
const CLICK_LIKE = new Set([
|
|
99
|
+
const CLICK_LIKE = new Set([
|
|
100
|
+
ActionType.CLICK,
|
|
101
|
+
ActionType.DBLCLICK,
|
|
102
|
+
// check/uncheck activate through a real click, so they need everything a click needs: the point
|
|
103
|
+
// geometry, the off-viewport scroll, the occlusion hit-test, and the component/source attribution.
|
|
104
|
+
// Their absence here is why a `check` came back with no `component` while a `click` on the same
|
|
105
|
+
// element carried one — the tell the field report noticed and could not explain.
|
|
106
|
+
ActionType.CHECK,
|
|
107
|
+
ActionType.UNCHECK,
|
|
108
|
+
]);
|
|
109
|
+
/**
|
|
110
|
+
* A check/uncheck whose requested state the element already reads as, so nothing will be dispatched.
|
|
111
|
+
*
|
|
112
|
+
* ONE predicate, read twice — the dispatch branch decides not to click, the effect reports that it
|
|
113
|
+
* did not. Two copies of this rule could disagree, and a disagreement here is precisely the false
|
|
114
|
+
* green: an act that reports it drove a control it never touched.
|
|
115
|
+
*/
|
|
116
|
+
function alreadyAtCheckedState(el, action) {
|
|
117
|
+
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
118
|
+
return false;
|
|
119
|
+
return isInput(el) && el.checked === (action === ActionType.CHECK);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* What the destructive-action guard classifies: the ELEMENT, and nothing rendered around it.
|
|
123
|
+
*
|
|
124
|
+
* This used to end with `form?.textContent`, so the whole enclosing form's rendered text decided the
|
|
125
|
+
* verdict for every control inside it — a settings form with per-row "Remove" buttons made its own
|
|
126
|
+
* Save button read as destructive, intermittently, depending on whether the rows had rendered yet.
|
|
127
|
+
* The field report's workaround was `confirmDangerous: true` on every click, which deletes the guard
|
|
128
|
+
* outright: a guard that fires on everything is a guard that gets switched off.
|
|
129
|
+
*
|
|
130
|
+
* The form's `action` stays — that is a URL this element submits to, i.e. a property of what this
|
|
131
|
+
* click DOES, not of what happens to be on screen beside it.
|
|
132
|
+
*/
|
|
99
133
|
function dangerousActionContext(el) {
|
|
100
134
|
const form = el.closest('form');
|
|
101
135
|
return [
|
|
@@ -106,7 +140,6 @@ function dangerousActionContext(el) {
|
|
|
106
140
|
el.getAttribute('aria-label') ?? '',
|
|
107
141
|
el.getAttribute('href') ?? '',
|
|
108
142
|
form?.getAttribute('action') ?? '',
|
|
109
|
-
form?.textContent ?? '',
|
|
110
143
|
].join(' ');
|
|
111
144
|
}
|
|
112
145
|
function requiresDangerousConfirmation(text) {
|
|
@@ -132,6 +165,23 @@ function pressKey(args) {
|
|
|
132
165
|
return text;
|
|
133
166
|
return asString(args['key'], 'Enter');
|
|
134
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Modifier flags for a `press`, from `args.modifiers`: an array of Meta / Control / Shift / Alt
|
|
170
|
+
* (case-insensitive, with the usual aliases). Without them a Cmd+K / Ctrl+Shift shortcut receives a
|
|
171
|
+
* keydown with every modifier false, so the app's own `event.metaKey` check never matches and
|
|
172
|
+
* nothing observable happens -- a false negative Reticle reports as no error. (#393)
|
|
173
|
+
*/
|
|
174
|
+
function pressModifiers(args) {
|
|
175
|
+
const raw = args['modifiers'];
|
|
176
|
+
const names = Array.isArray(raw) ? raw.map((m) => asString(m).toLowerCase()) : [];
|
|
177
|
+
const has = (...aliases) => aliases.some((a) => names.includes(a));
|
|
178
|
+
return {
|
|
179
|
+
metaKey: has('meta', 'cmd', 'command', 'super', 'win'),
|
|
180
|
+
ctrlKey: has('control', 'ctrl'),
|
|
181
|
+
shiftKey: has('shift'),
|
|
182
|
+
altKey: has('alt', 'option', 'opt'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
135
185
|
/**
|
|
136
186
|
* The PHYSICAL key identity — `event.code` — derived from the logical key.
|
|
137
187
|
*
|
|
@@ -207,9 +257,50 @@ function dragTargetRef(args) {
|
|
|
207
257
|
const named = asString(args['toRef']);
|
|
208
258
|
return '' !== named ? named : asString(args['target']);
|
|
209
259
|
}
|
|
260
|
+
/** The keys `upload` actually reads, and the generic ones every action accepts. */
|
|
261
|
+
const UPLOAD_ARG_KEYS = new Set(['content', 'name', 'type']);
|
|
262
|
+
const GENERIC_ACTION_ARG_KEYS = new Set([
|
|
263
|
+
DANGEROUS_ACTION_CONFIRM_ARG,
|
|
264
|
+
NATIVE_INPUT_ARG,
|
|
265
|
+
'holdMs',
|
|
266
|
+
]);
|
|
267
|
+
/**
|
|
268
|
+
* An `upload` must have been told what to upload.
|
|
269
|
+
*
|
|
270
|
+
* The branch below defaults every field, so a call whose keys were all unrecognised — the field
|
|
271
|
+
* report sent `args.files` — manufactured a 17-byte `file.txt` reading "reticle test file", uploaded
|
|
272
|
+
* THAT, and returned ok:true. The server answered 200, the UI refreshed, and every signal the agent
|
|
273
|
+
* could read said its PDF had been processed. That is a manufactured green on the write path, the
|
|
274
|
+
* same shape `drag` refuses one branch below.
|
|
275
|
+
*
|
|
276
|
+
* Both halves matter. A call with no recognised key at all never described a file; a call that mixes
|
|
277
|
+
* one in with a dropped one (`{ path, name }`) is worse, because the right filename arrives attached
|
|
278
|
+
* to invented bytes. A name-only call keeps working: the placeholder body is documented and the
|
|
279
|
+
* caller that omitted `content` knows it did.
|
|
280
|
+
*
|
|
281
|
+
* The refusal names the keys upload reads so a caller that guessed can correct in one turn.
|
|
282
|
+
*
|
|
283
|
+
* Scoped to upload deliberately. Dropping an unrecognised key in silence is a property of EVERY
|
|
284
|
+
* action — `args` is `z.record(z.unknown())` at the wire boundary — but upload is the one where the
|
|
285
|
+
* dropped key is replaced with fabricated content. Refusing unknown keys everywhere needs a
|
|
286
|
+
* per-action arg schema in core and is a larger change than this release should carry.
|
|
287
|
+
*/
|
|
288
|
+
function assertUploadArgs(args) {
|
|
289
|
+
const keys = Object.keys(args);
|
|
290
|
+
const dropped = keys.filter((k) => !UPLOAD_ARG_KEYS.has(k) && !GENERIC_ACTION_ARG_KEYS.has(k));
|
|
291
|
+
if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k)))
|
|
292
|
+
return;
|
|
293
|
+
const detail = 0 === dropped.length
|
|
294
|
+
? 'no file was described'
|
|
295
|
+
: `upload does not read ${dropped.join(', ')}, so it would be dropped`;
|
|
296
|
+
throw new Error(`upload needs the file described as args: { name, content?, type? } — ${detail}. ` +
|
|
297
|
+
'Reading a file from disk is not supported; pass its bytes as `content`.');
|
|
298
|
+
}
|
|
210
299
|
function assertActionAllowed(el, action, args) {
|
|
211
300
|
const canTrigger = action === ActionType.CLICK ||
|
|
212
301
|
action === ActionType.DBLCLICK ||
|
|
302
|
+
action === ActionType.CHECK ||
|
|
303
|
+
action === ActionType.UNCHECK ||
|
|
213
304
|
action === ActionType.DRAG ||
|
|
214
305
|
action === ActionType.SUBMIT ||
|
|
215
306
|
// Read through the SAME resolver as the dispatch below. Reading a different argument here meant
|
|
@@ -285,7 +376,7 @@ async function dispatchFor(el, action, args) {
|
|
|
285
376
|
async function dispatchOther(el, action, args) {
|
|
286
377
|
switch (action) {
|
|
287
378
|
case ActionType.DBLCLICK:
|
|
288
|
-
return !el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
|
|
379
|
+
return !asSyntheticInput(() => el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true })));
|
|
289
380
|
case ActionType.HOVER: {
|
|
290
381
|
const doc = el.ownerDocument;
|
|
291
382
|
// Best-effort "previous" node for relatedTarget so React's enter/leave synthesis has a "from".
|
|
@@ -411,30 +502,33 @@ async function dispatchOther(el, action, args) {
|
|
|
411
502
|
if (!enabledOf(el)) {
|
|
412
503
|
throw new Error(`cannot ${action} a disabled control — a real user could not, so neither will Reticle`);
|
|
413
504
|
}
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return false;
|
|
419
|
-
// `click()`, never `el.checked = …`. Assigning the property flips the pixel and tells no
|
|
420
|
-
// framework: React binds a checkbox's onChange to the CLICK event, and its value tracker
|
|
421
|
-
// dedups the change it would otherwise synthesise from a direct assignment — so a controlled
|
|
422
|
-
// `checked={state}` box never heard from us while the action reported success. `click()` is
|
|
423
|
-
// the one DOM call that runs the element's ACTIVATION BEHAVIOUR (the native toggle) as well
|
|
424
|
-
// as firing the event; `dispatchEvent` does not.
|
|
425
|
-
let prevented = false;
|
|
426
|
-
// On window, so it runs after every listener on the element itself whenever they were added.
|
|
427
|
-
const probe = (e) => {
|
|
428
|
-
prevented = e.defaultPrevented;
|
|
429
|
-
};
|
|
430
|
-
window.addEventListener('click', probe, { once: true, capture: false });
|
|
431
|
-
try {
|
|
432
|
-
el.click();
|
|
433
|
-
}
|
|
434
|
-
finally {
|
|
435
|
-
window.removeEventListener('click', probe, false);
|
|
505
|
+
// A radio is deselected by selecting another radio, never on its own. Refusing is the same
|
|
506
|
+
// rule as the disabled control above: a state no user could reach must not be forced.
|
|
507
|
+
if ('radio' === el.type && action === ActionType.UNCHECK) {
|
|
508
|
+
throw new Error('cannot uncheck a radio button — a real user could not; select another radio in the group');
|
|
436
509
|
}
|
|
437
|
-
|
|
510
|
+
// Already there: touch nothing. `check` means "end up checked", not "toggle", so clicking here
|
|
511
|
+
// would flip it OFF, and SETTING it would hand the app an `input`/`change` no user produced —
|
|
512
|
+
// enough to make an app that autosaves, logs or POSTs on change act on a no-op. Reported as
|
|
513
|
+
// `effect.alreadyAtValue` — the same predicate, read again by executeAction — because "nothing
|
|
514
|
+
// was dispatched" is not the same claim as "the app holds this value". See ActionEffect.
|
|
515
|
+
if (alreadyAtCheckedState(el, action))
|
|
516
|
+
return false;
|
|
517
|
+
// A real click, never `el.checked = …`. Assigning the property flips the pixel and tells no
|
|
518
|
+
// framework: React binds a checkbox's onChange to the CLICK event, and its value tracker dedups
|
|
519
|
+
// the change it would otherwise synthesise from a direct assignment — so a controlled
|
|
520
|
+
// `checked={state}` box never heard from us while the action reported success.
|
|
521
|
+
//
|
|
522
|
+
// Dispatched rather than `el.click()` so cancellation is read off the event object itself. The
|
|
523
|
+
// old probe listened on `window`, which never runs when a handler calls `stopPropagation()`, so
|
|
524
|
+
// a cancelled activation was reported as a successful one. Activation behaviour runs either way
|
|
525
|
+
// — a synthetic click toggles the box, which `adversarial.check.test.ts` pins, because the
|
|
526
|
+
// comment this replaces asserted the opposite and was wrong.
|
|
527
|
+
const event = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true });
|
|
528
|
+
// Marked as ours so the annotator's capture-phase listener lets it through: in annotate mode
|
|
529
|
+
// it cancels clicks to place a mark, which would otherwise swallow every action we dispatch.
|
|
530
|
+
const notPrevented = asSyntheticInput(() => el.dispatchEvent(event));
|
|
531
|
+
return !notPrevented || event.defaultPrevented;
|
|
438
532
|
}
|
|
439
533
|
case ActionType.SUBMIT: {
|
|
440
534
|
const form = isForm(el) ? el : el.closest('form');
|
|
@@ -446,8 +540,11 @@ async function dispatchOther(el, action, args) {
|
|
|
446
540
|
case ActionType.PRESS: {
|
|
447
541
|
const key = pressKey(args);
|
|
448
542
|
const code = pressCode(args, key);
|
|
449
|
-
const
|
|
450
|
-
|
|
543
|
+
const mods = pressModifiers(args);
|
|
544
|
+
// Marked as ours like the click sequence: the annotator leaves annotate mode on Escape, so an
|
|
545
|
+
// agent pressing Escape would otherwise switch off a mode the person turned on.
|
|
546
|
+
const down = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
547
|
+
asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
451
548
|
return !down;
|
|
452
549
|
}
|
|
453
550
|
case ActionType.SCROLL_INTO_VIEW:
|
|
@@ -457,6 +554,7 @@ async function dispatchOther(el, action, args) {
|
|
|
457
554
|
if (!isInput(el) || el.type !== 'file') {
|
|
458
555
|
throw new Error('upload target must be a <input type="file">');
|
|
459
556
|
}
|
|
557
|
+
assertUploadArgs(args);
|
|
460
558
|
const file = new File([asString(args['content'], 'reticle test file')], asString(args['name'], 'file.txt'), {
|
|
461
559
|
type: asString(args['type'], 'text/plain'),
|
|
462
560
|
});
|
|
@@ -504,6 +602,8 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
504
602
|
// Click-like: scroll an off-viewport target in + hit-test the click point BEFORE installing the
|
|
505
603
|
// mutation observer (scroll/hit-test never mutate the DOM, but keep the probe window clean).
|
|
506
604
|
const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
|
|
605
|
+
// Read BEFORE dispatch, from the same predicate the dispatch branch uses.
|
|
606
|
+
const alreadyAtValue = alreadyAtCheckedState(el, action);
|
|
507
607
|
let mutated = 0;
|
|
508
608
|
const said = new AppearedText();
|
|
509
609
|
const obs = new MutationObserver((records) => {
|
|
@@ -556,6 +656,9 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
556
656
|
// Omitted when there was no hold: an absent key says "this action does not hold", where a 0
|
|
557
657
|
// would read as "it held for no time", which is a different and misleading claim.
|
|
558
658
|
...(heldMs > 0 ? { heldMs } : {}),
|
|
659
|
+
// Omitted unless it applies: an absent key means the action actually drove the control (or is
|
|
660
|
+
// not a check at all), which is the only reading that must not be ambiguous.
|
|
661
|
+
...(alreadyAtValue ? { alreadyAtValue: true } : {}),
|
|
559
662
|
};
|
|
560
663
|
// Honesty caveats: a visually-occluded click is reported even though synthetic dispatch landed;
|
|
561
664
|
// else synthetic hover may not fire framework enter/leave handlers (no native hit-test).
|
|
@@ -0,0 +1,9 @@
|
|
|
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;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
|
@@ -3,7 +3,7 @@ import { buildSnapshot } from '../dom/snapshot.js';
|
|
|
3
3
|
import { matchQuery, runQuery } from '../dom/query.js';
|
|
4
4
|
import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
|
|
5
5
|
import { describe } from '../dom/a11y.js';
|
|
6
|
-
import { sourceFor, formatSource } from '../dom/source.js';
|
|
6
|
+
import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/source.js';
|
|
7
7
|
import { themeReport } from '../dom/theme.js';
|
|
8
8
|
import { echoRef, refs } from '../dom/refs.js';
|
|
9
9
|
import { isButton, isInput } from '../dom/realm.js';
|
|
@@ -91,6 +91,15 @@ function inspect(ref) {
|
|
|
91
91
|
// stamped host. `act` already did this; inspect did not, so the two tools disagreed about the same
|
|
92
92
|
// ref and inspect — the tool you reach for to ask where something lives — was the one saying null.
|
|
93
93
|
const source = formatSource(sourceFor(el, component?.source));
|
|
94
|
+
// Say WHY the source is missing rather than omitting the field and leaving the caller to guess.
|
|
95
|
+
// No stamp anywhere in the document is decisive: the loader is not running, so no element will
|
|
96
|
+
// have one this session, and the fix is a build-config change rather than anything about this
|
|
97
|
+
// element. Only computed when `source` is already absent, so the ordinary path pays nothing.
|
|
98
|
+
const sourceUnavailable = source !== undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: documentHasSourceStamps(el.ownerDocument)
|
|
101
|
+
? 'This element has no source stamp. Others on the page do, so the stamping loader is running — the nearest stamped ancestor is out of range, or this element is rendered outside instrumented code.'
|
|
102
|
+
: 'No element in this document carries a source stamp, so the stamping loader is not running: an older adapter, a bundler whose hook never ran, or a build the plugin was dropped from. Add @reticlehq/vite-plugin (or @reticlehq/babel-plugin) to the dev build and restart the dev server to get `file:line` back.';
|
|
94
103
|
const scroll = {
|
|
95
104
|
scrollTop: el.scrollTop,
|
|
96
105
|
scrollHeight: el.scrollHeight,
|
|
@@ -100,6 +109,7 @@ function inspect(ref) {
|
|
|
100
109
|
return {
|
|
101
110
|
...describe(el),
|
|
102
111
|
...(source !== undefined ? { source } : {}),
|
|
112
|
+
...(sourceUnavailable !== undefined ? { sourceUnavailable } : {}),
|
|
103
113
|
tag: el.tagName.toLowerCase(),
|
|
104
114
|
href: el.getAttribute('href') ?? undefined,
|
|
105
115
|
formAction: isButton(el) || isInput(el) ? (el.form?.getAttribute('action') ?? undefined) : undefined,
|
|
@@ -2,7 +2,7 @@ import type { RedactionConfig } from '@reticlehq/core';
|
|
|
2
2
|
/**
|
|
3
3
|
* What `reticle.connect()` accepts.
|
|
4
4
|
*
|
|
5
|
-
* Split out of reticle.ts when that file crossed the 600-line cap
|
|
5
|
+
* Split out of reticle.ts when that file crossed the 600-line cap - the same move `session-info.ts`
|
|
6
6
|
* made for the server's Session. A pure description of the SDK's public entry contract has no reason
|
|
7
7
|
* to live inside the class that happens to consume it, and separating them keeps the surface every
|
|
8
8
|
* integrating app actually reads reviewable on its own.
|
|
@@ -32,7 +32,7 @@ export interface ReticleConnectOptions {
|
|
|
32
32
|
overlay?: boolean;
|
|
33
33
|
/**
|
|
34
34
|
* Capture request/response bodies on net.request events (dev-only; text-like content only,
|
|
35
|
-
* sensitive keys redacted, per-body capped). Off by default
|
|
35
|
+
* sensitive keys redacted, per-body capped). Off by default - bodies cost tokens and can carry PII.
|
|
36
36
|
*/
|
|
37
37
|
captureNetworkBodies?: boolean;
|
|
38
38
|
/**
|
|
@@ -41,7 +41,7 @@ export interface ReticleConnectOptions {
|
|
|
41
41
|
* React's `_debugSource.fileName` is absolute; the babel stamp is repo-relative. Build plugins that
|
|
42
42
|
* can `define` a global set it themselves; frameworks that cannot (Next passes it through env,
|
|
43
43
|
* Astro through its own config) hand it here instead. Taking it as a connect OPTION is what keeps
|
|
44
|
-
* the generated app-side code plain JavaScript
|
|
44
|
+
* the generated app-side code plain JavaScript - the previous shape made the caller assign a global,
|
|
45
45
|
* and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
|
|
46
46
|
*/
|
|
47
47
|
root?: string;
|
|
@@ -66,13 +66,13 @@ export interface ReticleConnectOptions {
|
|
|
66
66
|
logMax?: number;
|
|
67
67
|
/**
|
|
68
68
|
* Mount the floating human-recorder toolbar (Record/Stop/Annotate).
|
|
69
|
-
* Default off
|
|
69
|
+
* Default off - purely additive, dev-only.
|
|
70
70
|
*/
|
|
71
71
|
recorder?: boolean;
|
|
72
72
|
/**
|
|
73
|
-
* Mount the
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Mount the page annotator: expanding the HUD enters click-to-annotate mode, and Reticle emits
|
|
74
|
+
* a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the presenter; pass
|
|
75
|
+
* `annotate: false` to suppress.
|
|
76
76
|
*/
|
|
77
77
|
annotate?: boolean;
|
|
78
78
|
/** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
|
|
@@ -91,13 +91,13 @@ export interface ReticleConnectOptions {
|
|
|
91
91
|
* });
|
|
92
92
|
* ```
|
|
93
93
|
*
|
|
94
|
-
* Additive only
|
|
94
|
+
* Additive only - there is no way to replace the built-in rule, because a config that could would
|
|
95
95
|
* eventually ship an app that leaks. `keys` strings match a key name exactly (case-insensitively);
|
|
96
96
|
* a RegExp is tested against it. `allow` exempts a key from the DEFAULT rule and loses to `keys`.
|
|
97
97
|
*
|
|
98
98
|
* Literal `keys` strings also cross the bridge, so the daemon redacts them on the driven path,
|
|
99
99
|
* where request bodies are captured raw from the network stack and never pass through this SDK.
|
|
100
|
-
* RegExp entries and `allow` do NOT cross
|
|
100
|
+
* RegExp entries and `allow` do NOT cross - see docs/usage.md ("Extending the redaction rules") for why, and what that means.
|
|
101
101
|
*/
|
|
102
102
|
redact?: RedactionConfig;
|
|
103
103
|
}
|
package/dist/dom/a11y.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export declare function getValue(el: Element): string | undefined;
|
|
|
23
23
|
* static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
|
|
24
24
|
* stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
|
|
25
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* True when the element is inside the viewport right now: visible AND its bounding box intersects
|
|
28
|
+
* the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
|
|
29
|
+
* container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
|
|
30
|
+
* "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
|
|
31
|
+
* already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
|
|
32
|
+
* the predicate pass.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
26
35
|
export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
27
36
|
/** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
|
|
28
37
|
* visibility cache with the query's state filter so ancestors aren't re-walked per element. */
|
package/dist/dom/a11y.js
CHANGED
|
@@ -272,6 +272,25 @@ function selfHidden(el) {
|
|
|
272
272
|
* static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
|
|
273
273
|
* stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
|
|
274
274
|
*/
|
|
275
|
+
/**
|
|
276
|
+
* True when the element is inside the viewport right now: visible AND its bounding box intersects
|
|
277
|
+
* the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
|
|
278
|
+
* container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
|
|
279
|
+
* "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
|
|
280
|
+
* already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
|
|
281
|
+
* the predicate pass.
|
|
282
|
+
*/
|
|
283
|
+
export function isInViewport(el, memo) {
|
|
284
|
+
if (!isVisible(el, memo))
|
|
285
|
+
return false;
|
|
286
|
+
const view = el.ownerDocument.defaultView;
|
|
287
|
+
if (null === view)
|
|
288
|
+
return false;
|
|
289
|
+
const r = el.getBoundingClientRect();
|
|
290
|
+
if (r.width <= 0 || r.height <= 0)
|
|
291
|
+
return false;
|
|
292
|
+
return r.bottom > 0 && r.right > 0 && r.top < view.innerHeight && r.left < view.innerWidth;
|
|
293
|
+
}
|
|
275
294
|
export function isVisible(el, memo) {
|
|
276
295
|
if (!el.isConnected)
|
|
277
296
|
return false;
|
package/dist/dom/dom-ignore.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Selectors for Reticle's own presenter overlay (cursor, HUD, glow) + the annotator's
|
|
3
|
-
* UI (`data-reticle-mark`)
|
|
4
|
-
* presenter, so omitting its selector here leaked
|
|
5
|
-
* snapshot and streamed its highlight-box mutations/transitions as if the APP had rendered them.
|
|
2
|
+
* Selectors for Reticle's own presenter overlay (cursor, HUD, glow) + the annotator's
|
|
3
|
+
* UI (`data-reticle-mark`) - never observed/snapshotted. The annotator mounts by DEFAULT with the
|
|
4
|
+
* presenter, so omitting its selector here leaked annotation chrome into every snapshot.
|
|
6
5
|
*/
|
|
7
|
-
export declare const RETICLE_OVERLAY
|
|
6
|
+
export declare const RETICLE_OVERLAY: string;
|
|
8
7
|
/** Let the host app add selectors to exclude from snapshots (e.g. its own dev widgets). */
|
|
9
8
|
export declare function setIgnoreSelectors(selectors: string[]): void;
|
|
10
9
|
/** True if the element is part of Reticle's own presenter overlay. */
|
|
11
10
|
export declare function isReticleOverlay(el: Element): boolean;
|
|
12
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* True iff the element is part of Reticle's OWN UI - the presenter overlay, the HUD, the synthetic
|
|
13
|
+
* cursor, the glow, or the annotator's marks - or lives inside one of them.
|
|
14
|
+
*
|
|
15
|
+
* The rule used to be "any ancestor carries a data-reticle* attribute", which is wrong twice over.
|
|
16
|
+
* `data-reticle-mark-active` sits on <html> while annotate mode is live, so the whole document
|
|
17
|
+
* answered yes; and `data-reticle-source` is stamped by the Vite/Babel plugins on every element the
|
|
18
|
+
* APP renders, so in an instrumented app - which is the only kind there is - most of the page
|
|
19
|
+
* answered yes too. Those attributes describe page content; they do not make it ours.
|
|
20
|
+
*
|
|
21
|
+
* Two things read this and both failed silently. `pageElementAt` (annotator) skipped every stamped
|
|
22
|
+
* element and anchored the note to the outermost unstamped ancestor, i.e. the app shell instead of
|
|
23
|
+
* the control under the cursor. `occlusion.ts` reads a yes as "nothing to report", so occlusion
|
|
24
|
+
* detection - a bug class Reticle advertises catching - came back clean wherever the stamp reached.
|
|
25
|
+
*/
|
|
13
26
|
export declare function isReticleUi(node: Element | null): boolean;
|
|
14
27
|
/** True if the element should be excluded from snapshots/queries (Reticle overlay or dev overlay). */
|
|
15
28
|
export declare function isIgnored(el: Element): boolean;
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Selectors for Reticle's own presenter overlay (cursor, HUD, glow) + the annotator's
|
|
3
|
-
* UI (`data-reticle-mark`)
|
|
4
|
-
* presenter, so omitting its selector here leaked
|
|
5
|
-
* snapshot and streamed its highlight-box mutations/transitions as if the APP had rendered them.
|
|
2
|
+
* Selectors for Reticle's own presenter overlay (cursor, HUD, glow) + the annotator's
|
|
3
|
+
* UI (`data-reticle-mark`) - never observed/snapshotted. The annotator mounts by DEFAULT with the
|
|
4
|
+
* presenter, so omitting its selector here leaked annotation chrome into every snapshot.
|
|
6
5
|
*/
|
|
7
|
-
export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark]';
|
|
6
|
+
export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker]';
|
|
8
7
|
/** Known third-party dev overlays to keep out of snapshots (Agentation, Next dev UI). */
|
|
9
8
|
const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
|
|
10
9
|
let extraIgnore = '';
|
|
@@ -16,15 +15,23 @@ export function setIgnoreSelectors(selectors) {
|
|
|
16
15
|
export function isReticleOverlay(el) {
|
|
17
16
|
return el.closest(RETICLE_OVERLAY) !== null;
|
|
18
17
|
}
|
|
19
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* True iff the element is part of Reticle's OWN UI - the presenter overlay, the HUD, the synthetic
|
|
20
|
+
* cursor, the glow, or the annotator's marks - or lives inside one of them.
|
|
21
|
+
*
|
|
22
|
+
* The rule used to be "any ancestor carries a data-reticle* attribute", which is wrong twice over.
|
|
23
|
+
* `data-reticle-mark-active` sits on <html> while annotate mode is live, so the whole document
|
|
24
|
+
* answered yes; and `data-reticle-source` is stamped by the Vite/Babel plugins on every element the
|
|
25
|
+
* APP renders, so in an instrumented app - which is the only kind there is - most of the page
|
|
26
|
+
* answered yes too. Those attributes describe page content; they do not make it ours.
|
|
27
|
+
*
|
|
28
|
+
* Two things read this and both failed silently. `pageElementAt` (annotator) skipped every stamped
|
|
29
|
+
* element and anchored the note to the outermost unstamped ancestor, i.e. the app shell instead of
|
|
30
|
+
* the control under the cursor. `occlusion.ts` reads a yes as "nothing to report", so occlusion
|
|
31
|
+
* detection - a bug class Reticle advertises catching - came back clean wherever the stamp reached.
|
|
32
|
+
*/
|
|
20
33
|
export function isReticleUi(node) {
|
|
21
|
-
|
|
22
|
-
for (const attr of Array.from(n.attributes)) {
|
|
23
|
-
if (attr.name.startsWith('data-reticle'))
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
34
|
+
return node !== null && node.closest(RETICLE_OVERLAY) !== null;
|
|
28
35
|
}
|
|
29
36
|
/** True if the element should be excluded from snapshots/queries (Reticle overlay or dev overlay). */
|
|
30
37
|
export function isIgnored(el) {
|
package/dist/dom/query.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare function matchQuery(query: ElementQuery, state?: ElementState, li
|
|
|
12
12
|
* Resolve a query to descriptors for the `query` MCP tool.
|
|
13
13
|
*
|
|
14
14
|
* `count` is carried through deliberately: the server reports match totals from it rather than from
|
|
15
|
-
* the array, because the array is capped in transit. Dropping it here
|
|
16
|
-
* do
|
|
15
|
+
* the array, because the array is capped in transit. Dropping it here - which this function used to
|
|
16
|
+
* do - silently put the server back to counting survivors and calling that the answer.
|
|
17
17
|
*/
|
|
18
18
|
export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
|