@orkestrel/browser 0.0.21 → 0.0.22
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/README.md +6 -3
- package/dist/src/browser/index.d.ts +77 -19
- package/dist/src/browser/index.js +361 -21
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +953 -172
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +354 -78
- package/dist/src/core/index.d.ts +354 -78
- package/dist/src/core/index.js +947 -174
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +36 -11
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +2 -2
- package/dist/src/server/index.d.ts +2 -2
- package/dist/src/server/index.js +36 -11
- package/dist/src/server/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/core/index.cjs
CHANGED
|
@@ -128,6 +128,16 @@ var BROWSER_SNAPSHOT_NODE_LIMIT = 1e5;
|
|
|
128
128
|
var BROWSER_FRAME_WORLD_NAME = "__orkestrelBrowserFrame";
|
|
129
129
|
/** Sets the number of animation frames whose element bounds must agree before trusted input. */
|
|
130
130
|
var BROWSER_STABLE_FRAME_COUNT = 2;
|
|
131
|
+
/**
|
|
132
|
+
* Names the finished transitions and animations that wake a parked wait.
|
|
133
|
+
*
|
|
134
|
+
* @remarks
|
|
135
|
+
* Chromium 154.0.4258.53 delivers these events in main and isolated worlds after a
|
|
136
|
+
* visibility or discrete display exit. Opacity alone leaves text in `innerText`.
|
|
137
|
+
*/
|
|
138
|
+
var BROWSER_WAIT_EVENTS = Object.freeze(["transitionend", "animationend"]);
|
|
139
|
+
/** Matches protocol errors caused by replacement or closure of an execution context. */
|
|
140
|
+
var BROWSER_CONTEXT_LOSS_PATTERN = /execution context was destroyed|cannot find context with specified id|inspected target navigated or closed/i;
|
|
131
141
|
/** Maps a canonical modifier name to its CDP Input modifier bit value. */
|
|
132
142
|
var BROWSER_KEY_MODIFIERS = Object.freeze({
|
|
133
143
|
Alt: 1,
|
|
@@ -153,7 +163,7 @@ var BROWSER_MOUSE_BUTTON_MASKS = Object.freeze({
|
|
|
153
163
|
*/
|
|
154
164
|
var BROWSER_HAR_CREATOR = Object.freeze({
|
|
155
165
|
name: "@orkestrel/browser",
|
|
156
|
-
version: "0.0.
|
|
166
|
+
version: "0.0.22"
|
|
157
167
|
});
|
|
158
168
|
/** Names the attribute that tags temporary screenshot styles and masks. */
|
|
159
169
|
var BROWSER_SCREENSHOT_ATTRIBUTE = "data-orkestrel-screenshot";
|
|
@@ -263,6 +273,15 @@ var BROWSER_REGISTRY_OUTPUT_LIMIT = 4096;
|
|
|
263
273
|
var BROWSER_REFERENCE_PREFIX = "e";
|
|
264
274
|
/** Bounds the default number of actionable elements in an outline. */
|
|
265
275
|
var BROWSER_OUTLINE_LIMIT = 150;
|
|
276
|
+
/**
|
|
277
|
+
* Matches one word of an outline search and of a row's role and name: a run of at least 3 letters
|
|
278
|
+
* or digits.
|
|
279
|
+
*
|
|
280
|
+
* @remarks
|
|
281
|
+
* The 3-character floor drops words such as `a`, `to`, and `of`, which would otherwise score a row
|
|
282
|
+
* whose name shares them with the search.
|
|
283
|
+
*/
|
|
284
|
+
var BROWSER_SEARCH_PATTERN = /[\p{L}\p{N}]{3,}/gu;
|
|
266
285
|
/** Names accessibility roles that receive actionable outline references. */
|
|
267
286
|
var BROWSER_INTERACTIVE_ROLES = Object.freeze(/* @__PURE__ */ new Set([
|
|
268
287
|
"button",
|
|
@@ -384,6 +403,7 @@ var BROWSER_TOOL_PENDING_NOTE = "An earlier input is still pending; call look.";
|
|
|
384
403
|
var BROWSER_OBSERVATION_TOOL_NAMES = Object.freeze([
|
|
385
404
|
"look",
|
|
386
405
|
"read",
|
|
406
|
+
"plain",
|
|
387
407
|
"tabs"
|
|
388
408
|
]);
|
|
389
409
|
/**
|
|
@@ -393,15 +413,15 @@ var BROWSER_OBSERVATION_TOOL_NAMES = Object.freeze([
|
|
|
393
413
|
*/
|
|
394
414
|
var BROWSER_TOOL_CHANGED_NOTE = "(The page changed before the view could be read; call look.)";
|
|
395
415
|
/**
|
|
396
|
-
* Holds the clause that ends the footer of a cut result
|
|
397
|
-
*
|
|
416
|
+
* Holds the clause that ends the footer of a cut result other than an action or `dialog` receipt
|
|
417
|
+
* that carries a view, including `look` and `read` results.
|
|
398
418
|
*/
|
|
399
419
|
var BROWSER_TOOL_CUT_FOOTER = "the rest was cut";
|
|
400
420
|
/**
|
|
401
|
-
* Holds the clause that ends the footer of a cut
|
|
402
|
-
*
|
|
421
|
+
* Holds the clause that ends the footer of a cut action or `dialog` receipt that carries a view,
|
|
422
|
+
* and names `look` as the call that finds an element the cut view leaves out.
|
|
403
423
|
*/
|
|
404
|
-
var BROWSER_TOOL_VIEW_FOOTER = "the rest was cut; call
|
|
424
|
+
var BROWSER_TOOL_VIEW_FOOTER = "the rest was cut; call look with words to find";
|
|
405
425
|
/**
|
|
406
426
|
* Names the accessibility roles the `type` tool writes to: `textbox`, `searchbox`, and
|
|
407
427
|
* `spinbutton` take typed text, and `combobox` and `listbox` take a select control's option
|
|
@@ -423,12 +443,13 @@ var BROWSER_TYPED_ROLES = Object.freeze(/* @__PURE__ */ new Set([
|
|
|
423
443
|
/** Caps the `wait` tool's `timeout` parameter at `30_000` milliseconds. */
|
|
424
444
|
var BROWSER_TOOL_TIMEOUT_LIMIT_MS = 3e4;
|
|
425
445
|
/**
|
|
426
|
-
* Names every tool the browser toolset reserves: `look`, `read`, `click`, `type`, `press`,
|
|
446
|
+
* Names every tool the browser toolset reserves: `look`, `read`, `plain`, `click`, `type`, `press`,
|
|
427
447
|
* `navigate`, `wait`, `dialog`, `tabs`, and `switch`.
|
|
428
448
|
*/
|
|
429
449
|
var BROWSER_TOOL_NAMES = Object.freeze([
|
|
430
450
|
"look",
|
|
431
451
|
"read",
|
|
452
|
+
"plain",
|
|
432
453
|
"click",
|
|
433
454
|
"type",
|
|
434
455
|
"press",
|
|
@@ -459,14 +480,15 @@ var BROWSER_SCHEMES = Object.freeze(["http:", "https:"]);
|
|
|
459
480
|
*
|
|
460
481
|
* @remarks
|
|
461
482
|
* Every tool description is at most 25 words and says what the tool shows or does, and for
|
|
462
|
-
* `look` and `
|
|
483
|
+
* `look`, `read`, and `plain` when to call it; every parameter description is at most 100 characters.
|
|
463
484
|
* Every tool declares at least one required parameter, because the streamed tool-call parser of
|
|
464
|
-
* Ollama 0.34.4 rejects a call to a tool that declares no parameter. `look`
|
|
465
|
-
*
|
|
466
|
-
*
|
|
485
|
+
* Ollama 0.34.4 rejects a call to a tool that declares no parameter. `look`, `read`, and `plain` take
|
|
486
|
+
* `search` and `offset`; an element's own reading is `BrowserElementInterface.read`.
|
|
487
|
+
* `look`, `read`, `plain`, and `journeys` annotate `pure` and
|
|
467
488
|
* `untrusted`, `wait` and `tabs` annotate `pure`, and the rest carry no annotation. `type` takes
|
|
468
489
|
* `secret` beside `ref`, `text`, and `submit`. The journey tools `record`, `save`, `journeys`,
|
|
469
490
|
* `edit`, and `replay` are advertised only by a toolset constructed with `journeys`.
|
|
491
|
+
* `wait` takes `absent` beside `text` and `timeout`.
|
|
470
492
|
*/
|
|
471
493
|
var BROWSER_TOOL_COPY = Object.freeze({
|
|
472
494
|
look: Object.freeze({
|
|
@@ -474,11 +496,17 @@ var BROWSER_TOOL_COPY = Object.freeze({
|
|
|
474
496
|
description: "Shows the page's text and the elements you can act on, each with a reference like e4. Call it first and after the page changes.",
|
|
475
497
|
parameters: Object.freeze({
|
|
476
498
|
type: "object",
|
|
477
|
-
properties: Object.freeze({
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
499
|
+
properties: Object.freeze({
|
|
500
|
+
search: Object.freeze({
|
|
501
|
+
type: "string",
|
|
502
|
+
description: "Words to find on this page; matching elements come first."
|
|
503
|
+
}),
|
|
504
|
+
offset: Object.freeze({
|
|
505
|
+
type: "integer",
|
|
506
|
+
description: "The character to continue from, as the last reply names. Default: 0."
|
|
507
|
+
})
|
|
508
|
+
}),
|
|
509
|
+
required: Object.freeze(["search"])
|
|
482
510
|
}),
|
|
483
511
|
annotations: Object.freeze({
|
|
484
512
|
pure: true,
|
|
@@ -487,20 +515,42 @@ var BROWSER_TOOL_COPY = Object.freeze({
|
|
|
487
515
|
}),
|
|
488
516
|
read: Object.freeze({
|
|
489
517
|
name: "read",
|
|
490
|
-
description: "Reads the page
|
|
518
|
+
description: "Reads the page as Markdown, with headings, tables, and link addresses. Call it to learn a fact; continue with the offset a cut result names.",
|
|
519
|
+
parameters: Object.freeze({
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: Object.freeze({
|
|
522
|
+
search: Object.freeze({
|
|
523
|
+
type: "string",
|
|
524
|
+
description: "Words to find on this page; the lines that share them come first."
|
|
525
|
+
}),
|
|
526
|
+
offset: Object.freeze({
|
|
527
|
+
type: "integer",
|
|
528
|
+
description: "The character to continue from, as the last reply names. Default: 0."
|
|
529
|
+
})
|
|
530
|
+
}),
|
|
531
|
+
required: Object.freeze(["search"])
|
|
532
|
+
}),
|
|
533
|
+
annotations: Object.freeze({
|
|
534
|
+
pure: true,
|
|
535
|
+
untrusted: true
|
|
536
|
+
})
|
|
537
|
+
}),
|
|
538
|
+
plain: Object.freeze({
|
|
539
|
+
name: "plain",
|
|
540
|
+
description: "Reads the page as plain text, without Markdown, link addresses, or image text. Call it for words to pass to wait or type.",
|
|
491
541
|
parameters: Object.freeze({
|
|
492
542
|
type: "object",
|
|
493
543
|
properties: Object.freeze({
|
|
494
|
-
|
|
544
|
+
search: Object.freeze({
|
|
495
545
|
type: "string",
|
|
496
|
-
description: "
|
|
546
|
+
description: "Words to find on this page; the lines that share them come first."
|
|
497
547
|
}),
|
|
498
548
|
offset: Object.freeze({
|
|
499
549
|
type: "integer",
|
|
500
550
|
description: "The character to continue from, as the last reply names. Default: 0."
|
|
501
551
|
})
|
|
502
552
|
}),
|
|
503
|
-
required: Object.freeze(["
|
|
553
|
+
required: Object.freeze(["search"])
|
|
504
554
|
}),
|
|
505
555
|
annotations: Object.freeze({
|
|
506
556
|
pure: true,
|
|
@@ -571,7 +621,7 @@ var BROWSER_TOOL_COPY = Object.freeze({
|
|
|
571
621
|
}),
|
|
572
622
|
wait: Object.freeze({
|
|
573
623
|
name: "wait",
|
|
574
|
-
description: "Waits for
|
|
624
|
+
description: "Waits for text to appear; set absent to true to wait for it to leave.",
|
|
575
625
|
parameters: Object.freeze({
|
|
576
626
|
type: "object",
|
|
577
627
|
properties: Object.freeze({
|
|
@@ -579,6 +629,10 @@ var BROWSER_TOOL_COPY = Object.freeze({
|
|
|
579
629
|
type: "string",
|
|
580
630
|
description: "The text to wait for."
|
|
581
631
|
}),
|
|
632
|
+
absent: Object.freeze({
|
|
633
|
+
type: "boolean",
|
|
634
|
+
description: "True to wait for the text to leave the page."
|
|
635
|
+
}),
|
|
582
636
|
timeout: Object.freeze({
|
|
583
637
|
type: "integer",
|
|
584
638
|
description: "The most seconds to wait, at most 30. Default: 5."
|
|
@@ -611,11 +665,11 @@ var BROWSER_TOOL_COPY = Object.freeze({
|
|
|
611
665
|
description: "Lists the open tabs; the current one is marked.",
|
|
612
666
|
parameters: Object.freeze({
|
|
613
667
|
type: "object",
|
|
614
|
-
properties: Object.freeze({
|
|
668
|
+
properties: Object.freeze({ search: Object.freeze({
|
|
615
669
|
type: "string",
|
|
616
|
-
description: "
|
|
670
|
+
description: "Words to find; matching tabs come first."
|
|
617
671
|
}) }),
|
|
618
|
-
required: Object.freeze(["
|
|
672
|
+
required: Object.freeze(["search"])
|
|
619
673
|
}),
|
|
620
674
|
annotations: Object.freeze({ pure: true })
|
|
621
675
|
}),
|
|
@@ -661,16 +715,16 @@ var BROWSER_TOOL_COPY = Object.freeze({
|
|
|
661
715
|
parameters: Object.freeze({
|
|
662
716
|
type: "object",
|
|
663
717
|
properties: Object.freeze({
|
|
664
|
-
|
|
718
|
+
search: Object.freeze({
|
|
665
719
|
type: "string",
|
|
666
|
-
description: "
|
|
720
|
+
description: "Words to find; matching journeys come first."
|
|
667
721
|
}),
|
|
668
722
|
offset: Object.freeze({
|
|
669
723
|
type: "integer",
|
|
670
724
|
description: "The character to continue from, as the last reply names. Default: 0."
|
|
671
725
|
})
|
|
672
726
|
}),
|
|
673
|
-
required: Object.freeze(["
|
|
727
|
+
required: Object.freeze(["search"])
|
|
674
728
|
}),
|
|
675
729
|
annotations: Object.freeze({
|
|
676
730
|
pure: true,
|
|
@@ -1627,14 +1681,252 @@ function filterBrowserOutline(nodes, query) {
|
|
|
1627
1681
|
return nodes.filter((node) => !node.ignored && !BROWSER_OUTLINE_OMITTED_ROLES.has(node.role ?? "") && (query.role === void 0 || node.role === query.role) && (query.name === void 0 || (query.exact === true ? normalizeBrowserName(node.name ?? "") === normalizeBrowserName(query.name) : normalizeBrowserName(node.name ?? "").toLowerCase().includes(normalizeBrowserName(query.name).toLowerCase()))));
|
|
1628
1682
|
}
|
|
1629
1683
|
/**
|
|
1684
|
+
* Renders one outline row: the node's reference, its role, its quoted accessible name, and the
|
|
1685
|
+
* states it carries.
|
|
1686
|
+
*
|
|
1687
|
+
* @remarks
|
|
1688
|
+
* After the name the row appends `value="V"` when the node carries a non-empty value,
|
|
1689
|
+
* `pressed=true|false|mixed`, `expanded=true|false`, and `selected=true|false` when present,
|
|
1690
|
+
* `[checked]` when checked, `[disabled]` when disabled, and `[tool=NAME]` for a page tool's form,
|
|
1691
|
+
* in that order. A node without a reference renders without one.
|
|
1692
|
+
*
|
|
1693
|
+
* @param node - The outline node
|
|
1694
|
+
* @returns The rendered row
|
|
1695
|
+
*
|
|
1696
|
+
* @example
|
|
1697
|
+
* ```ts
|
|
1698
|
+
* import { renderBrowserOutlineRow } from '@orkestrel/browser'
|
|
1699
|
+
*
|
|
1700
|
+
* renderBrowserOutlineRow({
|
|
1701
|
+
* id: '4',
|
|
1702
|
+
* parent: undefined,
|
|
1703
|
+
* children: [],
|
|
1704
|
+
* backend: undefined,
|
|
1705
|
+
* frame: undefined,
|
|
1706
|
+
* ignored: false,
|
|
1707
|
+
* role: 'checkbox',
|
|
1708
|
+
* name: ' Gift wrap ',
|
|
1709
|
+
* description: undefined,
|
|
1710
|
+
* value: undefined,
|
|
1711
|
+
* properties: { checked: 'true' },
|
|
1712
|
+
* session: 'main',
|
|
1713
|
+
* reference: 'e4',
|
|
1714
|
+
* }) // 'e4 checkbox "Gift wrap" [checked]'
|
|
1715
|
+
* ```
|
|
1716
|
+
*/
|
|
1717
|
+
function renderBrowserOutlineRow(node) {
|
|
1718
|
+
const name = JSON.stringify(normalizeBrowserName(node.name ?? ""));
|
|
1719
|
+
const role = node.role ?? "unknown";
|
|
1720
|
+
let row = node.reference === void 0 ? `${role} ${name}` : `${node.reference} ${role} ${name}`;
|
|
1721
|
+
if (node.value !== void 0 && node.value !== "") row += ` value=${JSON.stringify(String(node.value))}`;
|
|
1722
|
+
for (const key of [
|
|
1723
|
+
"pressed",
|
|
1724
|
+
"expanded",
|
|
1725
|
+
"selected"
|
|
1726
|
+
]) {
|
|
1727
|
+
const value = node.properties[key];
|
|
1728
|
+
if ((0, _orkestrel_contract.isString)(value) || (0, _orkestrel_contract.isBoolean)(value)) row += ` ${key}=${String(value)}`;
|
|
1729
|
+
}
|
|
1730
|
+
if (node.properties["checked"] === true || node.properties["checked"] === "true") row += " [checked]";
|
|
1731
|
+
if (node.properties["disabled"] === true) row += " [disabled]";
|
|
1732
|
+
if (node.tool !== void 0) row += ` [tool=${node.tool}]`;
|
|
1733
|
+
return row;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Returns the referenced outline nodes whose role and accessible name share the most words with a
|
|
1737
|
+
* search, in document order.
|
|
1738
|
+
*
|
|
1739
|
+
* @remarks
|
|
1740
|
+
* The search and each node's role and name are lowercased and split into whole words by
|
|
1741
|
+
* `BROWSER_SEARCH_PATTERN`, so a word shorter than 3 letters or digits never counts, and words
|
|
1742
|
+
* compare whole, with no substring or prefix match. A node scores the number of distinct search
|
|
1743
|
+
* words equal to one of its words, and every node with the highest score above 0 matches. Only a
|
|
1744
|
+
* node an outline renders as a referenced row takes part: one that is not ignored, carries a
|
|
1745
|
+
* reference, and whose role is not omitted, a heading, or text.
|
|
1746
|
+
*
|
|
1747
|
+
* @param nodes - Ordered accessibility rows
|
|
1748
|
+
* @param search - The words to match
|
|
1749
|
+
* @returns The best-matching nodes in document order; empty when no node shares a word
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* ```ts
|
|
1753
|
+
* import { scanBrowserOutline } from '@orkestrel/browser'
|
|
1754
|
+
*
|
|
1755
|
+
* const node = {
|
|
1756
|
+
* parent: undefined,
|
|
1757
|
+
* children: [],
|
|
1758
|
+
* backend: undefined,
|
|
1759
|
+
* frame: undefined,
|
|
1760
|
+
* ignored: false,
|
|
1761
|
+
* description: undefined,
|
|
1762
|
+
* value: undefined,
|
|
1763
|
+
* properties: {},
|
|
1764
|
+
* session: 'main',
|
|
1765
|
+
* }
|
|
1766
|
+
* const nodes = [
|
|
1767
|
+
* { ...node, id: '1', role: 'button', name: 'Close', reference: 'e1' },
|
|
1768
|
+
* { ...node, id: '2', role: 'button', name: 'Archive', reference: 'e2' },
|
|
1769
|
+
* ]
|
|
1770
|
+
* scanBrowserOutline(nodes, 'archive dialog button').map((match) => match.reference) // ['e2']
|
|
1771
|
+
* ```
|
|
1772
|
+
*/
|
|
1773
|
+
function scanBrowserOutline(nodes, search) {
|
|
1774
|
+
const words = collectBrowserWords(search);
|
|
1775
|
+
if (words.size === 0) return [];
|
|
1776
|
+
let best = 0;
|
|
1777
|
+
const scored = [];
|
|
1778
|
+
for (const node of nodes) {
|
|
1779
|
+
const role = node.role ?? "";
|
|
1780
|
+
if (node.ignored || node.reference === void 0 || BROWSER_OUTLINE_OMITTED_ROLES.has(role) || role === "heading" || role === "StaticText") continue;
|
|
1781
|
+
const own = collectBrowserWords(`${role} ${node.name ?? ""}`);
|
|
1782
|
+
let score = 0;
|
|
1783
|
+
for (const word of words) if (own.has(word)) score += 1;
|
|
1784
|
+
if (score === 0) continue;
|
|
1785
|
+
best = Math.max(best, score);
|
|
1786
|
+
scored.push({
|
|
1787
|
+
node,
|
|
1788
|
+
score
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
return scored.filter((entry) => entry.score === best).map((entry) => entry.node);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Collects distinct lowercase words of at least 3 letters or digits.
|
|
1795
|
+
*
|
|
1796
|
+
* @param text - Text to scan
|
|
1797
|
+
* @returns The searchable whole words
|
|
1798
|
+
*
|
|
1799
|
+
* @example
|
|
1800
|
+
* ```ts
|
|
1801
|
+
* import { collectBrowserWords } from '@orkestrel/browser'
|
|
1802
|
+
* [...collectBrowserWords('The cart, the bag')] // ['the', 'cart', 'bag']
|
|
1803
|
+
* ```
|
|
1804
|
+
*/
|
|
1805
|
+
function collectBrowserWords(text) {
|
|
1806
|
+
return new Set(Array.from(text.toLowerCase().matchAll(BROWSER_SEARCH_PATTERN), (match) => match[0]));
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Matches the lines sharing the most distinct search words, in document order.
|
|
1810
|
+
*
|
|
1811
|
+
* @param text - The complete projection
|
|
1812
|
+
* @param search - Words to match, using the outline's word rule
|
|
1813
|
+
* @returns The best-scoring lines and their original UTF-16 offsets; empty without a shared word
|
|
1814
|
+
*
|
|
1815
|
+
* @example
|
|
1816
|
+
* ```ts
|
|
1817
|
+
* import { scanBrowserText } from '@orkestrel/browser'
|
|
1818
|
+
* scanBrowserText('Cart\nBlue kettle', 'kettle') // [{ offset: 5, text: 'Blue kettle' }]
|
|
1819
|
+
* ```
|
|
1820
|
+
*/
|
|
1821
|
+
function scanBrowserText(text, search) {
|
|
1822
|
+
const words = collectBrowserWords(search);
|
|
1823
|
+
if (words.size === 0) return [];
|
|
1824
|
+
let best = 0;
|
|
1825
|
+
const scored = [];
|
|
1826
|
+
for (const line of text.matchAll(/[^\r\n]+/g)) {
|
|
1827
|
+
const own = collectBrowserWords(line[0]);
|
|
1828
|
+
let score = 0;
|
|
1829
|
+
for (const word of words) if (own.has(word)) score += 1;
|
|
1830
|
+
if (score === 0) continue;
|
|
1831
|
+
best = Math.max(best, score);
|
|
1832
|
+
scored.push({
|
|
1833
|
+
match: {
|
|
1834
|
+
offset: line.index,
|
|
1835
|
+
text: line[0]
|
|
1836
|
+
},
|
|
1837
|
+
score
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
return scored.filter((entry) => entry.score === best).map((entry) => entry.match);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Renders matching rows in at most half the available reply room.
|
|
1844
|
+
*
|
|
1845
|
+
* @remarks
|
|
1846
|
+
* An oversized first row is cut with an ellipsis without splitting a surrogate pair, reserving
|
|
1847
|
+
* room for the first later row that can fit beside it only when the cut keeps the leading token
|
|
1848
|
+
* through its first space. Otherwise it cuts without reserving. Later rows that do not fit are
|
|
1849
|
+
* skipped. An empty collection or a cut with no character before the ellipsis produces no block.
|
|
1850
|
+
*
|
|
1851
|
+
* @param heading - The match count and search, ending with a colon
|
|
1852
|
+
* @param rows - Matching rows in document order
|
|
1853
|
+
* @param room - Characters available in the reply
|
|
1854
|
+
* @returns The bounded block with a trailing blank line, or an empty string
|
|
1855
|
+
*
|
|
1856
|
+
* @example
|
|
1857
|
+
* ```ts
|
|
1858
|
+
* import { renderBrowserMatches } from '@orkestrel/browser'
|
|
1859
|
+
* renderBrowserMatches('Matches:', ['[5] Cart'], 100) // 'Matches:\n[5] Cart\n\n'
|
|
1860
|
+
* ```
|
|
1861
|
+
*/
|
|
1862
|
+
function renderBrowserMatches(heading, rows, room) {
|
|
1863
|
+
const half = Math.floor(room / 2);
|
|
1864
|
+
let text = `${heading}\n`;
|
|
1865
|
+
let count = 0;
|
|
1866
|
+
for (const row of rows) {
|
|
1867
|
+
const space = half - text.length - 2;
|
|
1868
|
+
if (space < 1) break;
|
|
1869
|
+
if (row.length > space) {
|
|
1870
|
+
if (count > 0) continue;
|
|
1871
|
+
let end = space - 1;
|
|
1872
|
+
const later = rows.slice(1).find((candidate) => candidate.length + 2 <= space);
|
|
1873
|
+
if (later !== void 0 && end - later.length - 1 >= row.indexOf(" ") + 1) end -= later.length + 1;
|
|
1874
|
+
const last = row.charCodeAt(end - 1);
|
|
1875
|
+
if (last >= 55296 && last <= 56319) end -= 1;
|
|
1876
|
+
if (end < 1) break;
|
|
1877
|
+
text += `${row.slice(0, end)}…\n`;
|
|
1878
|
+
} else text += `${row}\n`;
|
|
1879
|
+
count += 1;
|
|
1880
|
+
}
|
|
1881
|
+
return count === 0 ? "" : `${text}\n`;
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1630
1884
|
* Renders document-order text and referenced elements with a bounded element count.
|
|
1885
|
+
*
|
|
1886
|
+
* @remarks
|
|
1887
|
+
* Each referenced row renders through `renderBrowserOutlineRow`, and the closing
|
|
1888
|
+
* `(COUNT of TOTAL elements)` line counts the rows included and available. `matches` holds the
|
|
1889
|
+
* rows `scanBrowserOutline` returns for `search`, and `focus` the last referenced row whose node
|
|
1890
|
+
* carries a `focused` property of `true`; both reach past `limit`.
|
|
1891
|
+
*
|
|
1631
1892
|
* @param url - Document address
|
|
1632
1893
|
* @param title - Document title
|
|
1633
1894
|
* @param nodes - Ordered accessibility rows
|
|
1634
1895
|
* @param limit - Maximum referenced rows to include
|
|
1635
|
-
* @
|
|
1896
|
+
* @param search - The words whose best-matching rows `matches` lists. Default: none, which lists
|
|
1897
|
+
* none
|
|
1898
|
+
* @returns Outline text, element counts, matching rows, and the focused row
|
|
1899
|
+
*
|
|
1900
|
+
* @example
|
|
1901
|
+
* ```ts
|
|
1902
|
+
* import { renderBrowserOutline } from '@orkestrel/browser'
|
|
1903
|
+
*
|
|
1904
|
+
* const node = {
|
|
1905
|
+
* parent: undefined,
|
|
1906
|
+
* children: [],
|
|
1907
|
+
* backend: undefined,
|
|
1908
|
+
* frame: undefined,
|
|
1909
|
+
* ignored: false,
|
|
1910
|
+
* description: undefined,
|
|
1911
|
+
* value: undefined,
|
|
1912
|
+
* session: 'main',
|
|
1913
|
+
* }
|
|
1914
|
+
* const outline = renderBrowserOutline(
|
|
1915
|
+
* 'https://shop.example/cart',
|
|
1916
|
+
* 'Cart',
|
|
1917
|
+
* [
|
|
1918
|
+
* { ...node, id: '1', role: 'button', name: 'Close', properties: {}, reference: 'e1' },
|
|
1919
|
+
* { ...node, id: '2', role: 'button', name: 'Archive', properties: { focused: true }, reference: 'e2' },
|
|
1920
|
+
* ],
|
|
1921
|
+
* 1,
|
|
1922
|
+
* 'archive button',
|
|
1923
|
+
* )
|
|
1924
|
+
* outline.text // 'page "Cart" https://shop.example/cart\ne1 button "Close"\n(1 of 2 elements)'
|
|
1925
|
+
* outline.matches // ['e2 button "Archive"']
|
|
1926
|
+
* outline.focus // 'e2 button "Archive"'
|
|
1927
|
+
* ```
|
|
1636
1928
|
*/
|
|
1637
|
-
function renderBrowserOutline(url, title, nodes, limit) {
|
|
1929
|
+
function renderBrowserOutline(url, title, nodes, limit, search) {
|
|
1638
1930
|
const rows = [`page ${JSON.stringify(title)} ${url}`];
|
|
1639
1931
|
const sessions = /* @__PURE__ */ new Map();
|
|
1640
1932
|
for (const node of nodes) {
|
|
@@ -1647,6 +1939,7 @@ function renderBrowserOutline(url, title, nodes, limit) {
|
|
|
1647
1939
|
}
|
|
1648
1940
|
let count = 0;
|
|
1649
1941
|
let total = 0;
|
|
1942
|
+
let focus;
|
|
1650
1943
|
for (const node of nodes) {
|
|
1651
1944
|
if (node.ignored || BROWSER_OUTLINE_OMITTED_ROLES.has(node.role ?? "")) continue;
|
|
1652
1945
|
const name = normalizeBrowserName(node.name ?? "");
|
|
@@ -1661,14 +1954,10 @@ function renderBrowserOutline(url, title, nodes, limit) {
|
|
|
1661
1954
|
}
|
|
1662
1955
|
if (node.reference === void 0) continue;
|
|
1663
1956
|
total += 1;
|
|
1957
|
+
if (node.properties["focused"] === true) focus = node;
|
|
1664
1958
|
if (count >= limit) continue;
|
|
1665
1959
|
count += 1;
|
|
1666
|
-
|
|
1667
|
-
if (node.value !== void 0 && node.value !== "") row += ` value=${JSON.stringify(String(node.value))}`;
|
|
1668
|
-
if (node.properties["checked"] === true || node.properties["checked"] === "true") row += " [checked]";
|
|
1669
|
-
if (node.properties["disabled"] === true) row += " [disabled]";
|
|
1670
|
-
if (node.tool !== void 0) row += ` [tool=${node.tool}]`;
|
|
1671
|
-
rows.push(row);
|
|
1960
|
+
rows.push(renderBrowserOutlineRow(node));
|
|
1672
1961
|
}
|
|
1673
1962
|
rows.push(`(${count} of ${total} elements)`);
|
|
1674
1963
|
return {
|
|
@@ -1676,7 +1965,9 @@ function renderBrowserOutline(url, title, nodes, limit) {
|
|
|
1676
1965
|
title,
|
|
1677
1966
|
text: rows.join("\n"),
|
|
1678
1967
|
count,
|
|
1679
|
-
total
|
|
1968
|
+
total,
|
|
1969
|
+
matches: search === void 0 ? [] : scanBrowserOutline(nodes, search).map(renderBrowserOutlineRow),
|
|
1970
|
+
focus: focus === void 0 ? void 0 : renderBrowserOutlineRow(focus)
|
|
1680
1971
|
};
|
|
1681
1972
|
}
|
|
1682
1973
|
/**
|
|
@@ -1757,27 +2048,27 @@ function renderBrowserToolOutput(value) {
|
|
|
1757
2048
|
return (rendered.success ? rendered.value ?? String(value) : "[Unserializable tool output]").slice(0, BROWSER_REGISTRY_OUTPUT_LIMIT);
|
|
1758
2049
|
}
|
|
1759
2050
|
/**
|
|
1760
|
-
* Derives the advertised input schema from an authored one, adding a required `
|
|
2051
|
+
* Derives the advertised input schema from an authored one, adding a required `purpose` parameter when the schema requires nothing.
|
|
1761
2052
|
* @param schema - Authored input schema, or undefined for a parameterless tool
|
|
1762
|
-
* @returns An advertised schema, or undefined when the authored `
|
|
1763
|
-
* @remarks The caller strips only a synthetic `
|
|
2053
|
+
* @returns An advertised schema, or undefined when the authored `purpose` parameter is optional
|
|
2054
|
+
* @remarks The caller strips only a synthetic `purpose` parameter before invoking the page tool.
|
|
1764
2055
|
*/
|
|
1765
2056
|
function deriveBrowserToolSchema(schema) {
|
|
1766
2057
|
const properties = (0, _orkestrel_contract.isRecord)(schema?.["properties"]) ? schema["properties"] : {};
|
|
1767
2058
|
const required = (0, _orkestrel_contract.isArray)(schema?.["required"]) ? schema["required"].filter(_orkestrel_contract.isString) : [];
|
|
1768
|
-
if (Object.hasOwn(properties, "
|
|
2059
|
+
if (Object.hasOwn(properties, "purpose") && !required.includes("purpose")) return void 0;
|
|
1769
2060
|
if (required.length > 0) return schema;
|
|
1770
2061
|
return {
|
|
1771
2062
|
...schema,
|
|
1772
2063
|
type: "object",
|
|
1773
2064
|
properties: {
|
|
1774
2065
|
...properties,
|
|
1775
|
-
|
|
2066
|
+
purpose: {
|
|
1776
2067
|
type: "string",
|
|
1777
2068
|
description: "Describe the purpose of this action."
|
|
1778
2069
|
}
|
|
1779
2070
|
},
|
|
1780
|
-
required: ["
|
|
2071
|
+
required: ["purpose"]
|
|
1781
2072
|
};
|
|
1782
2073
|
}
|
|
1783
2074
|
/**
|
|
@@ -1789,8 +2080,8 @@ function deriveBrowserToolSchema(schema) {
|
|
|
1789
2080
|
* units, one fewer when the last would split a surrogate pair (so a limit of 1 before a pair keeps
|
|
1790
2081
|
* nothing), followed by `\n[characters 0–END of TOTAL; FOOTER]`. The footer has no default, so
|
|
1791
2082
|
* every caller states what the cut result's reader does next: the toolset passes
|
|
1792
|
-
* `BROWSER_TOOL_VIEW_FOOTER` for
|
|
1793
|
-
* any other.
|
|
2083
|
+
* `BROWSER_TOOL_VIEW_FOOTER` for an action or `dialog` receipt that carries a view and
|
|
2084
|
+
* `BROWSER_TOOL_CUT_FOOTER` for any other result, including `look` and `read` results.
|
|
1794
2085
|
*
|
|
1795
2086
|
* @param text - The page-authored or composed string
|
|
1796
2087
|
* @param limit - The most characters kept before the footer, a positive integer
|
|
@@ -1827,8 +2118,8 @@ function boundBrowserText(text, limit, footer) {
|
|
|
1827
2118
|
* ```ts
|
|
1828
2119
|
* import { BROWSER_TOOL_COPY, validateBrowserToolArguments } from '@orkestrel/browser'
|
|
1829
2120
|
*
|
|
1830
|
-
* validateBrowserToolArguments(BROWSER_TOOL_COPY.look, {
|
|
1831
|
-
* // throws 'The look tool takes no ref parameter; call look with
|
|
2121
|
+
* validateBrowserToolArguments(BROWSER_TOOL_COPY.look, { search: 'cart', ref: 'e1' })
|
|
2122
|
+
* // throws 'The look tool takes no ref parameter; call look with search and offset.'
|
|
1832
2123
|
* ```
|
|
1833
2124
|
*/
|
|
1834
2125
|
function validateBrowserToolArguments(definition, args) {
|
|
@@ -3608,7 +3899,7 @@ function renderBrowserJourney(journey) {
|
|
|
3608
3899
|
return `${step.id} type ${secret ? "(secret)" : JSON.stringify(text)}${suffix} into ${name}${step.arguments["submit"] === true ? ", submit" : ""}`;
|
|
3609
3900
|
}
|
|
3610
3901
|
case "press": return `${step.id} press ${text}${suffix}`;
|
|
3611
|
-
case "wait": return `${step.id} wait ${JSON.stringify(text)}${suffix}`;
|
|
3902
|
+
case "wait": return `${step.id} wait ${JSON.stringify(text)}${suffix}${step.arguments["absent"] === true ? ", absent" : ""}`;
|
|
3612
3903
|
case "dialog": return `${step.id} dialog ${step.arguments["accept"] === true ? "accept" : "dismiss"}${binding === void 0 ? "" : ` ${JSON.stringify(text)}${suffix}`}`;
|
|
3613
3904
|
case "switch": return `${step.id} switch ${JSON.stringify(step.tab?.title)} ${step.tab?.url}`;
|
|
3614
3905
|
case "unresolved": return `${step.id} unresolved: ${step.gap}`;
|
|
@@ -3916,46 +4207,55 @@ function buildBrowserJourney(steps, options) {
|
|
|
3916
4207
|
//#endregion
|
|
3917
4208
|
//#region src/core/compilers.ts
|
|
3918
4209
|
/**
|
|
3919
|
-
* Compiles a
|
|
4210
|
+
* Compiles a wait woken by mutations and finished transitions and animations, with one deadline and explicit disconnect ownership.
|
|
4211
|
+
* @remarks
|
|
4212
|
+
* `transitionend` and `animationend` inside a shadow root are not composed in Chromium
|
|
4213
|
+
* 154.0.4258.53, so this document listener does not see them. Mutations inside that root are
|
|
4214
|
+
* not observed either.
|
|
3920
4215
|
* @param deadline - Maximum time in milliseconds
|
|
3921
4216
|
* @param key - Isolated-world property owning this observer
|
|
3922
|
-
* @param predicate - Optional expression checked immediately and after
|
|
4217
|
+
* @param predicate - Optional expression checked immediately and on the task after a mutation batch or a `BROWSER_WAIT_EVENTS` event
|
|
3923
4218
|
* @returns Promise expression resolving true on a match or change, false at the deadline
|
|
3924
4219
|
*/
|
|
3925
4220
|
function compileQueryWaitExpression(deadline, key, predicate) {
|
|
3926
4221
|
return `new Promise((resolve) => {
|
|
3927
|
-
let
|
|
4222
|
+
let task
|
|
3928
4223
|
let timer
|
|
3929
4224
|
let observer
|
|
4225
|
+
const events = new AbortController()
|
|
3930
4226
|
const finish = (value) => {
|
|
3931
4227
|
observer?.disconnect()
|
|
3932
|
-
|
|
4228
|
+
events.abort()
|
|
4229
|
+
if (task !== undefined) clearTimeout(task)
|
|
3933
4230
|
clearTimeout(timer)
|
|
3934
4231
|
delete globalThis[${JSON.stringify(key)}]
|
|
3935
4232
|
resolve(value)
|
|
3936
4233
|
}
|
|
3937
4234
|
const check = () => {
|
|
3938
|
-
|
|
4235
|
+
task = undefined
|
|
3939
4236
|
if (${predicate ?? "true"}) finish(true)
|
|
3940
4237
|
}
|
|
3941
4238
|
globalThis[${JSON.stringify(key)}] = () => finish(false)
|
|
3942
|
-
|
|
3943
|
-
if (
|
|
3944
|
-
}
|
|
4239
|
+
const schedule = () => {
|
|
4240
|
+
if (task === undefined) task = setTimeout(check, 0)
|
|
4241
|
+
}
|
|
4242
|
+
observer = new MutationObserver(schedule)
|
|
3945
4243
|
observer.observe(document, { childList: true, attributes: true, characterData: true, subtree: true })
|
|
4244
|
+
for (const name of ${JSON.stringify(BROWSER_WAIT_EVENTS)}) document.addEventListener(name, schedule, { capture: true, signal: events.signal })
|
|
3946
4245
|
timer = setTimeout(() => finish(false), ${JSON.stringify(deadline)})
|
|
3947
4246
|
${predicate === void 0 ? "" : "check()"}
|
|
3948
4247
|
})`;
|
|
3949
4248
|
}
|
|
3950
4249
|
/**
|
|
3951
|
-
* Compiles a visible
|
|
4250
|
+
* Compiles a wait for the presence or absence of visible text, coalesced by tasks.
|
|
3952
4251
|
* @param text - Text to find in the main document body
|
|
3953
4252
|
* @param deadline - Maximum time in milliseconds
|
|
3954
4253
|
* @param key - Isolated-world property owning this observer
|
|
4254
|
+
* @param absent - If `true`, resolves when the body's `innerText` lacks `text`; if `false`, when it contains it. Default: `false`
|
|
3955
4255
|
* @returns Mutation-driven promise expression with one deadline
|
|
3956
4256
|
*/
|
|
3957
|
-
function compileTextWaitExpression(text, deadline, key) {
|
|
3958
|
-
return compileQueryWaitExpression(deadline, key,
|
|
4257
|
+
function compileTextWaitExpression(text, deadline, key, absent = false) {
|
|
4258
|
+
return compileQueryWaitExpression(deadline, key, `${absent ? "!" : ""}(document.body?.innerText ?? '').includes(${JSON.stringify(text)})`);
|
|
3959
4259
|
}
|
|
3960
4260
|
/**
|
|
3961
4261
|
* Compiles text selection or select-option assignment against the resolved element.
|
|
@@ -4267,20 +4567,337 @@ ${expression}
|
|
|
4267
4567
|
); const s = JSON.stringify(r); if (typeof s === 'string' && s.length > ${limit}) throw new Error(${JSON.stringify(BROWSER_RESULT_LIMIT_SENTINEL_PREFIX)} + s.length); return r })()`;
|
|
4268
4568
|
}
|
|
4269
4569
|
/**
|
|
4270
|
-
* Compiles the in-page function that reads the document's URL, title, and
|
|
4570
|
+
* Compiles the in-page function that reads the document's URL, title, and rendered HTML.
|
|
4271
4571
|
*
|
|
4272
4572
|
* @remarks
|
|
4273
|
-
* The
|
|
4274
|
-
*
|
|
4275
|
-
*
|
|
4276
|
-
*
|
|
4573
|
+
* The optional root defaults to `document.documentElement`. {@link BrowserReadingInput}
|
|
4574
|
+
* defines its inert capture, lowering, redaction, and limits. A missing root yields empty HTML.
|
|
4575
|
+
* Call it inside {@link compileGuardedEvaluateExpression} to bound the final serialized result
|
|
4576
|
+
* under {@link BROWSER_RESULT_LIMIT}.
|
|
4277
4577
|
*
|
|
4278
4578
|
* @returns A function declaration source
|
|
4279
4579
|
*/
|
|
4280
4580
|
function compileReadFunction() {
|
|
4281
|
-
return `function() {
|
|
4282
|
-
const
|
|
4283
|
-
return { url:
|
|
4581
|
+
return `function(node = document.documentElement) {
|
|
4582
|
+
const owner = node?.ownerDocument ?? document
|
|
4583
|
+
if (!node) return { url: owner.URL, title: owner.title, html: '' }
|
|
4584
|
+
const capture = { url: owner.URL, title: owner.title, html: '' }
|
|
4585
|
+
const view = owner.defaultView
|
|
4586
|
+
const rendered = view !== null && node.isConnected
|
|
4587
|
+
const inert = owner.implementation.createHTMLDocument('')
|
|
4588
|
+
let root = inert.importNode(node, true)
|
|
4589
|
+
const pending = [{ live: node, twin: root, visited: false }]
|
|
4590
|
+
if (rendered) {
|
|
4591
|
+
let child = node
|
|
4592
|
+
for (
|
|
4593
|
+
let parent = node;
|
|
4594
|
+
parent?.ownerDocument === owner;
|
|
4595
|
+
parent = parent.assignedSlot ?? parent.parentElement ?? (parent.parentNode instanceof view.ShadowRoot ? parent.parentNode.host : null)
|
|
4596
|
+
) {
|
|
4597
|
+
const style = view.getComputedStyle(parent)
|
|
4598
|
+
const outside =
|
|
4599
|
+
parent !== node &&
|
|
4600
|
+
(style.contentVisibility === 'hidden' ||
|
|
4601
|
+
['canvas', 'video', 'audio'].includes(parent.localName) ||
|
|
4602
|
+
parent instanceof view.HTMLInputElement ||
|
|
4603
|
+
parent instanceof view.HTMLTextAreaElement ||
|
|
4604
|
+
parent instanceof view.HTMLOptionElement ||
|
|
4605
|
+
(parent.namespaceURI === 'http://www.w3.org/2000/svg' &&
|
|
4606
|
+
parent.localName === 'switch' &&
|
|
4607
|
+
child !== (Array.from(parent.children).find((candidate) => candidate.getClientRects().length > 0) ?? parent.firstElementChild)) ||
|
|
4608
|
+
((parent instanceof view.HTMLSelectElement || parent instanceof view.HTMLOptGroupElement) &&
|
|
4609
|
+
!(node instanceof view.HTMLOptionElement || node instanceof view.HTMLOptGroupElement)) ||
|
|
4610
|
+
[
|
|
4611
|
+
'script',
|
|
4612
|
+
'style',
|
|
4613
|
+
'template',
|
|
4614
|
+
'frame',
|
|
4615
|
+
'frameset',
|
|
4616
|
+
'iframe',
|
|
4617
|
+
'object',
|
|
4618
|
+
'embed',
|
|
4619
|
+
'applet',
|
|
4620
|
+
'noscript',
|
|
4621
|
+
'meta',
|
|
4622
|
+
'link',
|
|
4623
|
+
'base',
|
|
4624
|
+
'math',
|
|
4625
|
+
].includes(parent.localName) ||
|
|
4626
|
+
(parent.localName === 'details' &&
|
|
4627
|
+
!parent.hasAttribute('open') &&
|
|
4628
|
+
child !== parent.querySelector(':scope > summary')))
|
|
4629
|
+
const unassigned =
|
|
4630
|
+
child.parentElement?.shadowRoot !== null &&
|
|
4631
|
+
child.parentElement?.shadowRoot !== undefined &&
|
|
4632
|
+
child.assignedSlot === null
|
|
4633
|
+
if (style.display === 'none' || outside || unassigned) {
|
|
4634
|
+
root = undefined
|
|
4635
|
+
pending.length = 0
|
|
4636
|
+
break
|
|
4637
|
+
}
|
|
4638
|
+
child = parent
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
while (pending.length > 0) {
|
|
4642
|
+
const entry = pending.pop()
|
|
4643
|
+
if (entry === undefined) continue
|
|
4644
|
+
const { live, twin, visited } = entry
|
|
4645
|
+
const tag = live.localName
|
|
4646
|
+
const input = tag === 'input' ? (live.getAttribute('type')?.toLowerCase() ?? 'text') : ''
|
|
4647
|
+
if (
|
|
4648
|
+
[
|
|
4649
|
+
'script',
|
|
4650
|
+
'style',
|
|
4651
|
+
'template',
|
|
4652
|
+
'frame',
|
|
4653
|
+
'frameset',
|
|
4654
|
+
'iframe',
|
|
4655
|
+
'object',
|
|
4656
|
+
'embed',
|
|
4657
|
+
'applet',
|
|
4658
|
+
'noscript',
|
|
4659
|
+
'meta',
|
|
4660
|
+
'link',
|
|
4661
|
+
'base',
|
|
4662
|
+
'math',
|
|
4663
|
+
].includes(tag) ||
|
|
4664
|
+
(tag === 'input' && ['password', 'hidden'].includes(input))
|
|
4665
|
+
) {
|
|
4666
|
+
twin.remove()
|
|
4667
|
+
if (twin === root) root = undefined
|
|
4668
|
+
continue
|
|
4669
|
+
}
|
|
4670
|
+
const style = entry.style ?? (rendered ? view.getComputedStyle(live) : undefined)
|
|
4671
|
+
const invisible = style?.visibility === 'hidden' || style?.visibility === 'collapse'
|
|
4672
|
+
if (style?.display === 'none' || (invisible && !live.hasChildNodes())) {
|
|
4673
|
+
twin.remove()
|
|
4674
|
+
if (twin === root) root = undefined
|
|
4675
|
+
continue
|
|
4676
|
+
}
|
|
4677
|
+
if (!visited) {
|
|
4678
|
+
pending.push({ live, twin, style, visited: true })
|
|
4679
|
+
if (rendered) {
|
|
4680
|
+
twin.removeAttribute('hidden')
|
|
4681
|
+
twin.removeAttribute('aria-hidden')
|
|
4682
|
+
}
|
|
4683
|
+
if (
|
|
4684
|
+
rendered &&
|
|
4685
|
+
(live instanceof view.HTMLAnchorElement || live instanceof view.HTMLAreaElement) &&
|
|
4686
|
+
live.hasAttribute('href')
|
|
4687
|
+
)
|
|
4688
|
+
twin.setAttribute('href', live.href)
|
|
4689
|
+
if (style?.contentVisibility === 'hidden' || ['canvas', 'video', 'audio'].includes(tag)) {
|
|
4690
|
+
twin.replaceChildren()
|
|
4691
|
+
continue
|
|
4692
|
+
}
|
|
4693
|
+
if (
|
|
4694
|
+
rendered &&
|
|
4695
|
+
(live instanceof view.HTMLInputElement ||
|
|
4696
|
+
live instanceof view.HTMLTextAreaElement ||
|
|
4697
|
+
live instanceof view.HTMLSelectElement ||
|
|
4698
|
+
live instanceof view.HTMLOptionElement ||
|
|
4699
|
+
live instanceof view.HTMLOptGroupElement)
|
|
4700
|
+
)
|
|
4701
|
+
continue
|
|
4702
|
+
const summary =
|
|
4703
|
+
tag === 'details' && !live.hasAttribute('open')
|
|
4704
|
+
? live.querySelector(':scope > summary')
|
|
4705
|
+
: undefined
|
|
4706
|
+
|
|
4707
|
+
// Native layout selects the switch branch after its conditional processing tests.
|
|
4708
|
+
const conditional = tag === 'switch' && live.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
4709
|
+
const branch = rendered && conditional
|
|
4710
|
+
? (Array.from(live.children).find((candidate) => candidate.getClientRects().length > 0) ?? live.firstElementChild)
|
|
4711
|
+
: undefined
|
|
4712
|
+
// Save sibling pointers before pruning invalidates the copied child collections.
|
|
4713
|
+
let element = live.lastElementChild
|
|
4714
|
+
let counterpart = twin.lastElementChild
|
|
4715
|
+
let child = live.lastChild
|
|
4716
|
+
let mirror = twin.lastChild
|
|
4717
|
+
while (child !== null && mirror !== null) {
|
|
4718
|
+
const previous = child.previousSibling
|
|
4719
|
+
const previousMirror = mirror.previousSibling
|
|
4720
|
+
const previousElement = element?.previousElementSibling ?? null
|
|
4721
|
+
const previousCounterpart = counterpart?.previousElementSibling ?? null
|
|
4722
|
+
let omitted = rendered && ((summary !== undefined && child !== summary) || (conditional && child !== branch))
|
|
4723
|
+
if (rendered && live.shadowRoot !== null) {
|
|
4724
|
+
const slot =
|
|
4725
|
+
child instanceof view.Element || child instanceof view.Text ? child.assignedSlot : null
|
|
4726
|
+
if (slot === null) omitted = true
|
|
4727
|
+
else {
|
|
4728
|
+
for (
|
|
4729
|
+
let parent = slot;
|
|
4730
|
+
parent !== null && parent !== live;
|
|
4731
|
+
parent = parent.assignedSlot ?? parent.parentElement ?? (parent.parentNode instanceof view.ShadowRoot ? parent.parentNode.host : null)
|
|
4732
|
+
) {
|
|
4733
|
+
if (view.getComputedStyle(parent).display === 'none') omitted = true
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if (omitted || (child.nodeType === 3 && invisible)) mirror.parentNode?.removeChild(mirror)
|
|
4738
|
+
else if (child.nodeType === 1) {
|
|
4739
|
+
// Element sibling pointers retain their type across windowless owner realms.
|
|
4740
|
+
if (element !== null && counterpart !== null)
|
|
4741
|
+
pending.push({ live: element, twin: counterpart, visited: false })
|
|
4742
|
+
}
|
|
4743
|
+
if (child.nodeType === 1) {
|
|
4744
|
+
element = previousElement
|
|
4745
|
+
counterpart = previousCounterpart
|
|
4746
|
+
}
|
|
4747
|
+
child = previous
|
|
4748
|
+
mirror = previousMirror
|
|
4749
|
+
}
|
|
4750
|
+
continue
|
|
4751
|
+
}
|
|
4752
|
+
if (!rendered || style?.contentVisibility === 'hidden') continue
|
|
4753
|
+
if (
|
|
4754
|
+
['div', 'summary', 'details'].includes(tag) &&
|
|
4755
|
+
!style?.display.startsWith('inline') &&
|
|
4756
|
+
style?.display !== 'contents' &&
|
|
4757
|
+
twin.hasChildNodes()
|
|
4758
|
+
) {
|
|
4759
|
+
twin.prepend(inert.createElement('br'))
|
|
4760
|
+
twin.append(inert.createElement('br'))
|
|
4761
|
+
}
|
|
4762
|
+
let fragments
|
|
4763
|
+
let lines
|
|
4764
|
+
let container = false
|
|
4765
|
+
if (live instanceof view.HTMLInputElement) {
|
|
4766
|
+
lines = []
|
|
4767
|
+
if (['text', 'email', 'search', 'tel', 'url', 'number'].includes(live.type)) {
|
|
4768
|
+
if (live.type !== 'number' || !live.validity.badInput) {
|
|
4769
|
+
const placeholder = view.getComputedStyle(live, '::placeholder')
|
|
4770
|
+
lines = [
|
|
4771
|
+
live.value ||
|
|
4772
|
+
(live.matches(':placeholder-shown') &&
|
|
4773
|
+
placeholder.visibility === 'visible' &&
|
|
4774
|
+
placeholder.opacity !== '0' &&
|
|
4775
|
+
placeholder.color !== 'rgba(0, 0, 0, 0)'
|
|
4776
|
+
? live.placeholder
|
|
4777
|
+
: ''),
|
|
4778
|
+
]
|
|
4779
|
+
}
|
|
4780
|
+
} else if (['submit', 'reset', 'button'].includes(live.type)) lines = [live.value]
|
|
4781
|
+
else if (live.type === 'file' && live.files?.length === 1)
|
|
4782
|
+
lines = Array.from(live.files ?? [], (file) => file.name)
|
|
4783
|
+
else if (live.type === 'image' && live.getAttribute('aria-hidden') !== 'true')
|
|
4784
|
+
lines = [live.getAttribute('aria-label')?.trim() || live.alt]
|
|
4785
|
+
} else if (live instanceof view.HTMLTextAreaElement) {
|
|
4786
|
+
const placeholder = view.getComputedStyle(live, '::placeholder')
|
|
4787
|
+
const value =
|
|
4788
|
+
live.value ||
|
|
4789
|
+
(live.matches(':placeholder-shown') &&
|
|
4790
|
+
placeholder.visibility === 'visible' &&
|
|
4791
|
+
placeholder.opacity !== '0' &&
|
|
4792
|
+
placeholder.color !== 'rgba(0, 0, 0, 0)'
|
|
4793
|
+
? live.placeholder
|
|
4794
|
+
: '')
|
|
4795
|
+
lines = value.split(/\\r?\\n/)
|
|
4796
|
+
} else if (
|
|
4797
|
+
live instanceof view.HTMLSelectElement ||
|
|
4798
|
+
live instanceof view.HTMLOptionElement ||
|
|
4799
|
+
live instanceof view.HTMLOptGroupElement
|
|
4800
|
+
) {
|
|
4801
|
+
lines = []
|
|
4802
|
+
const select = live instanceof view.HTMLSelectElement ? live : live.closest('select')
|
|
4803
|
+
if (select !== null) {
|
|
4804
|
+
if (!select.multiple && select.size <= 1) {
|
|
4805
|
+
const option = select.selectedOptions[0]
|
|
4806
|
+
if (option !== undefined && (live === select || live === option || live.contains(option)))
|
|
4807
|
+
lines.push(option.label)
|
|
4808
|
+
} else {
|
|
4809
|
+
const box = select.getBoundingClientRect()
|
|
4810
|
+
const top = box.top + select.clientTop
|
|
4811
|
+
const bottom = top + select.clientHeight
|
|
4812
|
+
const rows =
|
|
4813
|
+
live === select
|
|
4814
|
+
? select.querySelectorAll('option,optgroup')
|
|
4815
|
+
: [live, ...live.querySelectorAll('option')]
|
|
4816
|
+
for (const row of rows) {
|
|
4817
|
+
const rowStyle = view.getComputedStyle(row)
|
|
4818
|
+
const groupStyle =
|
|
4819
|
+
row.parentElement instanceof view.HTMLOptGroupElement
|
|
4820
|
+
? view.getComputedStyle(row.parentElement)
|
|
4821
|
+
: undefined
|
|
4822
|
+
if (
|
|
4823
|
+
rowStyle.display === 'none' ||
|
|
4824
|
+
rowStyle.visibility !== 'visible' ||
|
|
4825
|
+
groupStyle?.display === 'none'
|
|
4826
|
+
)
|
|
4827
|
+
continue
|
|
4828
|
+
const rect = row.getBoundingClientRect()
|
|
4829
|
+
// The group box includes its options; its caption ends where the first row starts.
|
|
4830
|
+
const end =
|
|
4831
|
+
row instanceof view.HTMLOptGroupElement
|
|
4832
|
+
? (Array.from(row.querySelectorAll('option'))
|
|
4833
|
+
.find((option) => option.getClientRects().length > 0)
|
|
4834
|
+
?.getBoundingClientRect().top ?? rect.bottom)
|
|
4835
|
+
: rect.bottom
|
|
4836
|
+
if (
|
|
4837
|
+
end > top &&
|
|
4838
|
+
rect.top < bottom &&
|
|
4839
|
+
rect.right > box.left + select.clientLeft &&
|
|
4840
|
+
rect.left < box.left + select.clientLeft + select.clientWidth &&
|
|
4841
|
+
(row instanceof view.HTMLOptionElement || row instanceof view.HTMLOptGroupElement)
|
|
4842
|
+
)
|
|
4843
|
+
lines.push(row.label)
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
} else if (tag === 'svg') {
|
|
4848
|
+
if (twin.parentElement?.closest('svg, foreignObject')?.localName === 'svg') continue
|
|
4849
|
+
lines = []
|
|
4850
|
+
fragments = []
|
|
4851
|
+
for (const content of twin.querySelectorAll('text, foreignObject')) {
|
|
4852
|
+
if (content.parentElement?.closest('svg, foreignObject')?.localName === 'foreignObject') continue
|
|
4853
|
+
if (content.localName === 'text' && content.parentElement?.closest('text')?.closest('svg') === content.closest('svg')) continue
|
|
4854
|
+
if (content.localName === 'foreignObject') fragments.push(...content.childNodes)
|
|
4855
|
+
else if (content.textContent) fragments.push(inert.createTextNode(content.textContent))
|
|
4856
|
+
}
|
|
4857
|
+
if (fragments.length === 0) fragments = undefined
|
|
4858
|
+
if (!invisible && fragments === undefined && live.getAttribute('aria-hidden') !== 'true')
|
|
4859
|
+
lines.push(
|
|
4860
|
+
live.getAttribute('aria-label')?.trim() ||
|
|
4861
|
+
live.querySelector(':scope > title')?.textContent ||
|
|
4862
|
+
'',
|
|
4863
|
+
)
|
|
4864
|
+
} else if (['form', 'dialog', 'button'].includes(tag)) {
|
|
4865
|
+
container = true
|
|
4866
|
+
if (live instanceof view.HTMLButtonElement && !invisible && live.innerText.trim() === '') {
|
|
4867
|
+
const name = live.getAttribute('aria-label')?.trim()
|
|
4868
|
+
if (name) {
|
|
4869
|
+
lines = [name]
|
|
4870
|
+
container = false
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
if (lines === undefined && !container) continue
|
|
4875
|
+
if (invisible && !container && tag !== 'svg') lines = []
|
|
4876
|
+
const block = !style?.display.startsWith('inline') && style?.display !== 'contents'
|
|
4877
|
+
const carrier = inert.createElement(block ? 'div' : 'span')
|
|
4878
|
+
if (container) carrier.append(...twin.childNodes)
|
|
4879
|
+
else if (fragments !== undefined) {
|
|
4880
|
+
for (const fragment of fragments) {
|
|
4881
|
+
carrier.append(fragment, inert.createElement('br'))
|
|
4882
|
+
}
|
|
4883
|
+
} else
|
|
4884
|
+
for (const [index, line] of (lines ?? []).entries()) {
|
|
4885
|
+
if (index > 0) carrier.append(inert.createElement('br'))
|
|
4886
|
+
carrier.append(inert.createTextNode(line))
|
|
4887
|
+
}
|
|
4888
|
+
if (carrier.hasChildNodes()) {
|
|
4889
|
+
carrier.prepend(inert.createElement('br'))
|
|
4890
|
+
carrier.append(inert.createElement('br'))
|
|
4891
|
+
}
|
|
4892
|
+
twin.replaceWith(carrier)
|
|
4893
|
+
if (twin === root) root = carrier
|
|
4894
|
+
}
|
|
4895
|
+
// Guard a copy that has fallen out of step with the live tree.
|
|
4896
|
+
for (const input of root?.querySelectorAll('input[type="password" i],input[type="hidden" i]') ??
|
|
4897
|
+
[])
|
|
4898
|
+
input.remove()
|
|
4899
|
+
capture.html = root?.outerHTML ?? ''
|
|
4900
|
+
return capture
|
|
4284
4901
|
}`;
|
|
4285
4902
|
}
|
|
4286
4903
|
/**
|
|
@@ -4434,7 +5051,7 @@ function compileActionabilityFunction(options) {
|
|
|
4434
5051
|
if (${JSON.stringify(options.enabled === true)} && this.matches(':disabled')) throw new Error('Element is disabled')
|
|
4435
5052
|
if (${JSON.stringify(options.editable === true)} && (this.matches('[readonly]') || (!this.isContentEditable && !('value' in this)))) throw new Error('Element is not editable')
|
|
4436
5053
|
let previous
|
|
4437
|
-
for (let index = 0; index < 2; index += 1) {
|
|
5054
|
+
for (let index = 0; index < ${options.stable === true ? 2 : 0}; index += 1) {
|
|
4438
5055
|
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
4439
5056
|
const rect = this.getBoundingClientRect()
|
|
4440
5057
|
const current = [rect.x, rect.y, rect.width, rect.height]
|
|
@@ -5567,7 +6184,7 @@ var BrowserCodegen = class {
|
|
|
5567
6184
|
* title: 'Example',
|
|
5568
6185
|
* html: '<nav>Menu</nav><main><p>Body</p></main>',
|
|
5569
6186
|
* })
|
|
5570
|
-
* reading.markdown() // { text: 'Body', offset: 0, total: 4 }
|
|
6187
|
+
* reading.markdown({ distill: true }) // { text: 'Body', offset: 0, total: 4 }
|
|
5571
6188
|
* ```
|
|
5572
6189
|
*/
|
|
5573
6190
|
var BrowserReading = class {
|
|
@@ -5599,7 +6216,7 @@ var BrowserReading = class {
|
|
|
5599
6216
|
return this.#navigation !== void 0 && this.#navigation() !== this.#epoch;
|
|
5600
6217
|
}
|
|
5601
6218
|
markdown(options) {
|
|
5602
|
-
const distill = options?.distill ??
|
|
6219
|
+
const distill = options?.distill ?? false;
|
|
5603
6220
|
let text = this.#markdowns.get(distill);
|
|
5604
6221
|
if (text === void 0) {
|
|
5605
6222
|
text = (0, _orkestrel_markdown.renderMarkdown)((0, _orkestrel_markdown.htmlToMarkdown)(this.#source(distill).document));
|
|
@@ -5608,7 +6225,7 @@ var BrowserReading = class {
|
|
|
5608
6225
|
return extractBrowserSlice(text, options?.offset, options?.limit);
|
|
5609
6226
|
}
|
|
5610
6227
|
text(options) {
|
|
5611
|
-
const distill = options?.distill ??
|
|
6228
|
+
const distill = options?.distill ?? false;
|
|
5612
6229
|
let text = this.#texts.get(distill);
|
|
5613
6230
|
if (text === void 0) {
|
|
5614
6231
|
text = (0, _orkestrel_html.renderText)(this.#source(distill).document);
|
|
@@ -5617,7 +6234,10 @@ var BrowserReading = class {
|
|
|
5617
6234
|
return extractBrowserSlice(text, options?.offset, options?.limit);
|
|
5618
6235
|
}
|
|
5619
6236
|
#source(distill) {
|
|
5620
|
-
if (!distill) return this.#html
|
|
6237
|
+
if (!distill) return this.#html.map((node) => node.category === "element" ? {
|
|
6238
|
+
...node,
|
|
6239
|
+
attributes: (0, _orkestrel_html.resolveAttributes)(node, this.#url)
|
|
6240
|
+
} : node);
|
|
5621
6241
|
this.#distilled ??= this.#html.distill({ base: this.#url });
|
|
5622
6242
|
return this.#distilled;
|
|
5623
6243
|
}
|
|
@@ -5688,8 +6308,7 @@ var BrowserPageElement = class {
|
|
|
5688
6308
|
async read(options) {
|
|
5689
6309
|
const epoch = this.#epoch();
|
|
5690
6310
|
const capture = await this.#call(`function() {
|
|
5691
|
-
const capture = (${compileReadFunction()})()
|
|
5692
|
-
capture.html = this.outerHTML
|
|
6311
|
+
const capture = (${compileReadFunction()})(this)
|
|
5693
6312
|
return ${compileGuardedEvaluateExpression("capture", BROWSER_RESULT_LIMIT)}
|
|
5694
6313
|
}`, options);
|
|
5695
6314
|
if (!(0, _orkestrel_contract.isRecord)(capture)) throw new BrowserElementError(this.reference, "UNKNOWN", "returned an invalid reading");
|
|
@@ -5890,6 +6509,7 @@ var BrowserPageElement = class {
|
|
|
5890
6509
|
}
|
|
5891
6510
|
async #pointer(options) {
|
|
5892
6511
|
this.#assert(options);
|
|
6512
|
+
await this.#input.page.send("Page.bringToFront", void 0, options);
|
|
5893
6513
|
await this.#input.client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId: this.#input.backend }, {
|
|
5894
6514
|
session: this.#input.node.session,
|
|
5895
6515
|
...options
|
|
@@ -6010,7 +6630,7 @@ var BrowserElementManager = class {
|
|
|
6010
6630
|
...options
|
|
6011
6631
|
});
|
|
6012
6632
|
this.#assertCapture(this.#input.page.id, epoch);
|
|
6013
|
-
return renderBrowserOutline(this.#input.page.url, requireBrowserString(readEvaluationResult(result), "Document title"), this.#within(rows, options?.within), limit);
|
|
6633
|
+
return renderBrowserOutline(this.#input.page.url, requireBrowserString(readEvaluationResult(result), "Document title"), this.#within(rows, options?.within), limit, options?.search);
|
|
6014
6634
|
} catch (error) {
|
|
6015
6635
|
if (options?.signal?.aborted === true || this.#changes === changes) throw error;
|
|
6016
6636
|
this.#lifetime.signal.throwIfAborted();
|
|
@@ -6100,34 +6720,52 @@ var BrowserElementManager = class {
|
|
|
6100
6720
|
const signal = options?.signal === void 0 ? this.#lifetime.signal : AbortSignal.any([options.signal, this.#lifetime.signal]);
|
|
6101
6721
|
while (true) {
|
|
6102
6722
|
signal.throwIfAborted();
|
|
6103
|
-
const remaining = Math.max(0, end - performance.now());
|
|
6104
|
-
const call = {
|
|
6105
|
-
timeout: remaining,
|
|
6106
|
-
signal
|
|
6107
|
-
};
|
|
6108
|
-
await this.#input.ready(call);
|
|
6109
|
-
const frame = this.#scopeFrame(query.within);
|
|
6110
|
-
const session = await this.#input.resolve(frame);
|
|
6111
|
-
const context = await this.#input.world(frame, session, call);
|
|
6112
6723
|
const key = `__browserQueryWait${++this.#sequence}`;
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
returnByValue: true,
|
|
6117
|
-
awaitPromise: true
|
|
6118
|
-
}, {
|
|
6119
|
-
session,
|
|
6120
|
-
timeout: remaining + 1e3,
|
|
6121
|
-
signal
|
|
6122
|
-
}).then((result) => ({ result }), (error) => ({ error }));
|
|
6724
|
+
let session;
|
|
6725
|
+
let context;
|
|
6726
|
+
const changes = this.#changes;
|
|
6123
6727
|
try {
|
|
6728
|
+
await this.#input.ready({
|
|
6729
|
+
timeout: this.#remaining(end),
|
|
6730
|
+
signal
|
|
6731
|
+
});
|
|
6732
|
+
const frame = this.#scopeFrame(query.within);
|
|
6733
|
+
session = await this.#input.resolve(frame);
|
|
6734
|
+
context = await this.#input.world(frame, session, {
|
|
6735
|
+
timeout: this.#remaining(end),
|
|
6736
|
+
signal
|
|
6737
|
+
});
|
|
6738
|
+
const remaining = this.#remaining(end);
|
|
6739
|
+
const call = {
|
|
6740
|
+
timeout: remaining,
|
|
6741
|
+
signal
|
|
6742
|
+
};
|
|
6743
|
+
const pending = this.#input.client.send("Runtime.evaluate", {
|
|
6744
|
+
expression: compileQueryWaitExpression(remaining, key),
|
|
6745
|
+
contextId: context,
|
|
6746
|
+
returnByValue: true,
|
|
6747
|
+
awaitPromise: true
|
|
6748
|
+
}, {
|
|
6749
|
+
session,
|
|
6750
|
+
timeout: remaining + 1e3,
|
|
6751
|
+
signal
|
|
6752
|
+
}).then((result) => ({ result }), (error) => ({ error }));
|
|
6124
6753
|
const found = await this.find(query, call);
|
|
6125
6754
|
if (options?.absent === true ? found.length === 0 : found.length > 0) return found;
|
|
6126
6755
|
const outcome = await pending;
|
|
6127
6756
|
if ("error" in outcome) throw outcome.error;
|
|
6128
6757
|
if (readEvaluationResult(outcome.result) !== true || performance.now() >= end) throw new BrowserError("Element wait timed out", "BROWSER_WAIT_TIMEOUT");
|
|
6758
|
+
} catch (error) {
|
|
6759
|
+
signal.throwIfAborted();
|
|
6760
|
+
if (isCDPTimeoutError(error)) throw new BrowserError("Element wait timed out", "BROWSER_WAIT_TIMEOUT");
|
|
6761
|
+
if (this.#input.page.closed || this.#changes === changes && (!(0, _orkestrel_contract.isError)(error) || !BROWSER_CONTEXT_LOSS_PATTERN.test(error.message))) throw error;
|
|
6762
|
+
const remaining = this.#remaining(end);
|
|
6763
|
+
if (this.#changes === changes) await this.#input.recover({
|
|
6764
|
+
signal,
|
|
6765
|
+
timeout: remaining
|
|
6766
|
+
});
|
|
6129
6767
|
} finally {
|
|
6130
|
-
await this.#input.client.send("Runtime.evaluate", {
|
|
6768
|
+
if (context !== void 0 && session !== void 0) await this.#input.client.send("Runtime.evaluate", {
|
|
6131
6769
|
expression: `globalThis[${JSON.stringify(key)}]?.()`,
|
|
6132
6770
|
contextId: context,
|
|
6133
6771
|
returnByValue: true
|
|
@@ -6138,6 +6776,11 @@ var BrowserElementManager = class {
|
|
|
6138
6776
|
}
|
|
6139
6777
|
}
|
|
6140
6778
|
}
|
|
6779
|
+
#remaining(end) {
|
|
6780
|
+
const remaining = end - performance.now();
|
|
6781
|
+
if (remaining <= 0) throw new BrowserError("Element wait timed out", "BROWSER_WAIT_TIMEOUT");
|
|
6782
|
+
return remaining;
|
|
6783
|
+
}
|
|
6141
6784
|
element(reference) {
|
|
6142
6785
|
const canonical = parseBrowserReference(reference);
|
|
6143
6786
|
return [...this.#records.values()].find((entry) => entry.node.reference === canonical)?.element;
|
|
@@ -6645,7 +7288,7 @@ var BrowserRegistry = class {
|
|
|
6645
7288
|
pending.reject(error);
|
|
6646
7289
|
}
|
|
6647
7290
|
async #executeAdopted(tool, synthetic, input, context) {
|
|
6648
|
-
const parameters = synthetic ? Object.fromEntries(Object.entries(input).filter(([key]) => key !== "
|
|
7291
|
+
const parameters = synthetic ? Object.fromEntries(Object.entries(input).filter(([key]) => key !== "purpose")) : input;
|
|
6649
7292
|
const result = await this.execute(tool, parameters, { signal: context.signal });
|
|
6650
7293
|
if (result.status !== "Completed") throw new BrowserError(`Browser tool ${result.status}: ${result.error ?? "No error description"}`.slice(0, BROWSER_REGISTRY_OUTPUT_LIMIT));
|
|
6651
7294
|
return renderBrowserToolOutput(result.output);
|
|
@@ -8962,6 +9605,8 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
8962
9605
|
#epoch = 0;
|
|
8963
9606
|
#floor = 0;
|
|
8964
9607
|
#closed = false;
|
|
9608
|
+
#waitRelease = new AbortController();
|
|
9609
|
+
#textWaits = /* @__PURE__ */ new Map();
|
|
8965
9610
|
#codegen;
|
|
8966
9611
|
#registry;
|
|
8967
9612
|
#codegenStart = new BrowserTransition();
|
|
@@ -9012,6 +9657,7 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9012
9657
|
session: sessionId,
|
|
9013
9658
|
resolve: this.#resolveFrameSession.bind(this),
|
|
9014
9659
|
world: this.#world.bind(this),
|
|
9660
|
+
recover: this.#recoverReadiness.bind(this),
|
|
9015
9661
|
reference: this.#reference,
|
|
9016
9662
|
ready: this.#ready.bind(this)
|
|
9017
9663
|
});
|
|
@@ -9075,6 +9721,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9075
9721
|
}
|
|
9076
9722
|
async wait(text, options) {
|
|
9077
9723
|
this.assert();
|
|
9724
|
+
options = {
|
|
9725
|
+
...options,
|
|
9726
|
+
signal: options?.signal === void 0 ? this.#waitRelease.signal : AbortSignal.any([options.signal, this.#waitRelease.signal])
|
|
9727
|
+
};
|
|
9078
9728
|
const timeout = options?.timeout ?? 3e4;
|
|
9079
9729
|
validateBrowserTimeout(timeout);
|
|
9080
9730
|
const end = performance.now() + timeout;
|
|
@@ -9087,9 +9737,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9087
9737
|
});
|
|
9088
9738
|
const context = await this.#world(this.id, this.#sessionId, options);
|
|
9089
9739
|
const key = `__browserTextWait${++this.#waitSequence}`;
|
|
9740
|
+
this.#textWaits.set(key, context);
|
|
9090
9741
|
try {
|
|
9091
9742
|
if (readEvaluationResult(await this.send("Runtime.evaluate", {
|
|
9092
|
-
expression: compileTextWaitExpression(text, remaining, key),
|
|
9743
|
+
expression: compileTextWaitExpression(text, remaining, key, options?.absent === true),
|
|
9093
9744
|
contextId: context,
|
|
9094
9745
|
returnByValue: true,
|
|
9095
9746
|
awaitPromise: true
|
|
@@ -9103,23 +9754,20 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9103
9754
|
return;
|
|
9104
9755
|
} catch (error) {
|
|
9105
9756
|
if (options?.signal?.aborted === true) {
|
|
9106
|
-
await this
|
|
9107
|
-
expression: `globalThis[${JSON.stringify(key)}]?.()`,
|
|
9108
|
-
contextId: context,
|
|
9109
|
-
returnByValue: true
|
|
9110
|
-
}, { timeout: 1e3 }).catch(() => void 0);
|
|
9757
|
+
if (!this.#closed) await this.#releaseTextWait(key, context);
|
|
9111
9758
|
throw options.signal.reason;
|
|
9112
9759
|
}
|
|
9113
|
-
if (!(0, _orkestrel_contract.isError)(error) ||
|
|
9114
|
-
this.#dom = void 0;
|
|
9760
|
+
if (!(0, _orkestrel_contract.isError)(error) || !BROWSER_CONTEXT_LOSS_PATTERN.test(error.message)) throw error;
|
|
9115
9761
|
if (performance.now() >= end) throw new BrowserError("Browser text wait timed out", "BROWSER_WAIT_TIMEOUT", {
|
|
9116
9762
|
text,
|
|
9117
9763
|
timeout
|
|
9118
9764
|
});
|
|
9119
|
-
await this.#
|
|
9765
|
+
await this.#recoverReadiness({
|
|
9120
9766
|
...options,
|
|
9121
9767
|
timeout: Math.max(0, end - performance.now())
|
|
9122
9768
|
});
|
|
9769
|
+
} finally {
|
|
9770
|
+
this.#textWaits.delete(key);
|
|
9123
9771
|
}
|
|
9124
9772
|
}
|
|
9125
9773
|
}
|
|
@@ -9507,6 +10155,8 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9507
10155
|
return release;
|
|
9508
10156
|
}
|
|
9509
10157
|
async #releaseResources() {
|
|
10158
|
+
this.#waitRelease.abort(new BrowserError("Browser page is closed"));
|
|
10159
|
+
await Promise.all([...this.#textWaits].map(([key, context]) => this.#releaseTextWait(key, context)));
|
|
9510
10160
|
this.#unhold();
|
|
9511
10161
|
this.#announcement.reject(new BrowserError("Browser session ended"));
|
|
9512
10162
|
this.#client.unsubscribe("Page.lifecycleEvent", this.#lifecycleHandler, this.#sessionId);
|
|
@@ -9556,6 +10206,16 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9556
10206
|
}
|
|
9557
10207
|
this.#steps.destroy();
|
|
9558
10208
|
}
|
|
10209
|
+
async #releaseTextWait(key, context) {
|
|
10210
|
+
await this.#client.send("Runtime.evaluate", {
|
|
10211
|
+
expression: `globalThis[${JSON.stringify(key)}]?.()`,
|
|
10212
|
+
contextId: context,
|
|
10213
|
+
returnByValue: true
|
|
10214
|
+
}, {
|
|
10215
|
+
session: this.#sessionId,
|
|
10216
|
+
timeout: 1e3
|
|
10217
|
+
}).catch(() => void 0);
|
|
10218
|
+
}
|
|
9559
10219
|
#frame(frame) {
|
|
9560
10220
|
return new BrowserFrame(this.#client, (id) => this.#resolveFrameSession(id), frame.id, frame.url, frame.parent, frame.name, true, () => this.#epochOf(frame.id), (session, call) => this.#world(frame.id, session, call));
|
|
9561
10221
|
}
|
|
@@ -9599,6 +10259,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9599
10259
|
#rejectSeed(seed) {
|
|
9600
10260
|
if (this.#seed === seed) this.#seed = void 0;
|
|
9601
10261
|
}
|
|
10262
|
+
#recoverReadiness(options) {
|
|
10263
|
+
this.#dom = void 0;
|
|
10264
|
+
return this.#parkReadiness(options);
|
|
10265
|
+
}
|
|
9602
10266
|
async #seedReadiness(options) {
|
|
9603
10267
|
const document = this.#floor;
|
|
9604
10268
|
const context = await this.#world(this.id, this.#sessionId, options);
|
|
@@ -9646,24 +10310,44 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
|
|
|
9646
10310
|
this.#dom = this.#loader ?? "initial";
|
|
9647
10311
|
for (const id of this.#readiness.keys()) this.#settleReadiness(id)?.resolve();
|
|
9648
10312
|
}
|
|
9649
|
-
#world(frame, session, options) {
|
|
10313
|
+
async #world(frame, session, options) {
|
|
10314
|
+
options?.signal?.throwIfAborted();
|
|
9650
10315
|
const cached = this.#worlds.get(frame);
|
|
9651
10316
|
if (cached !== void 0) return Promise.resolve(cached.context);
|
|
9652
10317
|
const existing = this.#creating.get(frame);
|
|
9653
|
-
if (existing !== void 0) return existing.promise;
|
|
10318
|
+
if (existing !== void 0) return await this.#awaitWorld(existing.promise, options);
|
|
9654
10319
|
const pending = this.#client.send("Page.createIsolatedWorld", {
|
|
9655
10320
|
frameId: frame,
|
|
9656
10321
|
worldName: BROWSER_FRAME_WORLD_NAME
|
|
9657
10322
|
}, {
|
|
9658
10323
|
session,
|
|
9659
|
-
|
|
10324
|
+
signal: this.#waitRelease.signal
|
|
9660
10325
|
}).then(this.#decodeWorld.bind(this, frame));
|
|
9661
10326
|
this.#creating.set(frame, {
|
|
9662
10327
|
session,
|
|
9663
10328
|
promise: pending
|
|
9664
10329
|
});
|
|
9665
10330
|
pending.then(this.#publishWorld.bind(this, frame, session, pending), this.#settleWorld.bind(this, frame, pending));
|
|
9666
|
-
return pending;
|
|
10331
|
+
return await this.#awaitWorld(pending, options);
|
|
10332
|
+
}
|
|
10333
|
+
async #awaitWorld(pending, options) {
|
|
10334
|
+
const signal = options?.signal;
|
|
10335
|
+
signal?.throwIfAborted();
|
|
10336
|
+
const timeout = options?.timeout ?? 3e4;
|
|
10337
|
+
validateBrowserTimeout(timeout);
|
|
10338
|
+
const abandoned = Promise.withResolvers();
|
|
10339
|
+
const abort = this.#abandonWorld.bind(this, abandoned, signal);
|
|
10340
|
+
const timer = setTimeout(() => abandoned.reject(new CDPTimeoutError("Isolated world wait timed out")), timeout);
|
|
10341
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
10342
|
+
try {
|
|
10343
|
+
return await Promise.race([pending, abandoned.promise]);
|
|
10344
|
+
} finally {
|
|
10345
|
+
clearTimeout(timer);
|
|
10346
|
+
signal?.removeEventListener("abort", abort);
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10349
|
+
#abandonWorld(pending, signal) {
|
|
10350
|
+
pending.reject(signal?.reason);
|
|
9667
10351
|
}
|
|
9668
10352
|
#decodeWorld(frame, world) {
|
|
9669
10353
|
return readBrowserWorld(world, frame);
|
|
@@ -11494,7 +12178,7 @@ function createBrowserReading(input) {
|
|
|
11494
12178
|
*
|
|
11495
12179
|
* const toolset = createBrowserToolset(page, { context })
|
|
11496
12180
|
* await toolset.start()
|
|
11497
|
-
* const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: {
|
|
12181
|
+
* const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: { search: 'cart' } })
|
|
11498
12182
|
* ```
|
|
11499
12183
|
*
|
|
11500
12184
|
* @example Drive a page with a small model
|
|
@@ -11508,8 +12192,8 @@ function createBrowserReading(input) {
|
|
|
11508
12192
|
* const system =
|
|
11509
12193
|
* 'You control a web browser with tools and must call a tool before you answer. ' +
|
|
11510
12194
|
* 'The first message shows the page as look returns it; references such as e4 name its elements. ' +
|
|
11511
|
-
* 'To learn a fact, call read with
|
|
11512
|
-
*
|
|
12195
|
+
* 'To learn a fact, call read with search set to words from your question; when its result ends by naming an offset, call read again with that offset. ' +
|
|
12196
|
+
* "To use the site's search box, call type with its reference, the words, and submit true. " +
|
|
11513
12197
|
* 'To press a button or follow a link, call click with its reference from the latest result. Never invent a reference. ' +
|
|
11514
12198
|
* 'If text you expect has not appeared, call wait once. ' +
|
|
11515
12199
|
* 'When the task is done, answer in one short sentence.'
|
|
@@ -11519,11 +12203,11 @@ function createBrowserReading(input) {
|
|
|
11519
12203
|
* const page = await browser.create({ url: 'https://shop.example.test/' })
|
|
11520
12204
|
* const toolset = createBrowserToolset(page, { tools: createToolManager() })
|
|
11521
12205
|
* await toolset.start()
|
|
11522
|
-
* toolset.tools.tools().map((tool) => tool.name) // ['look', 'read', 'click', 'type', 'press', 'navigate', 'wait']
|
|
12206
|
+
* toolset.tools.tools().map((tool) => tool.name) // ['look', 'read', 'plain', 'click', 'type', 'press', 'navigate', 'wait']
|
|
11523
12207
|
* const seeded = await toolset.tools.execute({
|
|
11524
12208
|
* id: 'seed',
|
|
11525
12209
|
* name: 'look',
|
|
11526
|
-
* arguments: {
|
|
12210
|
+
* arguments: { search: '' },
|
|
11527
12211
|
* })
|
|
11528
12212
|
* const view = seeded.success ? String(seeded.value) : seeded.error
|
|
11529
12213
|
* const agent = createAgent(createOllama({ model: 'qwen3.5:2b-q4_K_M' }), {
|
|
@@ -11743,6 +12427,10 @@ var BrowserJourneyToolset = class {
|
|
|
11743
12427
|
const offset = args["offset"] ?? 0;
|
|
11744
12428
|
if (!(0, _orkestrel_contract.isInteger)(offset) || offset < 0) throw new BrowserError("The offset parameter must be a non-negative integer.", "BROWSER_JOURNEY_ARGUMENT", { key: "offset" });
|
|
11745
12429
|
const listings = [];
|
|
12430
|
+
const headings = [];
|
|
12431
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
12432
|
+
let headingOffset = 0;
|
|
12433
|
+
let listingOffset = 0;
|
|
11746
12434
|
const faults = /* @__PURE__ */ new Set();
|
|
11747
12435
|
let position = 0;
|
|
11748
12436
|
for (;;) {
|
|
@@ -11750,11 +12438,21 @@ var BrowserJourneyToolset = class {
|
|
|
11750
12438
|
signal,
|
|
11751
12439
|
offset: position
|
|
11752
12440
|
});
|
|
11753
|
-
|
|
12441
|
+
for (const entry of page.entries) {
|
|
12442
|
+
const listing = renderBrowserJourney(entry.journey);
|
|
12443
|
+
const heading = listing.split("\n", 1)[0] ?? "";
|
|
12444
|
+
headings.push(heading);
|
|
12445
|
+
offsets.set(headingOffset, listingOffset);
|
|
12446
|
+
headingOffset += heading.length + 1;
|
|
12447
|
+
listingOffset += listing.length + 2;
|
|
12448
|
+
listings.push(listing);
|
|
12449
|
+
}
|
|
11754
12450
|
for (const fault of page.faults) {
|
|
11755
12451
|
if (faults.has(fault.name)) continue;
|
|
11756
12452
|
faults.add(fault.name);
|
|
11757
|
-
|
|
12453
|
+
const listing = renderBrowserJourneyFault(fault);
|
|
12454
|
+
listings.push(listing);
|
|
12455
|
+
listingOffset += listing.length + 2;
|
|
11758
12456
|
}
|
|
11759
12457
|
const span = page.entries.length;
|
|
11760
12458
|
position += span;
|
|
@@ -11763,7 +12461,11 @@ var BrowserJourneyToolset = class {
|
|
|
11763
12461
|
if (listings.length === 0) return BROWSER_JOURNEY_EMPTY_LISTING;
|
|
11764
12462
|
const listing = listings.join("\n\n");
|
|
11765
12463
|
const start = offset < listing.length ? offset : 0;
|
|
11766
|
-
|
|
12464
|
+
const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
|
|
12465
|
+
const matches = start === 0 ? scanBrowserText(headings.join("\n"), search) : [];
|
|
12466
|
+
const count = matches.length;
|
|
12467
|
+
const block = renderBrowserMatches(`${count} ${count === 1 ? "journey matches" : "journeys match"} ${JSON.stringify(search)}:`, matches.map((match) => `[${offsets.get(match.offset)}] ${match.text}`), this.#limit);
|
|
12468
|
+
let end = Math.min(listing.length, start + this.#limit - block.length);
|
|
11767
12469
|
const last = listing.charCodeAt(end - 1);
|
|
11768
12470
|
if (end < listing.length && last >= 55296 && last <= 56319) end -= 1;
|
|
11769
12471
|
if (end === start) throw new BrowserError(`The journeys limit of ${this.#limit} characters cannot hold the next character at offset ${start}; raise the journeys limit.`, "BROWSER_TOOLSET_LIMIT", {
|
|
@@ -11771,8 +12473,8 @@ var BrowserJourneyToolset = class {
|
|
|
11771
12473
|
offset: start
|
|
11772
12474
|
});
|
|
11773
12475
|
const text = listing.slice(start, end);
|
|
11774
|
-
if (start === 0 && end === listing.length) return text
|
|
11775
|
-
return `${text}\n\n[characters ${start}–${end} of ${listing.length}${end < listing.length ? `; call journeys with offset ${end} for more` : ""}]`;
|
|
12476
|
+
if (start === 0 && end === listing.length) return `${block}${text}`;
|
|
12477
|
+
return `${block}${text}\n\n[characters ${start}–${end} of ${listing.length}${end < listing.length ? `; call journeys with offset ${end} for more` : ""}]`;
|
|
11776
12478
|
}
|
|
11777
12479
|
async #edit(args, signal) {
|
|
11778
12480
|
if (this.#readonly) throw new BrowserError(BROWSER_JOURNEY_READONLY_REFUSAL, "BROWSER_JOURNEY_READONLY");
|
|
@@ -11938,7 +12640,7 @@ var BrowserJourneyToolset = class {
|
|
|
11938
12640
|
const performed = await this.#toolset.perform({
|
|
11939
12641
|
id: "journey",
|
|
11940
12642
|
name: "look",
|
|
11941
|
-
arguments: {
|
|
12643
|
+
arguments: { search: "" }
|
|
11942
12644
|
}, { signal });
|
|
11943
12645
|
return performed.result.success ? String(performed.result.value) : performed.result.error;
|
|
11944
12646
|
}
|
|
@@ -11966,7 +12668,7 @@ var BrowserJourneyToolset = class {
|
|
|
11966
12668
|
* view's own tools beside them.
|
|
11967
12669
|
*
|
|
11968
12670
|
* @remarks
|
|
11969
|
-
* Every toolset advertises `look`, `read`, `click`, `type`, and `wait`, which need only the
|
|
12671
|
+
* Every toolset advertises `look`, `read`, `plain`, `click`, `type`, and `wait`, which need only the
|
|
11970
12672
|
* `BrowserViewInterface` it is constructed over. With `options.page` it also advertises `press`
|
|
11971
12673
|
* and `navigate`, stages the `dialog` tool while a dialog is open on the current page, follows a
|
|
11972
12674
|
* popup the current page opens and returns to the opener when the popup closes, and adopts
|
|
@@ -11980,11 +12682,18 @@ var BrowserJourneyToolset = class {
|
|
|
11980
12682
|
* refuses every tool but `dialog` while a dialog is open, forwards `ToolContext.signal` into every
|
|
11981
12683
|
* protocol call, refuses a call to a toolset tool that carries a parameter the tool does not
|
|
11982
12684
|
* advertise, and cuts every returned string and every thrown message at `limit` characters plus
|
|
11983
|
-
* a footer; the footer of a cut
|
|
11984
|
-
*
|
|
11985
|
-
* the
|
|
11986
|
-
*
|
|
11987
|
-
* the
|
|
12685
|
+
* a footer; the footer of a cut action or `dialog` receipt that carries a view names `look` as the next
|
|
12686
|
+
* call. `look`, `read`, and `plain` read the page and return pages of at most `limit`
|
|
12687
|
+
* characters and name the offset the next page starts at. A page ends after the last line break
|
|
12688
|
+
* in its window that lies past the page's start, or at the window's end when no such break fits,
|
|
12689
|
+
* without splitting a surrogate pair. The last page ends at the end of the outline or reading. A `look` whose
|
|
12690
|
+
* `search` matches referenced rows lists those rows first, and its pages reach every referenced row.
|
|
12691
|
+
* A `press` receipt names the referenced element that has focus after the key. A page tool's
|
|
12692
|
+
* error message and JSON output reach the toolset already cut at `BROWSER_REGISTRY_OUTPUT_LIMIT`
|
|
12693
|
+
* (4 096) by the registry, so a `limit` over that shows at most 4 096 characters of either; a page
|
|
12694
|
+
* tool's text output reaches the boundary whole.
|
|
12695
|
+
* `wait` with `absent` set to `true` settles when the text is not on the page, and a miss
|
|
12696
|
+
* returns the `timeout` outcome as an appearance miss does.
|
|
11988
12697
|
*
|
|
11989
12698
|
* `click`, `type`, `press`, `navigate`, and `switch` are actions: one runs at a time, in call
|
|
11990
12699
|
* order, and holds the queue until its receipt. A queued action whose signal aborts leaves
|
|
@@ -12015,7 +12724,7 @@ var BrowserJourneyToolset = class {
|
|
|
12015
12724
|
* page placement's `type` with `submit` ends its action with `and submitted the form` when a read
|
|
12016
12725
|
* recorded a submission, or when the navigation the receipt settled names `formSubmissionGet` or
|
|
12017
12726
|
* `formSubmissionPost` as its reason, whatever the read answered, and with `and pressed Enter`
|
|
12018
|
-
* otherwise. A `read`
|
|
12727
|
+
* otherwise. A `read` or `plain`
|
|
12019
12728
|
* at an offset at or past its retained reading's end restarts that reading at 0. A receipt that
|
|
12020
12729
|
* waits for a requested navigation shares one `BROWSER_TOOL_TIMEOUT_MS` deadline between that
|
|
12021
12730
|
* wait and its view capture, of which `BROWSER_TOOL_CAPTURE_MS` is reserved for the capture. The
|
|
@@ -12031,9 +12740,9 @@ var BrowserJourneyToolset = class {
|
|
|
12031
12740
|
* A page tool is skipped, with `skip` emitted, when its name is reserved (`unresolved` included), when the manager holds
|
|
12032
12741
|
* its name under a tool the toolset did not add (checked again immediately before each addition),
|
|
12033
12742
|
* when its name falls outside `BROWSER_TOOL_NAME_PATTERN`, when its parameters or, for a source
|
|
12034
|
-
* that supplies `tools()`, its selected registration declare `
|
|
12743
|
+
* that supplies `tools()`, its selected registration declare `purpose` optional, or when that
|
|
12035
12744
|
* registration is marked `debugging`. The selected registration is the main frame's, then the
|
|
12036
|
-
* earlier one. A page tool whose parameters require nothing advertises a required `
|
|
12745
|
+
* earlier one. A page tool whose parameters require nothing advertises a required `purpose`, which
|
|
12037
12746
|
* is stripped before the tool runs. Every adopted tool is advertised `untrusted`. `destroy()`
|
|
12038
12747
|
* removes only the tools the manager still holds under the instances the toolset added, never
|
|
12039
12748
|
* closes a page, and ends only what `options.release` hands over, calling it one time last.
|
|
@@ -12044,7 +12753,7 @@ var BrowserJourneyToolset = class {
|
|
|
12044
12753
|
*
|
|
12045
12754
|
* const toolset = new BrowserToolset(page, { page })
|
|
12046
12755
|
* await toolset.start()
|
|
12047
|
-
* const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: {
|
|
12756
|
+
* const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: { search: 'cart' } })
|
|
12048
12757
|
* await toolset.destroy()
|
|
12049
12758
|
* ```
|
|
12050
12759
|
*/
|
|
@@ -12108,20 +12817,23 @@ var BrowserToolset = class {
|
|
|
12108
12817
|
...options?.on === void 0 ? {} : { on: options.on },
|
|
12109
12818
|
...options?.error === void 0 ? {} : { error: options.error }
|
|
12110
12819
|
});
|
|
12111
|
-
const look = this.#create("look", this.#look.bind(this),
|
|
12112
|
-
const read = this.#create("read", this.#read.bind(this), BROWSER_TOOL_CUT_FOOTER);
|
|
12820
|
+
const look = this.#create("look", this.#look.bind(this), BROWSER_TOOL_CUT_FOOTER);
|
|
12821
|
+
const read = this.#create("read", this.#read.bind(this, "read", (reading) => reading.markdown()), BROWSER_TOOL_CUT_FOOTER);
|
|
12822
|
+
const plain = this.#create("plain", this.#read.bind(this, "plain", (reading) => reading.text()), BROWSER_TOOL_CUT_FOOTER);
|
|
12113
12823
|
const click = this.#create("click", this.#click.bind(this), BROWSER_TOOL_VIEW_FOOTER);
|
|
12114
12824
|
const type = this.#create("type", this.#type.bind(this), BROWSER_TOOL_VIEW_FOOTER);
|
|
12115
12825
|
const wait = this.#create("wait", this.#wait.bind(this), BROWSER_TOOL_CUT_FOOTER);
|
|
12116
12826
|
this.#native = Object.freeze(options?.page === void 0 ? [
|
|
12117
12827
|
look,
|
|
12118
12828
|
read,
|
|
12829
|
+
plain,
|
|
12119
12830
|
click,
|
|
12120
12831
|
type,
|
|
12121
12832
|
wait
|
|
12122
12833
|
] : [
|
|
12123
12834
|
look,
|
|
12124
12835
|
read,
|
|
12836
|
+
plain,
|
|
12125
12837
|
click,
|
|
12126
12838
|
type,
|
|
12127
12839
|
this.#create("press", this.#press.bind(this), BROWSER_TOOL_VIEW_FOOTER),
|
|
@@ -12424,44 +13136,74 @@ var BrowserToolset = class {
|
|
|
12424
13136
|
if ((0, _orkestrel_contract.isString)(secret) && secret.length > 0) message = message.replaceAll(JSON.stringify(secret), "[redacted]").replaceAll(secret, "[redacted]");
|
|
12425
13137
|
return clause === void 0 ? message : boundBrowserText(message, this.#limit, clause);
|
|
12426
13138
|
}
|
|
12427
|
-
async #look(
|
|
12428
|
-
|
|
13139
|
+
async #look(args, context) {
|
|
13140
|
+
const offset = this.#readOffset(args);
|
|
13141
|
+
const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
|
|
13142
|
+
const outline = await this.#race(this.#cursor.elements.outline({
|
|
13143
|
+
signal: context.signal,
|
|
13144
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
13145
|
+
search
|
|
13146
|
+
}), "");
|
|
13147
|
+
const total = outline.text.length;
|
|
13148
|
+
const start = offset < total ? offset : 0;
|
|
13149
|
+
const note = this.#drainNote();
|
|
13150
|
+
const room = this.#limit - note.length;
|
|
13151
|
+
const count = outline.matches.length;
|
|
13152
|
+
const block = start === 0 ? renderBrowserMatches(`${count} ${count === 1 ? "element matches" : "elements match"} ${JSON.stringify(search)}:`, outline.matches, room) : "";
|
|
13153
|
+
const space = room - block.length;
|
|
13154
|
+
const slice = space < 1 ? {
|
|
13155
|
+
text: "",
|
|
13156
|
+
offset: start,
|
|
13157
|
+
total
|
|
13158
|
+
} : extractBrowserSlice(outline.text, start, space);
|
|
13159
|
+
return this.#pageSlice("look", note, slice, block);
|
|
12429
13160
|
}
|
|
12430
|
-
async #read(args, context) {
|
|
12431
|
-
const offset = args
|
|
12432
|
-
if (!(0, _orkestrel_contract.isInteger)(offset) || offset < 0) throw new BrowserError("The offset parameter must be a non-negative integer.", "BROWSER_TOOLSET_ARGUMENT", { key: "offset" });
|
|
13161
|
+
async #read(name, project, args, context) {
|
|
13162
|
+
const offset = this.#readOffset(args);
|
|
12433
13163
|
const view = this.#cursor;
|
|
12434
13164
|
const retained = this.#reading;
|
|
12435
13165
|
let reading = retained?.reading;
|
|
12436
|
-
if (offset === 0 || retained === void 0 || reading === void 0 || retained.view !== view || reading.stale) {
|
|
13166
|
+
if (offset === 0 || retained === void 0 || reading === void 0 || retained.name !== name || retained.view !== view || reading.stale) {
|
|
12437
13167
|
reading = await this.#race(view.read({ signal: context.signal }), "", context.signal);
|
|
12438
13168
|
this.#reading = {
|
|
12439
13169
|
view,
|
|
12440
|
-
reading
|
|
13170
|
+
reading,
|
|
13171
|
+
name
|
|
12441
13172
|
};
|
|
12442
13173
|
}
|
|
12443
|
-
const
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
}).total ? offset : 0;
|
|
12447
|
-
const note = boundBrowserText(this.#drain(), Math.max(1, this.#limit - 1), BROWSER_TOOL_CUT_FOOTER);
|
|
13174
|
+
const whole = project(reading);
|
|
13175
|
+
const start = reading === retained?.reading && offset < whole.total ? offset : 0;
|
|
13176
|
+
const note = this.#drainNote();
|
|
12448
13177
|
const room = this.#limit - note.length;
|
|
12449
|
-
const
|
|
13178
|
+
const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
|
|
13179
|
+
const matches = start === 0 ? scanBrowserText(whole.text, search) : [];
|
|
13180
|
+
const count = matches.length;
|
|
13181
|
+
const block = renderBrowserMatches(`${count} ${count === 1 ? "line matches" : "lines match"} ${JSON.stringify(search)}:`, matches.map((match) => `[${match.offset}] ${match.text}`), room);
|
|
13182
|
+
const space = room - block.length;
|
|
13183
|
+
const slice = space < 1 ? {
|
|
12450
13184
|
text: "",
|
|
12451
13185
|
offset: start,
|
|
12452
|
-
total:
|
|
12453
|
-
} :
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12457
|
-
|
|
13186
|
+
total: whole.total
|
|
13187
|
+
} : extractBrowserSlice(whole.text, start, space);
|
|
13188
|
+
return this.#pageSlice(name, note, slice, block);
|
|
13189
|
+
}
|
|
13190
|
+
#readOffset(args) {
|
|
13191
|
+
const offset = args["offset"] ?? 0;
|
|
13192
|
+
if (!(0, _orkestrel_contract.isInteger)(offset) || offset < 0) throw new BrowserError("The offset parameter must be a non-negative integer.", "BROWSER_TOOLSET_ARGUMENT", { key: "offset" });
|
|
13193
|
+
return offset;
|
|
13194
|
+
}
|
|
13195
|
+
#drainNote() {
|
|
13196
|
+
return boundBrowserText(this.#drain(), Math.max(1, this.#limit - 1), BROWSER_TOOL_CUT_FOOTER);
|
|
13197
|
+
}
|
|
13198
|
+
#pageSlice(name, note, slice, block = "") {
|
|
13199
|
+
if (note === "" && slice.text === "" && slice.offset < slice.total) throw new BrowserError(`The ${name} limit of ${this.#limit} characters cannot hold the next character at offset ${slice.offset}; raise the toolset limit.`, "BROWSER_TOOLSET_LIMIT", {
|
|
12458
13200
|
limit: this.#limit,
|
|
12459
|
-
offset:
|
|
13201
|
+
offset: slice.offset
|
|
12460
13202
|
});
|
|
12461
13203
|
const end = slice.offset + slice.text.length;
|
|
12462
13204
|
const more = end < slice.total;
|
|
12463
|
-
if (
|
|
12464
|
-
return [`${note}${slice.text}`, `\n\n[characters ${
|
|
13205
|
+
if (slice.offset === 0 && !more) return [`${note}${block}${slice.text}`, ""];
|
|
13206
|
+
return [`${note}${block}${slice.text}`, `\n\n[characters ${slice.offset}–${end} of ${slice.total}${more ? `; call ${name} with offset ${end} for more` : ""}]`];
|
|
12465
13207
|
}
|
|
12466
13208
|
async #click(args, context) {
|
|
12467
13209
|
const turn = await this.#acquire(context.signal);
|
|
@@ -12564,7 +13306,7 @@ var BrowserToolset = class {
|
|
|
12564
13306
|
return [await this.#settle(page.keyboard.press(key), action, context.signal, true, record, popups, observation, key === "Enter" ? {
|
|
12565
13307
|
explicit: false,
|
|
12566
13308
|
action
|
|
12567
|
-
} : void 0), ""];
|
|
13309
|
+
} : void 0, true), ""];
|
|
12568
13310
|
} finally {
|
|
12569
13311
|
record?.destroy();
|
|
12570
13312
|
popups?.destroy();
|
|
@@ -12603,7 +13345,7 @@ var BrowserToolset = class {
|
|
|
12603
13345
|
const action = `Navigated to ${result.url}`;
|
|
12604
13346
|
return [renderBrowserReceipt({
|
|
12605
13347
|
action,
|
|
12606
|
-
view: await this.#capture(action, context.signal)
|
|
13348
|
+
view: (await this.#capture(action, context.signal)).view
|
|
12607
13349
|
}), ""];
|
|
12608
13350
|
} finally {
|
|
12609
13351
|
if (this.#navigating === page) this.#navigating = void 0;
|
|
@@ -12614,22 +13356,25 @@ var BrowserToolset = class {
|
|
|
12614
13356
|
async #wait(args, context) {
|
|
12615
13357
|
const text = readBrowserToolString(args, "text");
|
|
12616
13358
|
const seconds = args["timeout"];
|
|
13359
|
+
const absent = args["absent"];
|
|
13360
|
+
if (absent !== void 0 && !(0, _orkestrel_contract.isBoolean)(absent)) throw new BrowserError("The absent parameter must be a boolean.", "BROWSER_TOOLSET_ARGUMENT", { key: "absent" });
|
|
12617
13361
|
if (seconds !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(seconds) || seconds <= 0)) throw new BrowserError("The timeout parameter must be a positive number of seconds.", "BROWSER_TOOLSET_ARGUMENT", { key: "timeout" });
|
|
12618
13362
|
const timeout = Math.min(seconds === void 0 ? BROWSER_TOOL_TIMEOUT_MS : Math.ceil(seconds * 1e3), BROWSER_TOOL_TIMEOUT_LIMIT_MS);
|
|
12619
13363
|
const quoted = JSON.stringify(text);
|
|
12620
13364
|
try {
|
|
12621
13365
|
await this.#race(this.#cursor.wait(text, {
|
|
12622
13366
|
timeout,
|
|
12623
|
-
signal: context.signal
|
|
12624
|
-
|
|
12625
|
-
|
|
13367
|
+
signal: context.signal,
|
|
13368
|
+
absent: absent === true
|
|
13369
|
+
}), `Waited for ${quoted}${absent === true ? " to leave" : ""}`, context.signal);
|
|
13370
|
+
return [`${quoted} is ${absent === true ? "not " : ""}on the page.`, ""];
|
|
12626
13371
|
} catch (error) {
|
|
12627
13372
|
if (!context.signal.aborted && isBrowserError(error) && error.code === "BROWSER_WAIT_TIMEOUT") {
|
|
12628
13373
|
this.#actions.set(context.signal, {
|
|
12629
13374
|
...this.#actions.get(context.signal),
|
|
12630
13375
|
outcome: "timeout"
|
|
12631
13376
|
});
|
|
12632
|
-
return [`${quoted} did not appear within ${timeout / 1e3} s.`, ""];
|
|
13377
|
+
return [absent === true ? `${quoted} is still on the page after ${timeout / 1e3} s.` : `${quoted} did not appear within ${timeout / 1e3} s.`, ""];
|
|
12633
13378
|
}
|
|
12634
13379
|
throw error;
|
|
12635
13380
|
}
|
|
@@ -12652,14 +13397,19 @@ var BrowserToolset = class {
|
|
|
12652
13397
|
const action = `${accept ? "Accepted" : "Dismissed"} the ${dialog.category} dialog ${JSON.stringify(dialog.message)}`;
|
|
12653
13398
|
return [renderBrowserReceipt({
|
|
12654
13399
|
action,
|
|
12655
|
-
view: await this.#capture(action, context.signal)
|
|
13400
|
+
view: (await this.#capture(action, context.signal)).view
|
|
12656
13401
|
}), ""];
|
|
12657
13402
|
} finally {
|
|
12658
13403
|
turn.resolve();
|
|
12659
13404
|
}
|
|
12660
13405
|
}
|
|
12661
|
-
async #tabs(
|
|
12662
|
-
|
|
13406
|
+
async #tabs(args, context) {
|
|
13407
|
+
const text = (await this.#list(context.signal, true)).map((tab) => `${tab.id} ${JSON.stringify(tab.title)} ${tab.url}${tab.current ? " (current)" : ""}`).join("\n");
|
|
13408
|
+
const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
|
|
13409
|
+
const matches = scanBrowserText(text, search);
|
|
13410
|
+
const count = matches.length;
|
|
13411
|
+
const note = this.#drainNote();
|
|
13412
|
+
return [`${note}${renderBrowserMatches(`${count} ${count === 1 ? "tab matches" : "tabs match"} ${JSON.stringify(search)}:`, matches.map((match) => match.text), this.#limit - note.length)}${text}`, ""];
|
|
12663
13413
|
}
|
|
12664
13414
|
async #switch(args, context) {
|
|
12665
13415
|
const tab = readBrowserToolString(args, "tab");
|
|
@@ -12682,7 +13432,7 @@ var BrowserToolset = class {
|
|
|
12682
13432
|
await this.#command(front, action, front);
|
|
12683
13433
|
return [renderBrowserReceipt({
|
|
12684
13434
|
action,
|
|
12685
|
-
view: await this.#capture(action, context.signal)
|
|
13435
|
+
view: (await this.#capture(action, context.signal)).view
|
|
12686
13436
|
}), ""];
|
|
12687
13437
|
} finally {
|
|
12688
13438
|
turn.resolve();
|
|
@@ -12691,7 +13441,7 @@ var BrowserToolset = class {
|
|
|
12691
13441
|
async #invoke(tool, synthetic, args, context) {
|
|
12692
13442
|
const turn = await this.#acquire(context.signal);
|
|
12693
13443
|
try {
|
|
12694
|
-
const input = synthetic ? Object.fromEntries(Object.entries(args).filter(([key]) => key !== "
|
|
13444
|
+
const input = synthetic ? Object.fromEntries(Object.entries(args).filter(([key]) => key !== "purpose")) : args;
|
|
12695
13445
|
const command = Promise.resolve(tool.execute(input, context));
|
|
12696
13446
|
return [renderBrowserToolOutput(await this.#command(command, `Called ${tool.name}`, command, context.signal)), ""];
|
|
12697
13447
|
} finally {
|
|
@@ -12771,7 +13521,7 @@ var BrowserToolset = class {
|
|
|
12771
13521
|
}
|
|
12772
13522
|
return turn;
|
|
12773
13523
|
}
|
|
12774
|
-
async #settle(command, action, signal, trusted, record, popups, observation, enter) {
|
|
13524
|
+
async #settle(command, action, signal, trusted, record, popups, observation, enter, focus = false) {
|
|
12775
13525
|
let deadline;
|
|
12776
13526
|
try {
|
|
12777
13527
|
const started = record?.wait({ signal }).then(() => true, () => new Promise(() => void 0));
|
|
@@ -12807,11 +13557,13 @@ var BrowserToolset = class {
|
|
|
12807
13557
|
const unmoved = settled === void 0 && submissions.destinations.length === 0;
|
|
12808
13558
|
const status = settled?.stage === "committed" ? `the page is still loading ${settled.url}` : settled?.stage === "requested" ? `it requested ${settled.url} and the page did not change` : unmoved && submissions.prevented ? BROWSER_TOOL_HANDLED_STATUS : unmoved && submissions.submitted === false && (enter?.explicit === true || enter !== void 0 && submissions.implicit) ? "no form received the submission" : void 0;
|
|
12809
13559
|
const line = enter !== void 0 && (submissions.submitted === true || settled?.reason === "formSubmissionGet" || settled?.reason === "formSubmissionPost") ? enter.action : action;
|
|
13560
|
+
const captured = await this.#capture(line, signal, deadline);
|
|
13561
|
+
const parts = [status, focus && captured.focus !== void 0 ? `focus is on ${captured.focus}` : void 0].filter((part) => part !== void 0);
|
|
12810
13562
|
return renderBrowserReceipt({
|
|
12811
13563
|
action: line,
|
|
12812
13564
|
trusted,
|
|
12813
|
-
...
|
|
12814
|
-
view:
|
|
13565
|
+
...parts.length === 0 ? {} : { status: parts.join("; ") },
|
|
13566
|
+
view: captured.view
|
|
12815
13567
|
});
|
|
12816
13568
|
} finally {
|
|
12817
13569
|
record?.destroy();
|
|
@@ -12978,7 +13730,10 @@ var BrowserToolset = class {
|
|
|
12978
13730
|
}
|
|
12979
13731
|
}
|
|
12980
13732
|
async #capture(action, signal, deadline = performance.now() + BROWSER_TOOL_TIMEOUT_MS) {
|
|
12981
|
-
if (deadline - performance.now() < 1) return
|
|
13733
|
+
if (deadline - performance.now() < 1) return {
|
|
13734
|
+
view: BROWSER_TOOL_DEADLINE_NOTE,
|
|
13735
|
+
focus: void 0
|
|
13736
|
+
};
|
|
12982
13737
|
const receipt = new AbortController();
|
|
12983
13738
|
const options = {
|
|
12984
13739
|
signal: AbortSignal.any([signal, receipt.signal]),
|
|
@@ -12986,16 +13741,34 @@ var BrowserToolset = class {
|
|
|
12986
13741
|
};
|
|
12987
13742
|
try {
|
|
12988
13743
|
const outline = await this.#bounded(this.#cursor.elements.outline(options), deadline, action);
|
|
12989
|
-
return outline === void 0 ?
|
|
13744
|
+
return outline === void 0 ? {
|
|
13745
|
+
view: BROWSER_TOOL_DEADLINE_NOTE,
|
|
13746
|
+
focus: void 0
|
|
13747
|
+
} : {
|
|
13748
|
+
view: outline.text,
|
|
13749
|
+
focus: outline.focus
|
|
13750
|
+
};
|
|
12990
13751
|
} catch (error) {
|
|
12991
13752
|
if (signal.aborted || isBrowserError(error) && error.code === "BROWSER_TOOLSET_RECEIPT") throw error;
|
|
12992
|
-
if (!isBrowserElementError(error) || error.context?.["reason"] !== "GONE") return
|
|
13753
|
+
if (!isBrowserElementError(error) || error.context?.["reason"] !== "GONE") return {
|
|
13754
|
+
view: `(The view could not be read: ${(0, _orkestrel_contract.isError)(error) ? error.message : String(error)}; call look.)`,
|
|
13755
|
+
focus: void 0
|
|
13756
|
+
};
|
|
12993
13757
|
try {
|
|
12994
13758
|
const outline = await this.#bounded(this.#cursor.elements.outline(options), deadline, action);
|
|
12995
|
-
return outline === void 0 ?
|
|
13759
|
+
return outline === void 0 ? {
|
|
13760
|
+
view: BROWSER_TOOL_DEADLINE_NOTE,
|
|
13761
|
+
focus: void 0
|
|
13762
|
+
} : {
|
|
13763
|
+
view: outline.text,
|
|
13764
|
+
focus: outline.focus
|
|
13765
|
+
};
|
|
12996
13766
|
} catch (retry) {
|
|
12997
13767
|
if (signal.aborted || isBrowserError(retry) && retry.code === "BROWSER_TOOLSET_RECEIPT") throw retry;
|
|
12998
|
-
return
|
|
13768
|
+
return {
|
|
13769
|
+
view: BROWSER_TOOL_CHANGED_NOTE,
|
|
13770
|
+
focus: void 0
|
|
13771
|
+
};
|
|
12999
13772
|
}
|
|
13000
13773
|
} finally {
|
|
13001
13774
|
receipt.abort(new BrowserError("the receipt settled", "BROWSER_TOOLSET_SETTLED"));
|
|
@@ -13235,6 +14008,7 @@ exports.BROWSER_ACTION_OUTCOMES = BROWSER_ACTION_OUTCOMES;
|
|
|
13235
14008
|
exports.BROWSER_ACTION_STAGES = BROWSER_ACTION_STAGES;
|
|
13236
14009
|
exports.BROWSER_CODEGEN_BINDING_NAME = BROWSER_CODEGEN_BINDING_NAME;
|
|
13237
14010
|
exports.BROWSER_CODEGEN_SOURCE = BROWSER_CODEGEN_SOURCE;
|
|
14011
|
+
exports.BROWSER_CONTEXT_LOSS_PATTERN = BROWSER_CONTEXT_LOSS_PATTERN;
|
|
13238
14012
|
exports.BROWSER_DEFAULT_TIMEOUT_MS = BROWSER_DEFAULT_TIMEOUT_MS;
|
|
13239
14013
|
exports.BROWSER_DEFAULT_VIEWPORT_HEIGHT = BROWSER_DEFAULT_VIEWPORT_HEIGHT;
|
|
13240
14014
|
exports.BROWSER_DEFAULT_VIEWPORT_WIDTH = BROWSER_DEFAULT_VIEWPORT_WIDTH;
|
|
@@ -13269,6 +14043,7 @@ exports.BROWSER_RESULT_LIMIT_SENTINEL_PREFIX = BROWSER_RESULT_LIMIT_SENTINEL_PRE
|
|
|
13269
14043
|
exports.BROWSER_RUN_ID_PATTERN = BROWSER_RUN_ID_PATTERN;
|
|
13270
14044
|
exports.BROWSER_SCHEMES = BROWSER_SCHEMES;
|
|
13271
14045
|
exports.BROWSER_SCREENSHOT_ATTRIBUTE = BROWSER_SCREENSHOT_ATTRIBUTE;
|
|
14046
|
+
exports.BROWSER_SEARCH_PATTERN = BROWSER_SEARCH_PATTERN;
|
|
13272
14047
|
exports.BROWSER_SNAPSHOT_NODE_LIMIT = BROWSER_SNAPSHOT_NODE_LIMIT;
|
|
13273
14048
|
exports.BROWSER_STABLE_FRAME_COUNT = BROWSER_STABLE_FRAME_COUNT;
|
|
13274
14049
|
exports.BROWSER_STOP_LOADING_TIMEOUT_MS = BROWSER_STOP_LOADING_TIMEOUT_MS;
|
|
@@ -13288,6 +14063,7 @@ exports.BROWSER_TOOL_TIMEOUT_LIMIT_MS = BROWSER_TOOL_TIMEOUT_LIMIT_MS;
|
|
|
13288
14063
|
exports.BROWSER_TOOL_TIMEOUT_MS = BROWSER_TOOL_TIMEOUT_MS;
|
|
13289
14064
|
exports.BROWSER_TOOL_VIEW_FOOTER = BROWSER_TOOL_VIEW_FOOTER;
|
|
13290
14065
|
exports.BROWSER_TYPED_ROLES = BROWSER_TYPED_ROLES;
|
|
14066
|
+
exports.BROWSER_WAIT_EVENTS = BROWSER_WAIT_EVENTS;
|
|
13291
14067
|
exports.BrowserAccessibility = BrowserAccessibility;
|
|
13292
14068
|
exports.BrowserClock = BrowserClock;
|
|
13293
14069
|
exports.BrowserCodegen = BrowserCodegen;
|
|
@@ -13340,6 +14116,7 @@ exports.buildBrowserJourney = buildBrowserJourney;
|
|
|
13340
14116
|
exports.bytesToText = bytesToText;
|
|
13341
14117
|
exports.collectBrowserJourneyBindings = collectBrowserJourneyBindings;
|
|
13342
14118
|
exports.collectBrowserJourneyTextBindings = collectBrowserJourneyTextBindings;
|
|
14119
|
+
exports.collectBrowserWords = collectBrowserWords;
|
|
13343
14120
|
exports.compileActionabilityFunction = compileActionabilityFunction;
|
|
13344
14121
|
exports.compileBrowserBindingCleanup = compileBrowserBindingCleanup;
|
|
13345
14122
|
exports.compileBrowserBindingResult = compileBrowserBindingResult;
|
|
@@ -13464,7 +14241,9 @@ exports.readRareStringData = readRareStringData;
|
|
|
13464
14241
|
exports.renderBrowserElement = renderBrowserElement;
|
|
13465
14242
|
exports.renderBrowserJourney = renderBrowserJourney;
|
|
13466
14243
|
exports.renderBrowserJourneyFault = renderBrowserJourneyFault;
|
|
14244
|
+
exports.renderBrowserMatches = renderBrowserMatches;
|
|
13467
14245
|
exports.renderBrowserOutline = renderBrowserOutline;
|
|
14246
|
+
exports.renderBrowserOutlineRow = renderBrowserOutlineRow;
|
|
13468
14247
|
exports.renderBrowserReceipt = renderBrowserReceipt;
|
|
13469
14248
|
exports.renderBrowserRun = renderBrowserRun;
|
|
13470
14249
|
exports.renderBrowserRunResult = renderBrowserRunResult;
|
|
@@ -13472,6 +14251,8 @@ exports.renderBrowserToolOutput = renderBrowserToolOutput;
|
|
|
13472
14251
|
exports.requireBrowserReference = requireBrowserReference;
|
|
13473
14252
|
exports.requireBrowserString = requireBrowserString;
|
|
13474
14253
|
exports.resolveBrowserJourneyBinding = resolveBrowserJourneyBinding;
|
|
14254
|
+
exports.scanBrowserOutline = scanBrowserOutline;
|
|
14255
|
+
exports.scanBrowserText = scanBrowserText;
|
|
13475
14256
|
exports.settleBrowserTeardown = settleBrowserTeardown;
|
|
13476
14257
|
exports.textToBytes = textToBytes;
|
|
13477
14258
|
exports.validateBrowserAccessibilityOptions = validateBrowserAccessibilityOptions;
|