@orkestrel/browser 0.0.20 → 0.0.22

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