@orkestrel/browser 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.21"
166
+ version: "0.0.23"
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,14 +1681,252 @@ function filterBrowserOutline(nodes, query) {
1627
1681
  return nodes.filter((node) => !node.ignored && !BROWSER_OUTLINE_OMITTED_ROLES.has(node.role ?? "") && (query.role === void 0 || node.role === query.role) && (query.name === void 0 || (query.exact === true ? normalizeBrowserName(node.name ?? "") === normalizeBrowserName(query.name) : normalizeBrowserName(node.name ?? "").toLowerCase().includes(normalizeBrowserName(query.name).toLowerCase()))));
1628
1682
  }
1629
1683
  /**
1684
+ * Renders one outline row: the node's reference, its role, its quoted accessible name, and the
1685
+ * states it carries.
1686
+ *
1687
+ * @remarks
1688
+ * After the name the row appends `value="V"` when the node carries a non-empty value,
1689
+ * `pressed=true|false|mixed`, `expanded=true|false`, and `selected=true|false` when present,
1690
+ * `[checked]` when checked, `[disabled]` when disabled, and `[tool=NAME]` for a page tool's form,
1691
+ * in that order. A node without a reference renders without one.
1692
+ *
1693
+ * @param node - The outline node
1694
+ * @returns The rendered row
1695
+ *
1696
+ * @example
1697
+ * ```ts
1698
+ * import { renderBrowserOutlineRow } from '@orkestrel/browser'
1699
+ *
1700
+ * renderBrowserOutlineRow({
1701
+ * id: '4',
1702
+ * parent: undefined,
1703
+ * children: [],
1704
+ * backend: undefined,
1705
+ * frame: undefined,
1706
+ * ignored: false,
1707
+ * role: 'checkbox',
1708
+ * name: ' Gift wrap ',
1709
+ * description: undefined,
1710
+ * value: undefined,
1711
+ * properties: { checked: 'true' },
1712
+ * session: 'main',
1713
+ * reference: 'e4',
1714
+ * }) // 'e4 checkbox "Gift wrap" [checked]'
1715
+ * ```
1716
+ */
1717
+ function renderBrowserOutlineRow(node) {
1718
+ const name = JSON.stringify(normalizeBrowserName(node.name ?? ""));
1719
+ const role = node.role ?? "unknown";
1720
+ let row = node.reference === void 0 ? `${role} ${name}` : `${node.reference} ${role} ${name}`;
1721
+ if (node.value !== void 0 && node.value !== "") row += ` value=${JSON.stringify(String(node.value))}`;
1722
+ for (const key of [
1723
+ "pressed",
1724
+ "expanded",
1725
+ "selected"
1726
+ ]) {
1727
+ const value = node.properties[key];
1728
+ if ((0, _orkestrel_contract.isString)(value) || (0, _orkestrel_contract.isBoolean)(value)) row += ` ${key}=${String(value)}`;
1729
+ }
1730
+ if (node.properties["checked"] === true || node.properties["checked"] === "true") row += " [checked]";
1731
+ if (node.properties["disabled"] === true) row += " [disabled]";
1732
+ if (node.tool !== void 0) row += ` [tool=${node.tool}]`;
1733
+ return row;
1734
+ }
1735
+ /**
1736
+ * Returns the referenced outline nodes whose role and accessible name share the most words with a
1737
+ * search, in document order.
1738
+ *
1739
+ * @remarks
1740
+ * The search and each node's role and name are lowercased and split into whole words by
1741
+ * `BROWSER_SEARCH_PATTERN`, so a word shorter than 3 letters or digits never counts, and words
1742
+ * compare whole, with no substring or prefix match. A node scores the number of distinct search
1743
+ * words equal to one of its words, and every node with the highest score above 0 matches. Only a
1744
+ * node an outline renders as a referenced row takes part: one that is not ignored, carries a
1745
+ * reference, and whose role is not omitted, a heading, or text.
1746
+ *
1747
+ * @param nodes - Ordered accessibility rows
1748
+ * @param search - The words to match
1749
+ * @returns The best-matching nodes in document order; empty when no node shares a word
1750
+ *
1751
+ * @example
1752
+ * ```ts
1753
+ * import { scanBrowserOutline } from '@orkestrel/browser'
1754
+ *
1755
+ * const node = {
1756
+ * parent: undefined,
1757
+ * children: [],
1758
+ * backend: undefined,
1759
+ * frame: undefined,
1760
+ * ignored: false,
1761
+ * description: undefined,
1762
+ * value: undefined,
1763
+ * properties: {},
1764
+ * session: 'main',
1765
+ * }
1766
+ * const nodes = [
1767
+ * { ...node, id: '1', role: 'button', name: 'Close', reference: 'e1' },
1768
+ * { ...node, id: '2', role: 'button', name: 'Archive', reference: 'e2' },
1769
+ * ]
1770
+ * scanBrowserOutline(nodes, 'archive dialog button').map((match) => match.reference) // ['e2']
1771
+ * ```
1772
+ */
1773
+ function scanBrowserOutline(nodes, search) {
1774
+ const words = collectBrowserWords(search);
1775
+ if (words.size === 0) return [];
1776
+ let best = 0;
1777
+ const scored = [];
1778
+ for (const node of nodes) {
1779
+ const role = node.role ?? "";
1780
+ if (node.ignored || node.reference === void 0 || BROWSER_OUTLINE_OMITTED_ROLES.has(role) || role === "heading" || role === "StaticText") continue;
1781
+ const own = collectBrowserWords(`${role} ${node.name ?? ""}`);
1782
+ let score = 0;
1783
+ for (const word of words) if (own.has(word)) score += 1;
1784
+ if (score === 0) continue;
1785
+ best = Math.max(best, score);
1786
+ scored.push({
1787
+ node,
1788
+ score
1789
+ });
1790
+ }
1791
+ return scored.filter((entry) => entry.score === best).map((entry) => entry.node);
1792
+ }
1793
+ /**
1794
+ * Collects distinct lowercase words of at least 3 letters or digits.
1795
+ *
1796
+ * @param text - Text to scan
1797
+ * @returns The searchable whole words
1798
+ *
1799
+ * @example
1800
+ * ```ts
1801
+ * import { collectBrowserWords } from '@orkestrel/browser'
1802
+ * [...collectBrowserWords('The cart, the bag')] // ['the', 'cart', 'bag']
1803
+ * ```
1804
+ */
1805
+ function collectBrowserWords(text) {
1806
+ return new Set(Array.from(text.toLowerCase().matchAll(BROWSER_SEARCH_PATTERN), (match) => match[0]));
1807
+ }
1808
+ /**
1809
+ * Matches the lines sharing the most distinct search words, in document order.
1810
+ *
1811
+ * @param text - The complete projection
1812
+ * @param search - Words to match, using the outline's word rule
1813
+ * @returns The best-scoring lines and their original UTF-16 offsets; empty without a shared word
1814
+ *
1815
+ * @example
1816
+ * ```ts
1817
+ * import { scanBrowserText } from '@orkestrel/browser'
1818
+ * scanBrowserText('Cart\nBlue kettle', 'kettle') // [{ offset: 5, text: 'Blue kettle' }]
1819
+ * ```
1820
+ */
1821
+ function scanBrowserText(text, search) {
1822
+ const words = collectBrowserWords(search);
1823
+ if (words.size === 0) return [];
1824
+ let best = 0;
1825
+ const scored = [];
1826
+ for (const line of text.matchAll(/[^\r\n]+/g)) {
1827
+ const own = collectBrowserWords(line[0]);
1828
+ let score = 0;
1829
+ for (const word of words) if (own.has(word)) score += 1;
1830
+ if (score === 0) continue;
1831
+ best = Math.max(best, score);
1832
+ scored.push({
1833
+ match: {
1834
+ offset: line.index,
1835
+ text: line[0]
1836
+ },
1837
+ score
1838
+ });
1839
+ }
1840
+ return scored.filter((entry) => entry.score === best).map((entry) => entry.match);
1841
+ }
1842
+ /**
1843
+ * Renders matching rows in at most half the available reply room.
1844
+ *
1845
+ * @remarks
1846
+ * An oversized first row is cut with an ellipsis without splitting a surrogate pair, reserving
1847
+ * room for the first later row that can fit beside it only when the cut keeps the leading token
1848
+ * through its first space. Otherwise it cuts without reserving. Later rows that do not fit are
1849
+ * skipped. An empty collection or a cut with no character before the ellipsis produces no block.
1850
+ *
1851
+ * @param heading - The match count and search, ending with a colon
1852
+ * @param rows - Matching rows in document order
1853
+ * @param room - Characters available in the reply
1854
+ * @returns The bounded block with a trailing blank line, or an empty string
1855
+ *
1856
+ * @example
1857
+ * ```ts
1858
+ * import { renderBrowserMatches } from '@orkestrel/browser'
1859
+ * renderBrowserMatches('Matches:', ['[5] Cart'], 100) // 'Matches:\n[5] Cart\n\n'
1860
+ * ```
1861
+ */
1862
+ function renderBrowserMatches(heading, rows, room) {
1863
+ const half = Math.floor(room / 2);
1864
+ let text = `${heading}\n`;
1865
+ let count = 0;
1866
+ for (const row of rows) {
1867
+ const space = half - text.length - 2;
1868
+ if (space < 1) break;
1869
+ if (row.length > space) {
1870
+ if (count > 0) continue;
1871
+ let end = space - 1;
1872
+ const later = rows.slice(1).find((candidate) => candidate.length + 2 <= space);
1873
+ if (later !== void 0 && end - later.length - 1 >= row.indexOf(" ") + 1) end -= later.length + 1;
1874
+ const last = row.charCodeAt(end - 1);
1875
+ if (last >= 55296 && last <= 56319) end -= 1;
1876
+ if (end < 1) break;
1877
+ text += `${row.slice(0, end)}…\n`;
1878
+ } else text += `${row}\n`;
1879
+ count += 1;
1880
+ }
1881
+ return count === 0 ? "" : `${text}\n`;
1882
+ }
1883
+ /**
1630
1884
  * Renders document-order text and referenced elements with a bounded element count.
1885
+ *
1886
+ * @remarks
1887
+ * Each referenced row renders through `renderBrowserOutlineRow`, and the closing
1888
+ * `(COUNT of TOTAL elements)` line counts the rows included and available. `matches` holds the
1889
+ * rows `scanBrowserOutline` returns for `search`, and `focus` the last referenced row whose node
1890
+ * carries a `focused` property of `true`; both reach past `limit`.
1891
+ *
1631
1892
  * @param url - Document address
1632
1893
  * @param title - Document title
1633
1894
  * @param nodes - Ordered accessibility rows
1634
1895
  * @param limit - Maximum referenced rows to include
1635
- * @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}`];
1639
1931
  const sessions = /* @__PURE__ */ new Map();
1640
1932
  for (const node of nodes) {
@@ -1647,6 +1939,7 @@ function renderBrowserOutline(url, title, nodes, limit) {
1647
1939
  }
1648
1940
  let count = 0;
1649
1941
  let total = 0;
1942
+ let focus;
1650
1943
  for (const node of nodes) {
1651
1944
  if (node.ignored || BROWSER_OUTLINE_OMITTED_ROLES.has(node.role ?? "")) continue;
1652
1945
  const name = normalizeBrowserName(node.name ?? "");
@@ -1661,14 +1954,10 @@ function renderBrowserOutline(url, title, nodes, limit) {
1661
1954
  }
1662
1955
  if (node.reference === void 0) continue;
1663
1956
  total += 1;
1957
+ if (node.properties["focused"] === true) focus = node;
1664
1958
  if (count >= limit) continue;
1665
1959
  count += 1;
1666
- let row = `${node.reference} ${node.role ?? "unknown"} ${JSON.stringify(name)}`;
1667
- if (node.value !== void 0 && node.value !== "") row += ` value=${JSON.stringify(String(node.value))}`;
1668
- if (node.properties["checked"] === true || node.properties["checked"] === "true") row += " [checked]";
1669
- if (node.properties["disabled"] === true) row += " [disabled]";
1670
- if (node.tool !== void 0) row += ` [tool=${node.tool}]`;
1671
- rows.push(row);
1960
+ rows.push(renderBrowserOutlineRow(node));
1672
1961
  }
1673
1962
  rows.push(`(${count} of ${total} elements)`);
1674
1963
  return {
@@ -1676,7 +1965,9 @@ function renderBrowserOutline(url, title, nodes, limit) {
1676
1965
  title,
1677
1966
  text: rows.join("\n"),
1678
1967
  count,
1679
- total
1968
+ total,
1969
+ matches: search === void 0 ? [] : scanBrowserOutline(nodes, search).map(renderBrowserOutlineRow),
1970
+ focus: focus === void 0 ? void 0 : renderBrowserOutlineRow(focus)
1680
1971
  };
1681
1972
  }
1682
1973
  /**
@@ -1757,27 +2048,27 @@ function renderBrowserToolOutput(value) {
1757
2048
  return (rendered.success ? rendered.value ?? String(value) : "[Unserializable tool output]").slice(0, BROWSER_REGISTRY_OUTPUT_LIMIT);
1758
2049
  }
1759
2050
  /**
1760
- * Derives the advertised input schema from an authored one, adding a required `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.
1761
2052
  * @param schema - Authored input schema, or undefined for a parameterless tool
1762
- * @returns An advertised schema, or undefined when the authored `what` parameter is optional
1763
- * @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.
1764
2055
  */
1765
2056
  function deriveBrowserToolSchema(schema) {
1766
2057
  const properties = (0, _orkestrel_contract.isRecord)(schema?.["properties"]) ? schema["properties"] : {};
1767
2058
  const required = (0, _orkestrel_contract.isArray)(schema?.["required"]) ? schema["required"].filter(_orkestrel_contract.isString) : [];
1768
- if (Object.hasOwn(properties, "what") && !required.includes("what")) return void 0;
2059
+ if (Object.hasOwn(properties, "purpose") && !required.includes("purpose")) return void 0;
1769
2060
  if (required.length > 0) return schema;
1770
2061
  return {
1771
2062
  ...schema,
1772
2063
  type: "object",
1773
2064
  properties: {
1774
2065
  ...properties,
1775
- what: {
2066
+ purpose: {
1776
2067
  type: "string",
1777
2068
  description: "Describe the purpose of this action."
1778
2069
  }
1779
2070
  },
1780
- required: ["what"]
2071
+ required: ["purpose"]
1781
2072
  };
1782
2073
  }
1783
2074
  /**
@@ -1789,8 +2080,8 @@ function deriveBrowserToolSchema(schema) {
1789
2080
  * units, one fewer when the last would split a surrogate pair (so a limit of 1 before a pair keeps
1790
2081
  * nothing), followed by `\n[characters 0–END of TOTAL; FOOTER]`. The footer has no default, so
1791
2082
  * every caller states what the cut result's reader does next: the toolset passes
1792
- * `BROWSER_TOOL_VIEW_FOOTER` for a result that carries a view and `BROWSER_TOOL_CUT_FOOTER` for
1793
- * any other.
2083
+ * `BROWSER_TOOL_VIEW_FOOTER` for an action or `dialog` receipt that carries a view and
2084
+ * `BROWSER_TOOL_CUT_FOOTER` for any other result, including `look` and `read` results.
1794
2085
  *
1795
2086
  * @param text - The page-authored or composed string
1796
2087
  * @param limit - The most characters kept before the footer, a positive integer
@@ -1827,8 +2118,8 @@ function boundBrowserText(text, limit, footer) {
1827
2118
  * ```ts
1828
2119
  * import { BROWSER_TOOL_COPY, validateBrowserToolArguments } from '@orkestrel/browser'
1829
2120
  *
1830
- * validateBrowserToolArguments(BROWSER_TOOL_COPY.look, { what: 'cart', ref: 'e1' })
1831
- * // 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.'
1832
2123
  * ```
1833
2124
  */
1834
2125
  function validateBrowserToolArguments(definition, args) {
@@ -3608,7 +3899,7 @@ function renderBrowserJourney(journey) {
3608
3899
  return `${step.id} type ${secret ? "(secret)" : JSON.stringify(text)}${suffix} into ${name}${step.arguments["submit"] === true ? ", submit" : ""}`;
3609
3900
  }
3610
3901
  case "press": return `${step.id} press ${text}${suffix}`;
3611
- case "wait": return `${step.id} wait ${JSON.stringify(text)}${suffix}`;
3902
+ case "wait": return `${step.id} wait ${JSON.stringify(text)}${suffix}${step.arguments["absent"] === true ? ", absent" : ""}`;
3612
3903
  case "dialog": return `${step.id} dialog ${step.arguments["accept"] === true ? "accept" : "dismiss"}${binding === void 0 ? "" : ` ${JSON.stringify(text)}${suffix}`}`;
3613
3904
  case "switch": return `${step.id} switch ${JSON.stringify(step.tab?.title)} ${step.tab?.url}`;
3614
3905
  case "unresolved": return `${step.id} unresolved: ${step.gap}`;
@@ -3916,46 +4207,55 @@ function buildBrowserJourney(steps, options) {
3916
4207
  //#endregion
3917
4208
  //#region src/core/compilers.ts
3918
4209
  /**
3919
- * Compiles a 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.
3920
4215
  * @param deadline - Maximum time in milliseconds
3921
4216
  * @param key - Isolated-world property owning this observer
3922
- * @param predicate - Optional expression checked immediately and after each mutation batch
4217
+ * @param predicate - Optional expression checked immediately and on the task after a mutation batch or a `BROWSER_WAIT_EVENTS` event
3923
4218
  * @returns Promise expression resolving true on a match or change, false at the deadline
3924
4219
  */
3925
4220
  function compileQueryWaitExpression(deadline, key, predicate) {
3926
4221
  return `new Promise((resolve) => {
3927
- let frame
4222
+ let task
3928
4223
  let timer
3929
4224
  let observer
4225
+ const events = new AbortController()
3930
4226
  const finish = (value) => {
3931
4227
  observer?.disconnect()
3932
- if (frame !== undefined) cancelAnimationFrame(frame)
4228
+ events.abort()
4229
+ if (task !== undefined) clearTimeout(task)
3933
4230
  clearTimeout(timer)
3934
4231
  delete globalThis[${JSON.stringify(key)}]
3935
4232
  resolve(value)
3936
4233
  }
3937
4234
  const check = () => {
3938
- frame = undefined
4235
+ task = undefined
3939
4236
  if (${predicate ?? "true"}) finish(true)
3940
4237
  }
3941
4238
  globalThis[${JSON.stringify(key)}] = () => finish(false)
3942
- observer = new MutationObserver(() => {
3943
- if (frame === undefined) frame = requestAnimationFrame(check)
3944
- })
4239
+ const schedule = () => {
4240
+ if (task === undefined) task = setTimeout(check, 0)
4241
+ }
4242
+ observer = new MutationObserver(schedule)
3945
4243
  observer.observe(document, { childList: true, attributes: true, characterData: true, subtree: true })
4244
+ for (const name of ${JSON.stringify(BROWSER_WAIT_EVENTS)}) document.addEventListener(name, schedule, { capture: true, signal: events.signal })
3946
4245
  timer = setTimeout(() => finish(false), ${JSON.stringify(deadline)})
3947
4246
  ${predicate === void 0 ? "" : "check()"}
3948
4247
  })`;
3949
4248
  }
3950
4249
  /**
3951
- * Compiles a visible-text wait coalesced by animation frames.
4250
+ * Compiles a wait for the presence or absence of visible text, coalesced by tasks.
3952
4251
  * @param text - Text to find in the main document body
3953
4252
  * @param deadline - Maximum time in milliseconds
3954
4253
  * @param key - Isolated-world property owning this observer
4254
+ * @param absent - If `true`, resolves when the body's `innerText` lacks `text`; if `false`, when it contains it. Default: `false`
3955
4255
  * @returns Mutation-driven promise expression with one deadline
3956
4256
  */
3957
- function compileTextWaitExpression(text, deadline, key) {
3958
- return compileQueryWaitExpression(deadline, key, `(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)})`);
3959
4259
  }
3960
4260
  /**
3961
4261
  * Compiles text selection or select-option assignment against the resolved element.
@@ -4267,20 +4567,337 @@ ${expression}
4267
4567
  ); const s = JSON.stringify(r); if (typeof s === 'string' && s.length > ${limit}) throw new Error(${JSON.stringify(BROWSER_RESULT_LIMIT_SENTINEL_PREFIX)} + s.length); return r })()`;
4268
4568
  }
4269
4569
  /**
4270
- * Compiles the in-page function that reads the document's URL, title, and serialized HTML.
4570
+ * Compiles the in-page function that reads the document's URL, title, and rendered HTML.
4271
4571
  *
4272
4572
  * @remarks
4273
- * The declaration takes no arguments and returns `{ url, title, html }` from `location.href`,
4274
- * `document.title`, and `document.documentElement.outerHTML`, with `html` empty for a document
4275
- * that has no root element. Call it inside {@link compileGuardedEvaluateExpression} to bound the
4276
- * 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}.
4277
4577
  *
4278
4578
  * @returns A function declaration source
4279
4579
  */
4280
4580
  function compileReadFunction() {
4281
- return `function() {
4282
- const root = document.documentElement
4283
- 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
4284
4901
  }`;
4285
4902
  }
4286
4903
  /**
@@ -4434,7 +5051,7 @@ function compileActionabilityFunction(options) {
4434
5051
  if (${JSON.stringify(options.enabled === true)} && this.matches(':disabled')) throw new Error('Element is disabled')
4435
5052
  if (${JSON.stringify(options.editable === true)} && (this.matches('[readonly]') || (!this.isContentEditable && !('value' in this)))) throw new Error('Element is not editable')
4436
5053
  let previous
4437
- for (let index = 0; index < 2; index += 1) {
5054
+ for (let index = 0; index < ${options.stable === true ? 2 : 0}; index += 1) {
4438
5055
  await new Promise((resolve) => requestAnimationFrame(resolve))
4439
5056
  const rect = this.getBoundingClientRect()
4440
5057
  const current = [rect.x, rect.y, rect.width, rect.height]
@@ -5567,7 +6184,7 @@ var BrowserCodegen = class {
5567
6184
  * title: 'Example',
5568
6185
  * html: '<nav>Menu</nav><main><p>Body</p></main>',
5569
6186
  * })
5570
- * reading.markdown() // { text: 'Body', offset: 0, total: 4 }
6187
+ * reading.markdown({ distill: true }) // { text: 'Body', offset: 0, total: 4 }
5571
6188
  * ```
5572
6189
  */
5573
6190
  var BrowserReading = class {
@@ -5599,7 +6216,7 @@ var BrowserReading = class {
5599
6216
  return this.#navigation !== void 0 && this.#navigation() !== this.#epoch;
5600
6217
  }
5601
6218
  markdown(options) {
5602
- const distill = options?.distill ?? true;
6219
+ const distill = options?.distill ?? false;
5603
6220
  let text = this.#markdowns.get(distill);
5604
6221
  if (text === void 0) {
5605
6222
  text = (0, _orkestrel_markdown.renderMarkdown)((0, _orkestrel_markdown.htmlToMarkdown)(this.#source(distill).document));
@@ -5608,7 +6225,7 @@ var BrowserReading = class {
5608
6225
  return extractBrowserSlice(text, options?.offset, options?.limit);
5609
6226
  }
5610
6227
  text(options) {
5611
- const distill = options?.distill ?? true;
6228
+ const distill = options?.distill ?? false;
5612
6229
  let text = this.#texts.get(distill);
5613
6230
  if (text === void 0) {
5614
6231
  text = (0, _orkestrel_html.renderText)(this.#source(distill).document);
@@ -5617,7 +6234,10 @@ var BrowserReading = class {
5617
6234
  return extractBrowserSlice(text, options?.offset, options?.limit);
5618
6235
  }
5619
6236
  #source(distill) {
5620
- if (!distill) return this.#html;
6237
+ if (!distill) return this.#html.map((node) => node.category === "element" ? {
6238
+ ...node,
6239
+ attributes: (0, _orkestrel_html.resolveAttributes)(node, this.#url)
6240
+ } : node);
5621
6241
  this.#distilled ??= this.#html.distill({ base: this.#url });
5622
6242
  return this.#distilled;
5623
6243
  }
@@ -5688,8 +6308,7 @@ var BrowserPageElement = class {
5688
6308
  async read(options) {
5689
6309
  const epoch = this.#epoch();
5690
6310
  const capture = await this.#call(`function() {
5691
- const capture = (${compileReadFunction()})()
5692
- capture.html = this.outerHTML
6311
+ const capture = (${compileReadFunction()})(this)
5693
6312
  return ${compileGuardedEvaluateExpression("capture", BROWSER_RESULT_LIMIT)}
5694
6313
  }`, options);
5695
6314
  if (!(0, _orkestrel_contract.isRecord)(capture)) throw new BrowserElementError(this.reference, "UNKNOWN", "returned an invalid reading");
@@ -5890,6 +6509,7 @@ var BrowserPageElement = class {
5890
6509
  }
5891
6510
  async #pointer(options) {
5892
6511
  this.#assert(options);
6512
+ await this.#input.page.send("Page.bringToFront", void 0, options);
5893
6513
  await this.#input.client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId: this.#input.backend }, {
5894
6514
  session: this.#input.node.session,
5895
6515
  ...options
@@ -6010,7 +6630,7 @@ var BrowserElementManager = class {
6010
6630
  ...options
6011
6631
  });
6012
6632
  this.#assertCapture(this.#input.page.id, epoch);
6013
- return renderBrowserOutline(this.#input.page.url, requireBrowserString(readEvaluationResult(result), "Document title"), this.#within(rows, options?.within), limit);
6633
+ return renderBrowserOutline(this.#input.page.url, requireBrowserString(readEvaluationResult(result), "Document title"), this.#within(rows, options?.within), limit, options?.search);
6014
6634
  } catch (error) {
6015
6635
  if (options?.signal?.aborted === true || this.#changes === changes) throw error;
6016
6636
  this.#lifetime.signal.throwIfAborted();
@@ -6100,34 +6720,52 @@ var BrowserElementManager = class {
6100
6720
  const signal = options?.signal === void 0 ? this.#lifetime.signal : AbortSignal.any([options.signal, this.#lifetime.signal]);
6101
6721
  while (true) {
6102
6722
  signal.throwIfAborted();
6103
- const remaining = Math.max(0, end - performance.now());
6104
- const call = {
6105
- timeout: remaining,
6106
- signal
6107
- };
6108
- await this.#input.ready(call);
6109
- const frame = this.#scopeFrame(query.within);
6110
- const session = await this.#input.resolve(frame);
6111
- const context = await this.#input.world(frame, session, call);
6112
6723
  const key = `__browserQueryWait${++this.#sequence}`;
6113
- const pending = this.#input.client.send("Runtime.evaluate", {
6114
- expression: compileQueryWaitExpression(remaining, key),
6115
- contextId: context,
6116
- returnByValue: true,
6117
- awaitPromise: true
6118
- }, {
6119
- session,
6120
- timeout: remaining + 1e3,
6121
- signal
6122
- }).then((result) => ({ result }), (error) => ({ error }));
6724
+ let session;
6725
+ let context;
6726
+ const changes = this.#changes;
6123
6727
  try {
6728
+ await this.#input.ready({
6729
+ timeout: this.#remaining(end),
6730
+ signal
6731
+ });
6732
+ const frame = this.#scopeFrame(query.within);
6733
+ session = await this.#input.resolve(frame);
6734
+ context = await this.#input.world(frame, session, {
6735
+ timeout: this.#remaining(end),
6736
+ signal
6737
+ });
6738
+ const remaining = this.#remaining(end);
6739
+ const call = {
6740
+ timeout: remaining,
6741
+ signal
6742
+ };
6743
+ const pending = this.#input.client.send("Runtime.evaluate", {
6744
+ expression: compileQueryWaitExpression(remaining, key),
6745
+ contextId: context,
6746
+ returnByValue: true,
6747
+ awaitPromise: true
6748
+ }, {
6749
+ session,
6750
+ timeout: remaining + 1e3,
6751
+ signal
6752
+ }).then((result) => ({ result }), (error) => ({ error }));
6124
6753
  const found = await this.find(query, call);
6125
6754
  if (options?.absent === true ? found.length === 0 : found.length > 0) return found;
6126
6755
  const outcome = await pending;
6127
6756
  if ("error" in outcome) throw outcome.error;
6128
6757
  if (readEvaluationResult(outcome.result) !== true || performance.now() >= end) throw new BrowserError("Element wait timed out", "BROWSER_WAIT_TIMEOUT");
6758
+ } catch (error) {
6759
+ signal.throwIfAborted();
6760
+ if (isCDPTimeoutError(error)) throw new BrowserError("Element wait timed out", "BROWSER_WAIT_TIMEOUT");
6761
+ if (this.#input.page.closed || this.#changes === changes && (!(0, _orkestrel_contract.isError)(error) || !BROWSER_CONTEXT_LOSS_PATTERN.test(error.message))) throw error;
6762
+ const remaining = this.#remaining(end);
6763
+ if (this.#changes === changes) await this.#input.recover({
6764
+ signal,
6765
+ timeout: remaining
6766
+ });
6129
6767
  } finally {
6130
- await this.#input.client.send("Runtime.evaluate", {
6768
+ if (context !== void 0 && session !== void 0) await this.#input.client.send("Runtime.evaluate", {
6131
6769
  expression: `globalThis[${JSON.stringify(key)}]?.()`,
6132
6770
  contextId: context,
6133
6771
  returnByValue: true
@@ -6138,6 +6776,11 @@ var BrowserElementManager = class {
6138
6776
  }
6139
6777
  }
6140
6778
  }
6779
+ #remaining(end) {
6780
+ const remaining = end - performance.now();
6781
+ if (remaining <= 0) throw new BrowserError("Element wait timed out", "BROWSER_WAIT_TIMEOUT");
6782
+ return remaining;
6783
+ }
6141
6784
  element(reference) {
6142
6785
  const canonical = parseBrowserReference(reference);
6143
6786
  return [...this.#records.values()].find((entry) => entry.node.reference === canonical)?.element;
@@ -6645,7 +7288,7 @@ var BrowserRegistry = class {
6645
7288
  pending.reject(error);
6646
7289
  }
6647
7290
  async #executeAdopted(tool, synthetic, input, context) {
6648
- const parameters = synthetic ? Object.fromEntries(Object.entries(input).filter(([key]) => key !== "what")) : input;
7291
+ const parameters = synthetic ? Object.fromEntries(Object.entries(input).filter(([key]) => key !== "purpose")) : input;
6649
7292
  const result = await this.execute(tool, parameters, { signal: context.signal });
6650
7293
  if (result.status !== "Completed") throw new BrowserError(`Browser tool ${result.status}: ${result.error ?? "No error description"}`.slice(0, BROWSER_REGISTRY_OUTPUT_LIMIT));
6651
7294
  return renderBrowserToolOutput(result.output);
@@ -8962,6 +9605,8 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
8962
9605
  #epoch = 0;
8963
9606
  #floor = 0;
8964
9607
  #closed = false;
9608
+ #waitRelease = new AbortController();
9609
+ #textWaits = /* @__PURE__ */ new Map();
8965
9610
  #codegen;
8966
9611
  #registry;
8967
9612
  #codegenStart = new BrowserTransition();
@@ -9012,6 +9657,7 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9012
9657
  session: sessionId,
9013
9658
  resolve: this.#resolveFrameSession.bind(this),
9014
9659
  world: this.#world.bind(this),
9660
+ recover: this.#recoverReadiness.bind(this),
9015
9661
  reference: this.#reference,
9016
9662
  ready: this.#ready.bind(this)
9017
9663
  });
@@ -9075,6 +9721,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9075
9721
  }
9076
9722
  async wait(text, options) {
9077
9723
  this.assert();
9724
+ options = {
9725
+ ...options,
9726
+ signal: options?.signal === void 0 ? this.#waitRelease.signal : AbortSignal.any([options.signal, this.#waitRelease.signal])
9727
+ };
9078
9728
  const timeout = options?.timeout ?? 3e4;
9079
9729
  validateBrowserTimeout(timeout);
9080
9730
  const end = performance.now() + timeout;
@@ -9087,9 +9737,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9087
9737
  });
9088
9738
  const context = await this.#world(this.id, this.#sessionId, options);
9089
9739
  const key = `__browserTextWait${++this.#waitSequence}`;
9740
+ this.#textWaits.set(key, context);
9090
9741
  try {
9091
9742
  if (readEvaluationResult(await this.send("Runtime.evaluate", {
9092
- expression: compileTextWaitExpression(text, remaining, key),
9743
+ expression: compileTextWaitExpression(text, remaining, key, options?.absent === true),
9093
9744
  contextId: context,
9094
9745
  returnByValue: true,
9095
9746
  awaitPromise: true
@@ -9103,23 +9754,20 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9103
9754
  return;
9104
9755
  } catch (error) {
9105
9756
  if (options?.signal?.aborted === true) {
9106
- await this.send("Runtime.evaluate", {
9107
- expression: `globalThis[${JSON.stringify(key)}]?.()`,
9108
- contextId: context,
9109
- returnByValue: true
9110
- }, { timeout: 1e3 }).catch(() => void 0);
9757
+ if (!this.#closed) await this.#releaseTextWait(key, context);
9111
9758
  throw options.signal.reason;
9112
9759
  }
9113
- if (!(0, _orkestrel_contract.isError)(error) || !/execution context was destroyed|cannot find context with specified id/i.test(error.message)) throw error;
9114
- this.#dom = void 0;
9760
+ if (!(0, _orkestrel_contract.isError)(error) || !BROWSER_CONTEXT_LOSS_PATTERN.test(error.message)) throw error;
9115
9761
  if (performance.now() >= end) throw new BrowserError("Browser text wait timed out", "BROWSER_WAIT_TIMEOUT", {
9116
9762
  text,
9117
9763
  timeout
9118
9764
  });
9119
- await this.#parkReadiness({
9765
+ await this.#recoverReadiness({
9120
9766
  ...options,
9121
9767
  timeout: Math.max(0, end - performance.now())
9122
9768
  });
9769
+ } finally {
9770
+ this.#textWaits.delete(key);
9123
9771
  }
9124
9772
  }
9125
9773
  }
@@ -9507,6 +10155,8 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9507
10155
  return release;
9508
10156
  }
9509
10157
  async #releaseResources() {
10158
+ this.#waitRelease.abort(new BrowserError("Browser page is closed"));
10159
+ await Promise.all([...this.#textWaits].map(([key, context]) => this.#releaseTextWait(key, context)));
9510
10160
  this.#unhold();
9511
10161
  this.#announcement.reject(new BrowserError("Browser session ended"));
9512
10162
  this.#client.unsubscribe("Page.lifecycleEvent", this.#lifecycleHandler, this.#sessionId);
@@ -9556,6 +10206,16 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9556
10206
  }
9557
10207
  this.#steps.destroy();
9558
10208
  }
10209
+ async #releaseTextWait(key, context) {
10210
+ await this.#client.send("Runtime.evaluate", {
10211
+ expression: `globalThis[${JSON.stringify(key)}]?.()`,
10212
+ contextId: context,
10213
+ returnByValue: true
10214
+ }, {
10215
+ session: this.#sessionId,
10216
+ timeout: 1e3
10217
+ }).catch(() => void 0);
10218
+ }
9559
10219
  #frame(frame) {
9560
10220
  return new BrowserFrame(this.#client, (id) => this.#resolveFrameSession(id), frame.id, frame.url, frame.parent, frame.name, true, () => this.#epochOf(frame.id), (session, call) => this.#world(frame.id, session, call));
9561
10221
  }
@@ -9599,6 +10259,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9599
10259
  #rejectSeed(seed) {
9600
10260
  if (this.#seed === seed) this.#seed = void 0;
9601
10261
  }
10262
+ #recoverReadiness(options) {
10263
+ this.#dom = void 0;
10264
+ return this.#parkReadiness(options);
10265
+ }
9602
10266
  async #seedReadiness(options) {
9603
10267
  const document = this.#floor;
9604
10268
  const context = await this.#world(this.id, this.#sessionId, options);
@@ -9646,24 +10310,44 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
9646
10310
  this.#dom = this.#loader ?? "initial";
9647
10311
  for (const id of this.#readiness.keys()) this.#settleReadiness(id)?.resolve();
9648
10312
  }
9649
- #world(frame, session, options) {
10313
+ async #world(frame, session, options) {
10314
+ options?.signal?.throwIfAborted();
9650
10315
  const cached = this.#worlds.get(frame);
9651
10316
  if (cached !== void 0) return Promise.resolve(cached.context);
9652
10317
  const existing = this.#creating.get(frame);
9653
- if (existing !== void 0) return existing.promise;
10318
+ if (existing !== void 0) return await this.#awaitWorld(existing.promise, options);
9654
10319
  const pending = this.#client.send("Page.createIsolatedWorld", {
9655
10320
  frameId: frame,
9656
10321
  worldName: BROWSER_FRAME_WORLD_NAME
9657
10322
  }, {
9658
10323
  session,
9659
- ...options
10324
+ signal: this.#waitRelease.signal
9660
10325
  }).then(this.#decodeWorld.bind(this, frame));
9661
10326
  this.#creating.set(frame, {
9662
10327
  session,
9663
10328
  promise: pending
9664
10329
  });
9665
10330
  pending.then(this.#publishWorld.bind(this, frame, session, pending), this.#settleWorld.bind(this, frame, pending));
9666
- return pending;
10331
+ return await this.#awaitWorld(pending, options);
10332
+ }
10333
+ async #awaitWorld(pending, options) {
10334
+ const signal = options?.signal;
10335
+ signal?.throwIfAborted();
10336
+ const timeout = options?.timeout ?? 3e4;
10337
+ validateBrowserTimeout(timeout);
10338
+ const abandoned = Promise.withResolvers();
10339
+ const abort = this.#abandonWorld.bind(this, abandoned, signal);
10340
+ const timer = setTimeout(() => abandoned.reject(new CDPTimeoutError("Isolated world wait timed out")), timeout);
10341
+ signal?.addEventListener("abort", abort, { once: true });
10342
+ try {
10343
+ return await Promise.race([pending, abandoned.promise]);
10344
+ } finally {
10345
+ clearTimeout(timer);
10346
+ signal?.removeEventListener("abort", abort);
10347
+ }
10348
+ }
10349
+ #abandonWorld(pending, signal) {
10350
+ pending.reject(signal?.reason);
9667
10351
  }
9668
10352
  #decodeWorld(frame, world) {
9669
10353
  return readBrowserWorld(world, frame);
@@ -10500,6 +11184,7 @@ var BrowserContext = class {
10500
11184
  #viewport;
10501
11185
  #writer;
10502
11186
  #downloads;
11187
+ #allocator;
10503
11188
  #emitter;
10504
11189
  #cookies;
10505
11190
  #permissions;
@@ -10519,6 +11204,7 @@ var BrowserContext = class {
10519
11204
  this.#viewport = viewport;
10520
11205
  this.#writer = writer;
10521
11206
  this.#downloads = downloads;
11207
+ this.#allocator = options?.reference;
10522
11208
  this.#emitter = new _orkestrel_emitter.Emitter({
10523
11209
  ...options?.on !== void 0 ? { on: options.on } : {},
10524
11210
  ...options?.error !== void 0 ? { error: options.error } : {}
@@ -10655,7 +11341,7 @@ var BrowserContext = class {
10655
11341
  sessionId = await this.#openSession(targetId);
10656
11342
  await this.#enableSession(sessionId);
10657
11343
  const frameId = await this.#mainFrame(sessionId);
10658
- page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, options, this.#nextReference.bind(this), ready);
11344
+ page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, options, this.#allocator ?? this.#nextReference.bind(this), ready);
10659
11345
  this.#observe(page);
10660
11346
  await this.#configurePage(page);
10661
11347
  await this.#emulation.attach(page);
@@ -10674,7 +11360,7 @@ var BrowserContext = class {
10674
11360
  sessionId = await this.#openSession(targetId);
10675
11361
  await this.#enableSession(sessionId);
10676
11362
  const frameId = await this.#mainFrame(sessionId);
10677
- page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, void 0, this.#nextReference.bind(this), ready);
11363
+ page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, void 0, this.#allocator ?? this.#nextReference.bind(this), ready);
10678
11364
  this.#observe(page);
10679
11365
  await this.#configurePage(page);
10680
11366
  await this.#emulation.attach(page);
@@ -11494,7 +12180,7 @@ function createBrowserReading(input) {
11494
12180
  *
11495
12181
  * const toolset = createBrowserToolset(page, { context })
11496
12182
  * await toolset.start()
11497
- * const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: { what: 'cart' } })
12183
+ * const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: { search: 'cart' } })
11498
12184
  * ```
11499
12185
  *
11500
12186
  * @example Drive a page with a small model
@@ -11508,8 +12194,8 @@ function createBrowserReading(input) {
11508
12194
  * const system =
11509
12195
  * 'You control a web browser with tools and must call a tool before you answer. ' +
11510
12196
  * 'The first message shows the page as look returns it; references such as e4 name its elements. ' +
11511
- * 'To learn a fact, call read with what set to your question; when its result ends by naming an offset, call read again with that offset. ' +
11512
- * 'To search, call type with the search box reference, the words, and submit true. ' +
12197
+ * '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. ' +
12198
+ * "To use the site's search box, call type with its reference, the words, and submit true. " +
11513
12199
  * 'To press a button or follow a link, call click with its reference from the latest result. Never invent a reference. ' +
11514
12200
  * 'If text you expect has not appeared, call wait once. ' +
11515
12201
  * 'When the task is done, answer in one short sentence.'
@@ -11519,11 +12205,11 @@ function createBrowserReading(input) {
11519
12205
  * const page = await browser.create({ url: 'https://shop.example.test/' })
11520
12206
  * const toolset = createBrowserToolset(page, { tools: createToolManager() })
11521
12207
  * await toolset.start()
11522
- * toolset.tools.tools().map((tool) => tool.name) // ['look', 'read', 'click', 'type', 'press', 'navigate', 'wait']
12208
+ * toolset.tools.tools().map((tool) => tool.name) // ['look', 'read', 'plain', 'click', 'type', 'press', 'navigate', 'wait']
11523
12209
  * const seeded = await toolset.tools.execute({
11524
12210
  * id: 'seed',
11525
12211
  * name: 'look',
11526
- * arguments: { what: 'the page' },
12212
+ * arguments: { search: '' },
11527
12213
  * })
11528
12214
  * const view = seeded.success ? String(seeded.value) : seeded.error
11529
12215
  * const agent = createAgent(createOllama({ model: 'qwen3.5:2b-q4_K_M' }), {
@@ -11743,6 +12429,10 @@ var BrowserJourneyToolset = class {
11743
12429
  const offset = args["offset"] ?? 0;
11744
12430
  if (!(0, _orkestrel_contract.isInteger)(offset) || offset < 0) throw new BrowserError("The offset parameter must be a non-negative integer.", "BROWSER_JOURNEY_ARGUMENT", { key: "offset" });
11745
12431
  const listings = [];
12432
+ const headings = [];
12433
+ const offsets = /* @__PURE__ */ new Map();
12434
+ let headingOffset = 0;
12435
+ let listingOffset = 0;
11746
12436
  const faults = /* @__PURE__ */ new Set();
11747
12437
  let position = 0;
11748
12438
  for (;;) {
@@ -11750,11 +12440,21 @@ var BrowserJourneyToolset = class {
11750
12440
  signal,
11751
12441
  offset: position
11752
12442
  });
11753
- listings.push(...page.entries.map((entry) => renderBrowserJourney(entry.journey)));
12443
+ for (const entry of page.entries) {
12444
+ const listing = renderBrowserJourney(entry.journey);
12445
+ const heading = listing.split("\n", 1)[0] ?? "";
12446
+ headings.push(heading);
12447
+ offsets.set(headingOffset, listingOffset);
12448
+ headingOffset += heading.length + 1;
12449
+ listingOffset += listing.length + 2;
12450
+ listings.push(listing);
12451
+ }
11754
12452
  for (const fault of page.faults) {
11755
12453
  if (faults.has(fault.name)) continue;
11756
12454
  faults.add(fault.name);
11757
- listings.push(renderBrowserJourneyFault(fault));
12455
+ const listing = renderBrowserJourneyFault(fault);
12456
+ listings.push(listing);
12457
+ listingOffset += listing.length + 2;
11758
12458
  }
11759
12459
  const span = page.entries.length;
11760
12460
  position += span;
@@ -11763,7 +12463,11 @@ var BrowserJourneyToolset = class {
11763
12463
  if (listings.length === 0) return BROWSER_JOURNEY_EMPTY_LISTING;
11764
12464
  const listing = listings.join("\n\n");
11765
12465
  const start = offset < listing.length ? offset : 0;
11766
- let end = Math.min(listing.length, start + this.#limit);
12466
+ const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
12467
+ const matches = start === 0 ? scanBrowserText(headings.join("\n"), search) : [];
12468
+ const count = matches.length;
12469
+ const block = renderBrowserMatches(`${count} ${count === 1 ? "journey matches" : "journeys match"} ${JSON.stringify(search)}:`, matches.map((match) => `[${offsets.get(match.offset)}] ${match.text}`), this.#limit);
12470
+ let end = Math.min(listing.length, start + this.#limit - block.length);
11767
12471
  const last = listing.charCodeAt(end - 1);
11768
12472
  if (end < listing.length && last >= 55296 && last <= 56319) end -= 1;
11769
12473
  if (end === start) throw new BrowserError(`The journeys limit of ${this.#limit} characters cannot hold the next character at offset ${start}; raise the journeys limit.`, "BROWSER_TOOLSET_LIMIT", {
@@ -11771,8 +12475,8 @@ var BrowserJourneyToolset = class {
11771
12475
  offset: start
11772
12476
  });
11773
12477
  const text = listing.slice(start, end);
11774
- if (start === 0 && end === listing.length) return text;
11775
- return `${text}\n\n[characters ${start}–${end} of ${listing.length}${end < listing.length ? `; call journeys with offset ${end} for more` : ""}]`;
12478
+ if (start === 0 && end === listing.length) return `${block}${text}`;
12479
+ return `${block}${text}\n\n[characters ${start}–${end} of ${listing.length}${end < listing.length ? `; call journeys with offset ${end} for more` : ""}]`;
11776
12480
  }
11777
12481
  async #edit(args, signal) {
11778
12482
  if (this.#readonly) throw new BrowserError(BROWSER_JOURNEY_READONLY_REFUSAL, "BROWSER_JOURNEY_READONLY");
@@ -11938,7 +12642,7 @@ var BrowserJourneyToolset = class {
11938
12642
  const performed = await this.#toolset.perform({
11939
12643
  id: "journey",
11940
12644
  name: "look",
11941
- arguments: { what: "the page" }
12645
+ arguments: { search: "" }
11942
12646
  }, { signal });
11943
12647
  return performed.result.success ? String(performed.result.value) : performed.result.error;
11944
12648
  }
@@ -11966,7 +12670,7 @@ var BrowserJourneyToolset = class {
11966
12670
  * view's own tools beside them.
11967
12671
  *
11968
12672
  * @remarks
11969
- * Every toolset advertises `look`, `read`, `click`, `type`, and `wait`, which need only the
12673
+ * Every toolset advertises `look`, `read`, `plain`, `click`, `type`, and `wait`, which need only the
11970
12674
  * `BrowserViewInterface` it is constructed over. With `options.page` it also advertises `press`
11971
12675
  * and `navigate`, stages the `dialog` tool while a dialog is open on the current page, follows a
11972
12676
  * popup the current page opens and returns to the opener when the popup closes, and adopts
@@ -11980,11 +12684,18 @@ var BrowserJourneyToolset = class {
11980
12684
  * refuses every tool but `dialog` while a dialog is open, forwards `ToolContext.signal` into every
11981
12685
  * protocol call, refuses a call to a toolset tool that carries a parameter the tool does not
11982
12686
  * advertise, and cuts every returned string and every thrown message at `limit` characters plus
11983
- * a footer; the footer of a cut result that carries a view names `read` as the next call. `look`
11984
- * and `read` read the page, never one element. A page tool's error message and JSON output reach
11985
- * the toolset already cut at `BROWSER_REGISTRY_OUTPUT_LIMIT` (4 096) by the registry, so a
11986
- * `limit` over that shows at most 4 096 characters of either; a page tool's text output reaches
11987
- * the boundary whole.
12687
+ * a footer; the footer of a cut action or `dialog` receipt that carries a view names `look` as the next
12688
+ * call. `look`, `read`, and `plain` read the page and return pages of at most `limit`
12689
+ * characters and name the offset the next page starts at. A page ends after the last line break
12690
+ * in its window that lies past the page's start, or at the window's end when no such break fits,
12691
+ * without splitting a surrogate pair. The last page ends at the end of the outline or reading. A `look` whose
12692
+ * `search` matches referenced rows lists those rows first, and its pages reach every referenced row.
12693
+ * A `press` receipt names the referenced element that has focus after the key. A page tool's
12694
+ * error message and JSON output reach the toolset already cut at `BROWSER_REGISTRY_OUTPUT_LIMIT`
12695
+ * (4 096) by the registry, so a `limit` over that shows at most 4 096 characters of either; a page
12696
+ * tool's text output reaches the boundary whole.
12697
+ * `wait` with `absent` set to `true` settles when the text is not on the page, and a miss
12698
+ * returns the `timeout` outcome as an appearance miss does.
11988
12699
  *
11989
12700
  * `click`, `type`, `press`, `navigate`, and `switch` are actions: one runs at a time, in call
11990
12701
  * order, and holds the queue until its receipt. A queued action whose signal aborts leaves
@@ -12015,7 +12726,7 @@ var BrowserJourneyToolset = class {
12015
12726
  * page placement's `type` with `submit` ends its action with `and submitted the form` when a read
12016
12727
  * recorded a submission, or when the navigation the receipt settled names `formSubmissionGet` or
12017
12728
  * `formSubmissionPost` as its reason, whatever the read answered, and with `and pressed Enter`
12018
- * otherwise. A `read`
12729
+ * otherwise. A `read` or `plain`
12019
12730
  * at an offset at or past its retained reading's end restarts that reading at 0. A receipt that
12020
12731
  * waits for a requested navigation shares one `BROWSER_TOOL_TIMEOUT_MS` deadline between that
12021
12732
  * wait and its view capture, of which `BROWSER_TOOL_CAPTURE_MS` is reserved for the capture. The
@@ -12031,9 +12742,9 @@ var BrowserJourneyToolset = class {
12031
12742
  * A page tool is skipped, with `skip` emitted, when its name is reserved (`unresolved` included), when the manager holds
12032
12743
  * its name under a tool the toolset did not add (checked again immediately before each addition),
12033
12744
  * when its name falls outside `BROWSER_TOOL_NAME_PATTERN`, when its parameters or, for a source
12034
- * that supplies `tools()`, its selected registration declare `what` optional, or when that
12745
+ * that supplies `tools()`, its selected registration declare `purpose` optional, or when that
12035
12746
  * registration is marked `debugging`. The selected registration is the main frame's, then the
12036
- * earlier one. A page tool whose parameters require nothing advertises a required `what`, which
12747
+ * earlier one. A page tool whose parameters require nothing advertises a required `purpose`, which
12037
12748
  * is stripped before the tool runs. Every adopted tool is advertised `untrusted`. `destroy()`
12038
12749
  * removes only the tools the manager still holds under the instances the toolset added, never
12039
12750
  * closes a page, and ends only what `options.release` hands over, calling it one time last.
@@ -12044,7 +12755,7 @@ var BrowserJourneyToolset = class {
12044
12755
  *
12045
12756
  * const toolset = new BrowserToolset(page, { page })
12046
12757
  * await toolset.start()
12047
- * const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: { what: 'cart' } })
12758
+ * const result = await toolset.tools.execute({ id: '1', name: 'look', arguments: { search: 'cart' } })
12048
12759
  * await toolset.destroy()
12049
12760
  * ```
12050
12761
  */
@@ -12108,20 +12819,23 @@ var BrowserToolset = class {
12108
12819
  ...options?.on === void 0 ? {} : { on: options.on },
12109
12820
  ...options?.error === void 0 ? {} : { error: options.error }
12110
12821
  });
12111
- const look = this.#create("look", this.#look.bind(this), BROWSER_TOOL_VIEW_FOOTER);
12112
- const read = this.#create("read", this.#read.bind(this), BROWSER_TOOL_CUT_FOOTER);
12822
+ const look = this.#create("look", this.#look.bind(this), BROWSER_TOOL_CUT_FOOTER);
12823
+ const read = this.#create("read", this.#read.bind(this, "read", (reading) => reading.markdown()), BROWSER_TOOL_CUT_FOOTER);
12824
+ const plain = this.#create("plain", this.#read.bind(this, "plain", (reading) => reading.text()), BROWSER_TOOL_CUT_FOOTER);
12113
12825
  const click = this.#create("click", this.#click.bind(this), BROWSER_TOOL_VIEW_FOOTER);
12114
12826
  const type = this.#create("type", this.#type.bind(this), BROWSER_TOOL_VIEW_FOOTER);
12115
12827
  const wait = this.#create("wait", this.#wait.bind(this), BROWSER_TOOL_CUT_FOOTER);
12116
12828
  this.#native = Object.freeze(options?.page === void 0 ? [
12117
12829
  look,
12118
12830
  read,
12831
+ plain,
12119
12832
  click,
12120
12833
  type,
12121
12834
  wait
12122
12835
  ] : [
12123
12836
  look,
12124
12837
  read,
12838
+ plain,
12125
12839
  click,
12126
12840
  type,
12127
12841
  this.#create("press", this.#press.bind(this), BROWSER_TOOL_VIEW_FOOTER),
@@ -12424,44 +13138,74 @@ var BrowserToolset = class {
12424
13138
  if ((0, _orkestrel_contract.isString)(secret) && secret.length > 0) message = message.replaceAll(JSON.stringify(secret), "[redacted]").replaceAll(secret, "[redacted]");
12425
13139
  return clause === void 0 ? message : boundBrowserText(message, this.#limit, clause);
12426
13140
  }
12427
- async #look(_args, context) {
12428
- return [(await this.#race(this.#cursor.elements.outline({ signal: context.signal }), "")).text, ""];
13141
+ async #look(args, context) {
13142
+ const offset = this.#readOffset(args);
13143
+ const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
13144
+ const outline = await this.#race(this.#cursor.elements.outline({
13145
+ signal: context.signal,
13146
+ limit: Number.MAX_SAFE_INTEGER,
13147
+ search
13148
+ }), "");
13149
+ const total = outline.text.length;
13150
+ const start = offset < total ? offset : 0;
13151
+ const note = this.#drainNote();
13152
+ const room = this.#limit - note.length;
13153
+ const count = outline.matches.length;
13154
+ const block = start === 0 ? renderBrowserMatches(`${count} ${count === 1 ? "element matches" : "elements match"} ${JSON.stringify(search)}:`, outline.matches, room) : "";
13155
+ const space = room - block.length;
13156
+ const slice = space < 1 ? {
13157
+ text: "",
13158
+ offset: start,
13159
+ total
13160
+ } : extractBrowserSlice(outline.text, start, space);
13161
+ return this.#pageSlice("look", note, slice, block);
12429
13162
  }
12430
- async #read(args, context) {
12431
- const offset = args["offset"] ?? 0;
12432
- if (!(0, _orkestrel_contract.isInteger)(offset) || offset < 0) throw new BrowserError("The offset parameter must be a non-negative integer.", "BROWSER_TOOLSET_ARGUMENT", { key: "offset" });
13163
+ async #read(name, project, args, context) {
13164
+ const offset = this.#readOffset(args);
12433
13165
  const view = this.#cursor;
12434
13166
  const retained = this.#reading;
12435
13167
  let reading = retained?.reading;
12436
- if (offset === 0 || retained === void 0 || reading === void 0 || retained.view !== view || reading.stale) {
13168
+ if (offset === 0 || retained === void 0 || reading === void 0 || retained.name !== name || retained.view !== view || reading.stale) {
12437
13169
  reading = await this.#race(view.read({ signal: context.signal }), "", context.signal);
12438
13170
  this.#reading = {
12439
13171
  view,
12440
- reading
13172
+ reading,
13173
+ name
12441
13174
  };
12442
13175
  }
12443
- const start = reading === retained?.reading && offset < reading.markdown({
12444
- offset: 0,
12445
- limit: 1
12446
- }).total ? offset : 0;
12447
- const note = boundBrowserText(this.#drain(), Math.max(1, this.#limit - 1), BROWSER_TOOL_CUT_FOOTER);
13176
+ const whole = project(reading);
13177
+ const start = reading === retained?.reading && offset < whole.total ? offset : 0;
13178
+ const note = this.#drainNote();
12448
13179
  const room = this.#limit - note.length;
12449
- const slice = room < 1 ? {
13180
+ const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
13181
+ const matches = start === 0 ? scanBrowserText(whole.text, search) : [];
13182
+ const count = matches.length;
13183
+ const block = renderBrowserMatches(`${count} ${count === 1 ? "line matches" : "lines match"} ${JSON.stringify(search)}:`, matches.map((match) => `[${match.offset}] ${match.text}`), room);
13184
+ const space = room - block.length;
13185
+ const slice = space < 1 ? {
12450
13186
  text: "",
12451
13187
  offset: start,
12452
- total: reading.markdown({ offset: 0 }).total
12453
- } : reading.markdown({
12454
- offset: start,
12455
- limit: room
12456
- });
12457
- 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", {
13188
+ total: whole.total
13189
+ } : extractBrowserSlice(whole.text, start, space);
13190
+ return this.#pageSlice(name, note, slice, block);
13191
+ }
13192
+ #readOffset(args) {
13193
+ const offset = args["offset"] ?? 0;
13194
+ 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" });
13195
+ return offset;
13196
+ }
13197
+ #drainNote() {
13198
+ return boundBrowserText(this.#drain(), Math.max(1, this.#limit - 1), BROWSER_TOOL_CUT_FOOTER);
13199
+ }
13200
+ #pageSlice(name, note, slice, block = "") {
13201
+ if (note === "" && slice.text === "" && slice.offset < slice.total) throw new BrowserError(`The ${name} limit of ${this.#limit} characters cannot hold the next character at offset ${slice.offset}; raise the toolset limit.`, "BROWSER_TOOLSET_LIMIT", {
12458
13202
  limit: this.#limit,
12459
- offset: start
13203
+ offset: slice.offset
12460
13204
  });
12461
13205
  const end = slice.offset + slice.text.length;
12462
13206
  const more = end < slice.total;
12463
- if (start === 0 && !more) return [`${note}${slice.text}`, ""];
12464
- return [`${note}${slice.text}`, `\n\n[characters ${start}–${end} of ${slice.total}${more ? `; call read with offset ${end} for more` : ""}]`];
13207
+ if (slice.offset === 0 && !more) return [`${note}${block}${slice.text}`, ""];
13208
+ return [`${note}${block}${slice.text}`, `\n\n[characters ${slice.offset}–${end} of ${slice.total}${more ? `; call ${name} with offset ${end} for more` : ""}]`];
12465
13209
  }
12466
13210
  async #click(args, context) {
12467
13211
  const turn = await this.#acquire(context.signal);
@@ -12564,7 +13308,7 @@ var BrowserToolset = class {
12564
13308
  return [await this.#settle(page.keyboard.press(key), action, context.signal, true, record, popups, observation, key === "Enter" ? {
12565
13309
  explicit: false,
12566
13310
  action
12567
- } : void 0), ""];
13311
+ } : void 0, true), ""];
12568
13312
  } finally {
12569
13313
  record?.destroy();
12570
13314
  popups?.destroy();
@@ -12603,7 +13347,7 @@ var BrowserToolset = class {
12603
13347
  const action = `Navigated to ${result.url}`;
12604
13348
  return [renderBrowserReceipt({
12605
13349
  action,
12606
- view: await this.#capture(action, context.signal)
13350
+ view: (await this.#capture(action, context.signal)).view
12607
13351
  }), ""];
12608
13352
  } finally {
12609
13353
  if (this.#navigating === page) this.#navigating = void 0;
@@ -12614,22 +13358,25 @@ var BrowserToolset = class {
12614
13358
  async #wait(args, context) {
12615
13359
  const text = readBrowserToolString(args, "text");
12616
13360
  const seconds = args["timeout"];
13361
+ const absent = args["absent"];
13362
+ if (absent !== void 0 && !(0, _orkestrel_contract.isBoolean)(absent)) throw new BrowserError("The absent parameter must be a boolean.", "BROWSER_TOOLSET_ARGUMENT", { key: "absent" });
12617
13363
  if (seconds !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(seconds) || seconds <= 0)) throw new BrowserError("The timeout parameter must be a positive number of seconds.", "BROWSER_TOOLSET_ARGUMENT", { key: "timeout" });
12618
13364
  const timeout = Math.min(seconds === void 0 ? BROWSER_TOOL_TIMEOUT_MS : Math.ceil(seconds * 1e3), BROWSER_TOOL_TIMEOUT_LIMIT_MS);
12619
13365
  const quoted = JSON.stringify(text);
12620
13366
  try {
12621
13367
  await this.#race(this.#cursor.wait(text, {
12622
13368
  timeout,
12623
- signal: context.signal
12624
- }), `Waited for ${quoted}`, context.signal);
12625
- return [`${quoted} is on the page.`, ""];
13369
+ signal: context.signal,
13370
+ absent: absent === true
13371
+ }), `Waited for ${quoted}${absent === true ? " to leave" : ""}`, context.signal);
13372
+ return [`${quoted} is ${absent === true ? "not " : ""}on the page.`, ""];
12626
13373
  } catch (error) {
12627
13374
  if (!context.signal.aborted && isBrowserError(error) && error.code === "BROWSER_WAIT_TIMEOUT") {
12628
13375
  this.#actions.set(context.signal, {
12629
13376
  ...this.#actions.get(context.signal),
12630
13377
  outcome: "timeout"
12631
13378
  });
12632
- return [`${quoted} did not appear within ${timeout / 1e3} s.`, ""];
13379
+ return [absent === true ? `${quoted} is still on the page after ${timeout / 1e3} s.` : `${quoted} did not appear within ${timeout / 1e3} s.`, ""];
12633
13380
  }
12634
13381
  throw error;
12635
13382
  }
@@ -12652,14 +13399,19 @@ var BrowserToolset = class {
12652
13399
  const action = `${accept ? "Accepted" : "Dismissed"} the ${dialog.category} dialog ${JSON.stringify(dialog.message)}`;
12653
13400
  return [renderBrowserReceipt({
12654
13401
  action,
12655
- view: await this.#capture(action, context.signal)
13402
+ view: (await this.#capture(action, context.signal)).view
12656
13403
  }), ""];
12657
13404
  } finally {
12658
13405
  turn.resolve();
12659
13406
  }
12660
13407
  }
12661
- async #tabs(_args, context) {
12662
- return [(await this.#list(context.signal, true)).map((tab) => `${tab.id} ${JSON.stringify(tab.title)} ${tab.url}${tab.current ? " (current)" : ""}`).join("\n"), ""];
13408
+ async #tabs(args, context) {
13409
+ const text = (await this.#list(context.signal, true)).map((tab) => `${tab.id} ${JSON.stringify(tab.title)} ${tab.url}${tab.current ? " (current)" : ""}`).join("\n");
13410
+ const search = (0, _orkestrel_contract.isString)(args["search"]) ? args["search"] : "";
13411
+ const matches = scanBrowserText(text, search);
13412
+ const count = matches.length;
13413
+ const note = this.#drainNote();
13414
+ return [`${note}${renderBrowserMatches(`${count} ${count === 1 ? "tab matches" : "tabs match"} ${JSON.stringify(search)}:`, matches.map((match) => match.text), this.#limit - note.length)}${text}`, ""];
12663
13415
  }
12664
13416
  async #switch(args, context) {
12665
13417
  const tab = readBrowserToolString(args, "tab");
@@ -12682,7 +13434,7 @@ var BrowserToolset = class {
12682
13434
  await this.#command(front, action, front);
12683
13435
  return [renderBrowserReceipt({
12684
13436
  action,
12685
- view: await this.#capture(action, context.signal)
13437
+ view: (await this.#capture(action, context.signal)).view
12686
13438
  }), ""];
12687
13439
  } finally {
12688
13440
  turn.resolve();
@@ -12691,7 +13443,7 @@ var BrowserToolset = class {
12691
13443
  async #invoke(tool, synthetic, args, context) {
12692
13444
  const turn = await this.#acquire(context.signal);
12693
13445
  try {
12694
- const input = synthetic ? Object.fromEntries(Object.entries(args).filter(([key]) => key !== "what")) : args;
13446
+ const input = synthetic ? Object.fromEntries(Object.entries(args).filter(([key]) => key !== "purpose")) : args;
12695
13447
  const command = Promise.resolve(tool.execute(input, context));
12696
13448
  return [renderBrowserToolOutput(await this.#command(command, `Called ${tool.name}`, command, context.signal)), ""];
12697
13449
  } finally {
@@ -12771,7 +13523,7 @@ var BrowserToolset = class {
12771
13523
  }
12772
13524
  return turn;
12773
13525
  }
12774
- async #settle(command, action, signal, trusted, record, popups, observation, enter) {
13526
+ async #settle(command, action, signal, trusted, record, popups, observation, enter, focus = false) {
12775
13527
  let deadline;
12776
13528
  try {
12777
13529
  const started = record?.wait({ signal }).then(() => true, () => new Promise(() => void 0));
@@ -12807,11 +13559,13 @@ var BrowserToolset = class {
12807
13559
  const unmoved = settled === void 0 && submissions.destinations.length === 0;
12808
13560
  const status = settled?.stage === "committed" ? `the page is still loading ${settled.url}` : settled?.stage === "requested" ? `it requested ${settled.url} and the page did not change` : unmoved && submissions.prevented ? BROWSER_TOOL_HANDLED_STATUS : unmoved && submissions.submitted === false && (enter?.explicit === true || enter !== void 0 && submissions.implicit) ? "no form received the submission" : void 0;
12809
13561
  const line = enter !== void 0 && (submissions.submitted === true || settled?.reason === "formSubmissionGet" || settled?.reason === "formSubmissionPost") ? enter.action : action;
13562
+ const captured = await this.#capture(line, signal, deadline);
13563
+ const parts = [status, focus && captured.focus !== void 0 ? `focus is on ${captured.focus}` : void 0].filter((part) => part !== void 0);
12810
13564
  return renderBrowserReceipt({
12811
13565
  action: line,
12812
13566
  trusted,
12813
- ...status === void 0 ? {} : { status },
12814
- view: await this.#capture(line, signal, deadline)
13567
+ ...parts.length === 0 ? {} : { status: parts.join("; ") },
13568
+ view: captured.view
12815
13569
  });
12816
13570
  } finally {
12817
13571
  record?.destroy();
@@ -12978,7 +13732,10 @@ var BrowserToolset = class {
12978
13732
  }
12979
13733
  }
12980
13734
  async #capture(action, signal, deadline = performance.now() + BROWSER_TOOL_TIMEOUT_MS) {
12981
- if (deadline - performance.now() < 1) return BROWSER_TOOL_DEADLINE_NOTE;
13735
+ if (deadline - performance.now() < 1) return {
13736
+ view: BROWSER_TOOL_DEADLINE_NOTE,
13737
+ focus: void 0
13738
+ };
12982
13739
  const receipt = new AbortController();
12983
13740
  const options = {
12984
13741
  signal: AbortSignal.any([signal, receipt.signal]),
@@ -12986,16 +13743,34 @@ var BrowserToolset = class {
12986
13743
  };
12987
13744
  try {
12988
13745
  const outline = await this.#bounded(this.#cursor.elements.outline(options), deadline, action);
12989
- return outline === void 0 ? BROWSER_TOOL_DEADLINE_NOTE : outline.text;
13746
+ return outline === void 0 ? {
13747
+ view: BROWSER_TOOL_DEADLINE_NOTE,
13748
+ focus: void 0
13749
+ } : {
13750
+ view: outline.text,
13751
+ focus: outline.focus
13752
+ };
12990
13753
  } catch (error) {
12991
13754
  if (signal.aborted || isBrowserError(error) && error.code === "BROWSER_TOOLSET_RECEIPT") throw error;
12992
- 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.)`;
13755
+ if (!isBrowserElementError(error) || error.context?.["reason"] !== "GONE") return {
13756
+ view: `(The view could not be read: ${(0, _orkestrel_contract.isError)(error) ? error.message : String(error)}; call look.)`,
13757
+ focus: void 0
13758
+ };
12993
13759
  try {
12994
13760
  const outline = await this.#bounded(this.#cursor.elements.outline(options), deadline, action);
12995
- return outline === void 0 ? BROWSER_TOOL_DEADLINE_NOTE : outline.text;
13761
+ return outline === void 0 ? {
13762
+ view: BROWSER_TOOL_DEADLINE_NOTE,
13763
+ focus: void 0
13764
+ } : {
13765
+ view: outline.text,
13766
+ focus: outline.focus
13767
+ };
12996
13768
  } catch (retry) {
12997
13769
  if (signal.aborted || isBrowserError(retry) && retry.code === "BROWSER_TOOLSET_RECEIPT") throw retry;
12998
- return BROWSER_TOOL_CHANGED_NOTE;
13770
+ return {
13771
+ view: BROWSER_TOOL_CHANGED_NOTE,
13772
+ focus: void 0
13773
+ };
12999
13774
  }
13000
13775
  } finally {
13001
13776
  receipt.abort(new BrowserError("the receipt settled", "BROWSER_TOOLSET_SETTLED"));
@@ -13235,6 +14010,7 @@ exports.BROWSER_ACTION_OUTCOMES = BROWSER_ACTION_OUTCOMES;
13235
14010
  exports.BROWSER_ACTION_STAGES = BROWSER_ACTION_STAGES;
13236
14011
  exports.BROWSER_CODEGEN_BINDING_NAME = BROWSER_CODEGEN_BINDING_NAME;
13237
14012
  exports.BROWSER_CODEGEN_SOURCE = BROWSER_CODEGEN_SOURCE;
14013
+ exports.BROWSER_CONTEXT_LOSS_PATTERN = BROWSER_CONTEXT_LOSS_PATTERN;
13238
14014
  exports.BROWSER_DEFAULT_TIMEOUT_MS = BROWSER_DEFAULT_TIMEOUT_MS;
13239
14015
  exports.BROWSER_DEFAULT_VIEWPORT_HEIGHT = BROWSER_DEFAULT_VIEWPORT_HEIGHT;
13240
14016
  exports.BROWSER_DEFAULT_VIEWPORT_WIDTH = BROWSER_DEFAULT_VIEWPORT_WIDTH;
@@ -13269,6 +14045,7 @@ exports.BROWSER_RESULT_LIMIT_SENTINEL_PREFIX = BROWSER_RESULT_LIMIT_SENTINEL_PRE
13269
14045
  exports.BROWSER_RUN_ID_PATTERN = BROWSER_RUN_ID_PATTERN;
13270
14046
  exports.BROWSER_SCHEMES = BROWSER_SCHEMES;
13271
14047
  exports.BROWSER_SCREENSHOT_ATTRIBUTE = BROWSER_SCREENSHOT_ATTRIBUTE;
14048
+ exports.BROWSER_SEARCH_PATTERN = BROWSER_SEARCH_PATTERN;
13272
14049
  exports.BROWSER_SNAPSHOT_NODE_LIMIT = BROWSER_SNAPSHOT_NODE_LIMIT;
13273
14050
  exports.BROWSER_STABLE_FRAME_COUNT = BROWSER_STABLE_FRAME_COUNT;
13274
14051
  exports.BROWSER_STOP_LOADING_TIMEOUT_MS = BROWSER_STOP_LOADING_TIMEOUT_MS;
@@ -13288,6 +14065,7 @@ exports.BROWSER_TOOL_TIMEOUT_LIMIT_MS = BROWSER_TOOL_TIMEOUT_LIMIT_MS;
13288
14065
  exports.BROWSER_TOOL_TIMEOUT_MS = BROWSER_TOOL_TIMEOUT_MS;
13289
14066
  exports.BROWSER_TOOL_VIEW_FOOTER = BROWSER_TOOL_VIEW_FOOTER;
13290
14067
  exports.BROWSER_TYPED_ROLES = BROWSER_TYPED_ROLES;
14068
+ exports.BROWSER_WAIT_EVENTS = BROWSER_WAIT_EVENTS;
13291
14069
  exports.BrowserAccessibility = BrowserAccessibility;
13292
14070
  exports.BrowserClock = BrowserClock;
13293
14071
  exports.BrowserCodegen = BrowserCodegen;
@@ -13340,6 +14118,7 @@ exports.buildBrowserJourney = buildBrowserJourney;
13340
14118
  exports.bytesToText = bytesToText;
13341
14119
  exports.collectBrowserJourneyBindings = collectBrowserJourneyBindings;
13342
14120
  exports.collectBrowserJourneyTextBindings = collectBrowserJourneyTextBindings;
14121
+ exports.collectBrowserWords = collectBrowserWords;
13343
14122
  exports.compileActionabilityFunction = compileActionabilityFunction;
13344
14123
  exports.compileBrowserBindingCleanup = compileBrowserBindingCleanup;
13345
14124
  exports.compileBrowserBindingResult = compileBrowserBindingResult;
@@ -13464,7 +14243,9 @@ exports.readRareStringData = readRareStringData;
13464
14243
  exports.renderBrowserElement = renderBrowserElement;
13465
14244
  exports.renderBrowserJourney = renderBrowserJourney;
13466
14245
  exports.renderBrowserJourneyFault = renderBrowserJourneyFault;
14246
+ exports.renderBrowserMatches = renderBrowserMatches;
13467
14247
  exports.renderBrowserOutline = renderBrowserOutline;
14248
+ exports.renderBrowserOutlineRow = renderBrowserOutlineRow;
13468
14249
  exports.renderBrowserReceipt = renderBrowserReceipt;
13469
14250
  exports.renderBrowserRun = renderBrowserRun;
13470
14251
  exports.renderBrowserRunResult = renderBrowserRunResult;
@@ -13472,6 +14253,8 @@ exports.renderBrowserToolOutput = renderBrowserToolOutput;
13472
14253
  exports.requireBrowserReference = requireBrowserReference;
13473
14254
  exports.requireBrowserString = requireBrowserString;
13474
14255
  exports.resolveBrowserJourneyBinding = resolveBrowserJourneyBinding;
14256
+ exports.scanBrowserOutline = scanBrowserOutline;
14257
+ exports.scanBrowserText = scanBrowserText;
13475
14258
  exports.settleBrowserTeardown = settleBrowserTeardown;
13476
14259
  exports.textToBytes = textToBytes;
13477
14260
  exports.validateBrowserAccessibilityOptions = validateBrowserAccessibilityOptions;