@orkestrel/browser 0.0.20 → 0.0.22

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