@orkestrel/browser 0.0.21 → 0.0.23

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