From e6490173223518588fc7790e345b8d4e927d9725 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 21 Jun 2026 06:36:11 +0200 Subject: [PATCH] feat(coding-agent): added ARIA snapshot support for browser tools - Implemented `tab.ariaSnapshot()` to capture and represent page structures as ARIA-tree YAML. - Introduced `tab.ref()` and ref-based selector parsing to enable precise element interaction via unique ARIA identifiers. - Integrated automated script bundling for cross-environment evaluation of ARIA snapshot logic. - Updated browser action methods to resolve and target elements using ARIA-ref handles. --- docs/provider-streaming-internals.md | 2 +- docs/tools/browser.md | 8 ++ packages/coding-agent/CHANGELOG.md | 6 +- .../scripts/generate-aria-snapshot.ts | 134 ++++++++++++++++++ .../coding-agent/src/prompts/tools/browser.md | 2 + packages/coding-agent/src/tools/browser.ts | 5 + .../browser/aria/aria-snapshot.bundle.txt | 7 + .../src/tools/browser/aria/aria-snapshot.ts | 103 ++++++++++++++ .../src/tools/browser/cmux/cmux-tab.ts | 37 ++++- .../src/tools/browser/tab-worker.ts | 106 +++++++++++--- .../test/tools/browser-aria-snapshot.test.ts | 48 +++++++ 11 files changed, 438 insertions(+), 20 deletions(-) create mode 100755 packages/coding-agent/scripts/generate-aria-snapshot.ts create mode 100644 packages/coding-agent/src/tools/browser/aria/aria-snapshot.bundle.txt create mode 100644 packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts create mode 100644 packages/coding-agent/test/tools/browser-aria-snapshot.test.ts diff --git a/docs/provider-streaming-internals.md b/docs/provider-streaming-internals.md index 83e22d8f8..339cd005a 100644 --- a/docs/provider-streaming-internals.md +++ b/docs/provider-streaming-internals.md @@ -104,7 +104,7 @@ Tool-call argument streaming: Shared behavior for Anthropic/OpenAI Responses uses `parseStreamingJson()` / `parseStreamingJsonThrottled()` (`packages/ai/src/utils/json-parse.ts`): 1. try `JSON.parse` -2. fallback to `repairJson()` + the `partial-json` parser for incomplete fragments +2. fallback to the in-house `RelaxedJson` parser (relaxed/repairing) for incomplete fragments 3. if both fail, return `{}` Implications: diff --git a/docs/tools/browser.md b/docs/tools/browser.md index ee4d5b75f..bafc4a224 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -14,6 +14,9 @@ - `packages/coding-agent/src/tools/browser/attach.ts` — CDP attach/reuse, target picking, spawned-app process handling. - `packages/coding-agent/src/tools/browser/tab-protocol.ts` — worker init/run/result message schema. - `packages/coding-agent/src/tools/browser/readable.ts` — `tab.extract()` readability extraction. + - `packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts` — `captureAriaSnapshot()` (puppeteer/CDP path) and `buildAriaSnapshotScript()` (cmux path); imports the committed `aria-snapshot.bundle.txt`. + - `packages/coding-agent/src/tools/browser/aria/aria-snapshot.bundle.txt` — generated, committed artifact: Playwright's injected ARIA-snapshot sources (Apache-2.0, (c) Microsoft; ARIA tree + W3C accessible-name computation) bundled to a CJS module. Upstream sources are not vendored into the repo. + - `packages/coding-agent/scripts/generate-aria-snapshot.ts` — fetches the pinned Playwright sources to a temp dir and bundles them into `aria-snapshot.bundle.txt` (CJS, browser target). Dev-time, network-bound; only the bundle is committed. - `packages/coding-agent/src/tools/browser/cmux/rpc.ts` — cmux browser-kind resolution plus snapshot/eval/wait-state helpers for the cmux backend. - `packages/coding-agent/src/tools/browser/cmux/socket-client.ts` — `CmuxSocketClient`: JSON-RPC over the cmux unix socket. - `packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts` — `CmuxTab` surface helper API and `runCmuxCode()` execution path. @@ -120,6 +123,8 @@ The tool returns one result per call; no streaming partial output is emitted fro - `tab.title(): Promise` - `tab.goto(url, { waitUntil? })` - `tab.observe({ includeAll?, viewportOnly? })` + - `tab.ariaSnapshot(selector?, { depth?, boxes? })` + - `tab.ref(id)` - `tab.screenshot({ selector?, fullPage?, save?, silent? })` - `tab.extract(format = "markdown")` - `tab.click(selector)` @@ -136,9 +141,12 @@ The tool returns one result per call; no streaming partial output is emitted fro - `tab.waitForUrl(pattern, { timeout? })` - `tab.waitForResponse(pattern, { timeout? })` - `tab.id(n)` + - `tab.ref(id)` 14. Selector handling in `normalizeSelector()` accepts plain CSS and Puppeteer query handlers, and rewrites legacy Playwright-style prefixes `p-text/`, `p-xpath/`, `p-pierce/`, `p-aria/`; other `p-*` prefixes throw a `ToolError`. 15. `tab.observe()` clears the element cache, takes a Puppeteer accessibility snapshot, filters to interactive nodes unless `includeAll`, optionally filters to viewport-visible nodes, assigns numeric ids, caches `ElementHandle`s, and returns URL/title/viewport/scroll metadata plus `elements`. +15a. `tab.ariaSnapshot()` resolves the optional `selector` (via `normalizeSelector()` → `page.$`, defaulting to the whole document) and runs the generated Playwright ARIA-snapshot bundle (`src/tools/browser/aria/aria-snapshot.bundle.txt`) via `captureAriaSnapshot()`. The bundle is wrapped in a `new Function` built worker-side (so page CSP never applies) and serialized to a CDP `page.evaluate` in the page's **main world**, returning Playwright-format YAML. It always runs in `ai` mode: every node gets a `[ref=eN]` id, clickables get `[cursor=pointer]`, and matched DOM nodes are tagged with an `_ariaRef` expando. Existing `_ariaRef` expandos are cleared before each snapshot so ids renumber deterministically from e1 (the fresh module's counter resets each call); refs stay valid until the next snapshot. The cmux backend uses `buildAriaSnapshotScript()` over `browser.eval` instead (no `ElementHandle`; CSS selectors only for the root). 16. `tab.id(n)` resolves the cached `ElementHandle`, verifies `el.isConnected`, and throws a stale-id error after cache invalidation if the DOM changed or the cache was cleared. +16a. `tab.ref(id)` resolves a `[ref=eN]` id from the latest `ariaSnapshot()` to a live `ElementHandle` via `resolveAriaRefHandle()` (`page.evaluateHandle` in the main world, walking the document + shadow roots for the matching `_ariaRef`), throwing if no element matches; it accepts a bare `eN` or a prefixed form. For inline selector use, `parseAriaRefSelector()` recognizes only the explicit `aria-ref=eN` / `aria-ref/eN` / `ariaref/eN` forms inside `tab.click/type/fill/waitFor/scrollIntoView` — a bare `eN` is intentionally rejected there so it does not collide with cmux's native observe ids. The cmux backend resolves the same explicit forms through its `aria-ref` `SelectorSpec` kind in `findElement`. 17. `tab.goto()` clears the cached element ids before navigating. Any new `tab.observe()` also clears and rebuilds the cache. 18. `tab.click()` uses a custom retry loop for `text/...` selectors to find an actionable visible match; other selectors use `page.locator(...).click()` with the run timeout. 19. `tab.screenshot()` captures either the whole page or a selector PNG, downsizes a copy for model output, chooses a persistence path, writes the image to disk, records metadata, and optionally emits text + image display entries. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d2cf011f1..3d696a54f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added + +- Added `tab.ariaSnapshot(selector?)` to the browser tool for Playwright-format ARIA-tree YAML +- Added `tab.ref("e5")` and support for `aria-ref=e5` selectors in all `tab` action methods ## [16.1.9] - 2026-06-21 @@ -12232,4 +12236,4 @@ Initial public release. ## [0.7.6] - 2025-11-13 -Previous releases did not maintain a changelog. +Previous releases did not maintain a changelog. \ No newline at end of file diff --git a/packages/coding-agent/scripts/generate-aria-snapshot.ts b/packages/coding-agent/scripts/generate-aria-snapshot.ts new file mode 100755 index 000000000..433f868ea --- /dev/null +++ b/packages/coding-agent/scripts/generate-aria-snapshot.ts @@ -0,0 +1,134 @@ +#!/usr/bin/env bun +/** + * Regenerates the committed browser asset + * + * src/tools/browser/aria/aria-snapshot.bundle.txt ← bundled CJS module + * + * by fetching Playwright's injected ARIA-snapshot sources (pinned to + * PLAYWRIGHT_TAG), wrapping them with a small entry, and bundling — all in a + * throwaway temp dir. Only the bundle is committed; the upstream sources are NOT + * vendored into the repo (no shipping both source + generated copies). This is a + * dev-time, network-bound step, exactly like `generate-models`. + * + * The tab worker imports the `.txt` with `{ type: "text" }`, wraps it in a + * `new Function` worker-side, and runs it via puppeteer's CDP evaluate (it + * installs nothing on `window`). The committed output means binary and source + * installs need no network or build step at runtime. + * + * Usage: bun scripts/generate-aria-snapshot.ts + */ +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; + +const PLAYWRIGHT_TAG = "v1.61.0"; +const RAW_BASE = `https://raw.githubusercontent.com/microsoft/playwright/${PLAYWRIGHT_TAG}/packages`; + +const OUTPUT = path.join(import.meta.dir, "..", "src", "tools", "browser", "aria", "aria-snapshot.bundle.txt"); + +// Upstream source path -> temp path (relative to the temp root). +const VENDOR_FILES: Array<[string, string]> = [ + ["injected/src/ariaSnapshot.ts", "injected/ariaSnapshot.ts"], + ["injected/src/roleUtils.ts", "injected/roleUtils.ts"], + ["injected/src/domUtils.ts", "injected/domUtils.ts"], + ["isomorphic/ariaSnapshot.ts", "isomorphic/ariaSnapshot.ts"], + ["isomorphic/stringUtils.ts", "isomorphic/stringUtils.ts"], + ["isomorphic/cssTokenizer.ts", "isomorphic/cssTokenizer.ts"], + ["isomorphic/yaml.ts", "isomorphic/yaml.ts"], +]; + +// Entry wrapping the upstream modules. Always runs Playwright's `ai` mode so every +// node carries a `[ref=eN]` id; matched nodes get an `_ariaRef` expando. Existing +// expandos are cleared first so the fresh module's counter renumbers from e1 +// deterministically (refs are valid until the next snapshot). Installs nothing on +// `window`. +const ENTRY_SOURCE = ` +import { generateAriaTree, renderAriaTree } from "./injected/ariaSnapshot"; + +export interface AriaSnapshotRequest { + depth?: number; + boxes?: boolean; +} + +function walkElements(fn: (el: Element) => void): void { + const walk = (root: { querySelectorAll(s: string): ArrayLike }): void => { + for (const el of Array.from(root.querySelectorAll("*"))) { + fn(el); + const shadow = (el as Element & { shadowRoot?: { querySelectorAll(s: string): ArrayLike } | null }).shadowRoot; + if (shadow) walk(shadow); + } + }; + walk(document as unknown as { querySelectorAll(s: string): ArrayLike }); +} +type RefElement = Element & { _ariaRef?: { role: string; name: string; ref: string } }; + +export function ariaSnapshot(root: Element | null, request: AriaSnapshotRequest = {}): string { + walkElements(el => { + if ((el as RefElement)._ariaRef) delete (el as RefElement)._ariaRef; + }); + const target = root ?? document.body ?? document.documentElement; + const options = { mode: "ai", depth: request.depth, boxes: request.boxes } as const; + const tree = generateAriaTree(target, options); + return renderAriaTree(tree, options).text; +} + +export function resolveAriaRef(ref: string): Element | null { + let found: Element | null = null; + walkElements(el => { + if (!found && (el as RefElement)._ariaRef?.ref === ref) found = el; + }); + return found; +} +`; + +async function main(): Promise { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "omp-aria-")); + try { + // Fetch pinned upstream sources into the temp dir. + for (const [src, dst] of VENDOR_FILES) { + const url = `${RAW_BASE}/${src}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`); + await Bun.write(path.join(tmp, dst), await res.text()); + } + const entry = path.join(tmp, "entry.ts"); + await Bun.write(entry, ENTRY_SOURCE); + + // The injected sources import isomorphic modules via the `@isomorphic/*` + // alias and the `yaml` package (type-only). Resolve the alias to the fetched + // copies and stub `yaml` (only referenced from erased `import type`). + const aliasPlugin: Bun.BunPlugin = { + name: "aria-vendor-alias", + setup(build) { + build.onResolve({ filter: /^@isomorphic\// }, args => ({ + path: path.join(tmp, "isomorphic", `${args.path.slice("@isomorphic/".length)}.ts`), + })); + build.onResolve({ filter: /^yaml$/ }, () => ({ path: "yaml", namespace: "aria-yaml-stub" })); + build.onLoad({ filter: /.*/, namespace: "aria-yaml-stub" }, () => ({ + contents: "export {};", + loader: "ts", + })); + }, + }; + + const result = await Bun.build({ + entrypoints: [entry], + target: "browser", + format: "cjs", + minify: true, + plugins: [aliasPlugin], + }); + if (!result.success) { + for (const log of result.logs) console.error(log); + throw new Error("aria snapshot bundle failed"); + } + const code = await result.outputs[0].text(); + const header = `// @generated by scripts/generate-aria-snapshot.ts from Playwright ${PLAYWRIGHT_TAG}\n// Bundled from Playwright's injected ARIA-snapshot sources (Apache-2.0, (c) Microsoft).\n// Do not edit by hand. Regenerate with: bun scripts/generate-aria-snapshot.ts\n`; + await Bun.write(OUTPUT, header + code); + console.log(`bundled ${path.relative(process.cwd(), OUTPUT)} (${code.length}b)`); + } finally { + await fs.rm(tmp, { recursive: true, force: true }); + } +} + +await main(); diff --git a/packages/coding-agent/src/prompts/tools/browser.md b/packages/coding-agent/src/prompts/tools/browser.md index 212713e8b..9a38f98e6 100644 --- a/packages/coding-agent/src/prompts/tools/browser.md +++ b/packages/coding-agent/src/prompts/tools/browser.md @@ -15,6 +15,8 @@ Drives real Chromium tab; full puppeteer access via JS. - `tab` helpers; drop to raw puppeteer `page` for anything uncovered: - `tab.goto(url, { waitUntil? })` — navigate. - `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto. + - `tab.ariaSnapshot(selector?, { depth?, boxes? })` — Playwright-format ARIA-tree YAML (nested roles + accessible names + `/url`/`/placeholder`), scoped to `selector` or the whole document. Every node carries a `[ref=eN]` id; `[cursor=pointer]` flags clickables. Captures dense, hierarchical structure/text that `observe()`'s flat list flattens away. Refs renumber from e1 each call and stay valid until the next `ariaSnapshot()`. + - `tab.ref("e5")` — `[ref=eN]` from the last ariaSnapshot → element handle with the common action methods (`.click()`, `.type()`, `.fill()`, `.hover()`, `.evaluate()`, …); the primary way to act on a ref. For convenience `aria-ref=e5` also works inline in `tab.click`/`type`/`fill`/`waitFor`/`scrollIntoView` (e.g. `tab.click("aria-ref=e5")`). - `tab.id(n)` — id from last observe → `ElementHandle` (`.click()`, `.type()`, …). - `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`. - `tab.waitFor(selector)` — wait until attached; returns `ElementHandle`. diff --git a/packages/coding-agent/src/tools/browser.ts b/packages/coding-agent/src/tools/browser.ts index ac8829579..d401f1cce 100644 --- a/packages/coding-agent/src/tools/browser.ts +++ b/packages/coding-agent/src/tools/browser.ts @@ -16,6 +16,11 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors"; import { toolResult } from "./tool-result"; import { clampTimeout } from "./tool-timeouts"; +export { + type AriaSnapshotOptions, + buildAriaSnapshotScript, + parseAriaRefSelector, +} from "./browser/aria/aria-snapshot"; export { cmuxSnapshotToObservation, mapWaitUntil, resolveCmuxKind, serializeEval } from "./browser/cmux/rpc"; export { CmuxSocketClient } from "./browser/cmux/socket-client"; export { extractReadableFromHtml, type ReadableFormat, type ReadableResult } from "./browser/readable"; diff --git a/packages/coding-agent/src/tools/browser/aria/aria-snapshot.bundle.txt b/packages/coding-agent/src/tools/browser/aria/aria-snapshot.bundle.txt new file mode 100644 index 000000000..95616b70a --- /dev/null +++ b/packages/coding-agent/src/tools/browser/aria/aria-snapshot.bundle.txt @@ -0,0 +1,7 @@ +// @generated by scripts/generate-aria-snapshot.ts from Playwright v1.61.0 +// Bundled from Playwright's injected ARIA-snapshot sources (Apache-2.0, (c) Microsoft). +// Do not edit by hand. Regenerate with: bun scripts/generate-aria-snapshot.ts +var{defineProperty:M_,getOwnPropertyNames:WX,getOwnPropertyDescriptor:LX}=Object,zX=Object.prototype.hasOwnProperty;function MX(_){return this[_]}var jX=(_)=>{var J=(k_??=new WeakMap).get(_),Z;if(J)return J;if(J=M_({},"__esModule",{value:!0}),_&&typeof _==="object"||typeof _==="function"){for(var $ of WX(_))if(!zX.call(J,$))M_(J,$,{get:MX.bind(_,$),enumerable:!(Z=LX(_,$))||Z.enumerable})}return k_.set(_,J),J},k_;var FX=(_)=>_;function BX(_,J){this[_]=FX.bind(null,J)}var IX=(_,J)=>{for(var Z in J)M_(_,Z,{get:J[Z],enumerable:!0,configurable:!0,set:BX.bind(J,Z)})};var YZ={};IX(YZ,{resolveAriaRef:()=>$Z,ariaSnapshot:()=>ZZ});module.exports=jX(YZ);function h_(_,J){if(_.role!==J.role||_.name!==J.name)return!1;if(!VX(_,J)||d(_)!==d(J))return!1;let Z=Object.keys(_.props),$=Object.keys(J.props);return Z.length===$.length&&Z.every((X)=>_.props[X]===J.props[X])}function d(_){return _.box.cursor==="pointer"}function VX(_,J){return _.active===J.active&&_.checked===J.checked&&_.disabled===J.disabled&&_.expanded===J.expanded&&_.invalid===J.invalid&&_.selected===J.selected&&_.level===J.level&&_.pressed===J.pressed}var u_;function j_(_){let J=u_?.get(_);if(J===void 0)J=_.replace(/[\u200b\u00ad]/g,"").trim().replace(/\s+/g," "),u_?.set(_,J);return J}function F_(_){return _.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function p_(_,J){let Z=_.length,$=J.length,X=0,Q=0,U=Array(Z+1).fill(null).map(()=>Array($+1).fill(0));for(let W=1;W<=Z;W++)for(let Y=1;Y<=$;Y++)if(_[W-1]===J[Y-1]){if(U[W][Y]=U[W-1][Y-1]+1,U[W][Y]>X)X=U[W][Y],Q=W}return _.slice(Q-X,Q)}var HZ=new RegExp("([\\u001B\\u009B][[\\]()#?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{0,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))","g");function c_(_){if(!m_(_))return _;return"'"+_.replace(/'/g,"''")+"'"}function e(_){if(!m_(_))return _;return'"'+_.replace(/[\\"\x00-\x1f\x7f-\x9f]/g,(J)=>{switch(J){case"\\":return"\\\\";case'"':return"\\\"";case"\b":return"\\b";case"\f":return"\\f";case` +`:return"\\n";case"\r":return"\\r";case"\t":return"\\t";default:return"\\x"+J.charCodeAt(0).toString(16).padStart(2,"0")}})+'"'}function m_(_){if(_.length===0)return!0;if(/^\s|\s$/.test(_))return!0;if(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(_))return!0;if(/^-/.test(_))return!0;if(/[\n:](\s|$)/.test(_))return!0;if(/\s#/.test(_))return!0;if(/[\n\r]/.test(_))return!0;if(/^[&*\],?!>|@"'#%]/.test(_))return!0;if(/[{}`]/.test(_))return!0;if(/^\[/.test(_))return!0;if(!isNaN(Number(_))||["y","n","yes","no","true","false","on","off","null"].includes(_.toLowerCase()))return!0;return!1}var DX={};function h(_){if(_.parentElement)return _.parentElement;if(!_.parentNode)return;if(_.parentNode.nodeType===11&&_.parentNode.host)return _.parentNode.host}function d_(_){let J=_;while(J.parentNode)J=J.parentNode;if(J.nodeType===11||J.nodeType===9)return J}function OX(_){while(_.parentElement)_=_.parentElement;return h(_)}function s(_,J,Z){while(_){let $=_.closest(J);if(Z&&$!==Z&&$?.contains(Z))return;if($)return $;_=OX(_)}}function b(_,J){let Z=J==="::before"?R_:J==="::after"?D_:V_;if(Z&&Z.has(_))return Z.get(_);let $=_.ownerDocument&&_.ownerDocument.defaultView?_.ownerDocument.defaultView.getComputedStyle(_,J):void 0;return Z?.set(_,$),$}function B_(_,J){if(J=J??b(_),!J)return!0;if(Element.prototype.checkVisibility&&DX.browserNameForWorkarounds!=="webkit"){if(!_.checkVisibility())return!1}else{let Z=_.closest("details,summary");if(Z!==_&&Z?.nodeName==="DETAILS"&&!Z.open)return!1}if(J.visibility!=="visible")return!1;return!0}function i(_){let J=b(_);if(!J)return{visible:!0,inline:!1};let Z=J.cursor;if(J.display==="contents"){for(let X=_.firstChild;X;X=X.nextSibling){if(X.nodeType===1&&__(X))return{visible:!0,inline:!1,cursor:Z};if(X.nodeType===3&&I_(X))return{visible:!0,inline:!0,cursor:Z}}return{visible:!1,inline:!1,cursor:Z}}if(!B_(_,J))return{cursor:Z,visible:!1,inline:!1};let $=_.getBoundingClientRect();return{cursor:Z,visible:$.width>0&&$.height>0,inline:J.display==="inline"}}function __(_){return i(_).visible}function I_(_){let J=_.ownerDocument.createRange();J.selectNode(_);let Z=J.getBoundingClientRect();return Z.width>0&&Z.height>0}function V(_){let J=_.tagName;if(typeof J==="string")return J.toUpperCase();if(_ instanceof HTMLFormElement)return"FORM";return _.tagName.toUpperCase()}var V_,R_,D_,s_=0;function i_(){++s_,V_??=new Map,R_??=new Map,D_??=new Map}function l_(){if(!--s_)V_=void 0,R_=void 0,D_=void 0}var R=function(_,J,Z){return _>=J&&_<=Z};function A(_){return R(_,48,57)}function r_(_){return A(_)||R(_,65,70)||R(_,97,102)}function wX(_){return R(_,65,90)}function AX(_){return R(_,97,122)}function EX(_){return wX(_)||AX(_)}function PX(_){return _>=128}function J_(_){return EX(_)||PX(_)||_===95}function a_(_){return J_(_)||A(_)||_===45}function fX(_){return R(_,0,8)||_===11||R(_,14,31)||_===127}function u(_){return _===10}function T(_){return u(_)||_===9||_===32}var CX=1114111;class Z_ extends Error{constructor(_){super(_);this.name="InvalidCharacterError"}}function bX(_){let J=[];for(let Z=0;Z<_.length;Z++){let $=_.charCodeAt(Z);if($===13&&_.charCodeAt(Z+1)===10)$=10,Z++;if($===13||$===12)$=10;if($===0)$=65533;if(R($,55296,56319)&&R(_.charCodeAt(Z+1),56320,57343)){let X=$-55296,Q=_.charCodeAt(Z+1)-56320;$=Math.pow(2,16)+X*Math.pow(2,10)+Q,Z++}J.push($)}return J}function D(_){if(_<=65535)return String.fromCharCode(_);_-=Math.pow(2,16);let J=Math.floor(_/Math.pow(2,10))+55296,Z=_%Math.pow(2,10)+56320;return String.fromCharCode(J)+String.fromCharCode(Z)}function n_(_){let J=bX(_),Z=-1,$=[],X,Q=0,U=0,W=0,Y=function(){Q+=1,W=U,U=0},K={line:Q,column:U},z=function(G){if(G>=J.length)return-1;return J[G]},q=function(G){if(G===void 0)G=1;if(G>3)throw"Spec Error: no more than three codepoints of lookahead.";return z(Z+G)},L=function(G){if(G===void 0)G=1;if(Z+=G,X=z(Z),u(X))Y();else U+=G;return!0},I=function(){if(Z-=1,u(X))Q-=1,U=W;else U-=1;return K.line=Q,K.column=U,!0},H=function(G){if(G===void 0)G=X;return G===-1},F=function(){},j=function(){},w=function(){if(E(),L(),T(X)){while(T(q()))L();return new $_}else if(X===34)return K_();else if(X===35)if(a_(q())||k(q(1),q(2))){let G=new jJ("");if(o(q(1),q(2),q(3)))G.type="id";return G.value=t(),G}else return new O(X);else if(X===36)if(q()===61)return L(),new KJ;else return new O(X);else if(X===39)return K_();else if(X===40)return new HJ;else if(X===41)return new Y_;else if(X===42)if(q()===61)return L(),new WJ;else return new O(X);else if(X===43)if(W_())return I(),S();else return new O(X);else if(X===44)return new XJ;else if(X===45)if(W_())return I(),S();else if(q(1)===45&&q(2)===62)return L(2),new e_;else if(QX())return I(),m();else return new O(X);else if(X===46)if(W_())return I(),S();else return new O(X);else if(X===58)return new _J;else if(X===59)return new JJ;else if(X===60)if(q(1)===33&&q(2)===45&&q(3)===45)return L(3),new t_;else return new O(X);else if(X===64)if(o(q(1),q(2),q(3)))return new MJ(t());else return new O(X);else if(X===91)return new YJ;else if(X===92)if(n())return I(),m();else return j(),new O(X);else if(X===93)return new QJ;else if(X===94)if(q()===61)return L(),new UJ;else return new O(X);else if(X===123)return new ZJ;else if(X===124)if(q()===61)return L(),new GJ;else if(q()===124)return L(),new LJ;else return new O(X);else if(X===125)return new $J;else if(X===126)if(q()===61)return L(),new qJ;else return new O(X);else if(A(X))return I(),S();else if(J_(X))return I(),m();else if(H())return new zJ;else return new O(X)},E=function(){while(q(1)===47&&q(2)===42){L(2);while(!0)if(L(),X===42&&q()===47){L();break}else if(H()){j();return}}},S=function(){let G=qX();if(o(q(1),q(2),q(3))){let M=new VJ;return M.value=G.value,M.repr=G.repr,M.type=G.type,M.unit=t(),M}else if(q()===37){L();let M=new IJ;return M.value=G.value,M.repr=G.repr,M}else{let M=new BJ;return M.value=G.value,M.repr=G.repr,M.type=G.type,M}},m=function(){let G=t();if(G.toLowerCase()==="url"&&q()===40){L();while(T(q(1))&&T(q(2)))L();if(q()===34||q()===39)return new p(G);else if(T(q())&&(q(2)===34||q(2)===39))return new p(G);else return a()}else if(q()===40)return L(),new p(G);else return new Q_(G)},K_=function(G){if(G===void 0)G=X;let M="";while(L())if(X===G||H())return new H_(M);else if(u(X))return j(),I(),new o_;else if(X===92)if(H(q()))F();else if(u(q()))L();else M+=D(C());else M+=D(X);throw Error("Internal error")},a=function(){let G=new FJ("");while(T(q()))L();if(H(q()))return G;while(L())if(X===41||H())return G;else if(T(X)){while(T(q()))L();if(q()===41||H(q()))return L(),G;else return L_(),new X_}else if(X===34||X===39||X===40||fX(X))return j(),L_(),new X_;else if(X===92)if(n())G.value+=D(C());else return j(),L_(),new X_;else G.value+=D(X);throw Error("Internal error")},C=function(){if(L(),r_(X)){let G=[X];for(let P=0;P<5;P++)if(r_(q()))L(),G.push(X);else break;if(T(q()))L();let M=parseInt(G.map(function(P){return String.fromCharCode(P)}).join(""),16);if(M>CX)M=65533;return M}else if(H())return 65533;else return X},k=function(G,M){if(G!==92)return!1;if(u(M))return!1;return!0},n=function(){return k(X,q())},o=function(G,M,P){if(G===45)return J_(M)||M===45||k(M,P);else if(J_(G))return!0;else if(G===92)return k(G,M);else return!1},QX=function(){return o(X,q(1),q(2))},HX=function(G,M,P){if(G===43||G===45){if(A(M))return!0;if(M===46&&A(P))return!0;return!1}else if(G===46){if(A(M))return!0;return!1}else if(A(G))return!0;else return!1},W_=function(){return HX(X,q(1),q(2))},t=function(){let G="";while(L())if(a_(X))G+=D(X);else if(n())G+=D(C());else return I(),G;throw Error("Internal parse error")},qX=function(){let G="",M="integer";if(q()===43||q()===45)L(),G+=D(X);while(A(q()))L(),G+=D(X);if(q(1)===46&&A(q(2))){L(),G+=D(X),L(),G+=D(X),M="number";while(A(q()))L(),G+=D(X)}let P=q(1),z_=q(2),UX=q(3);if((P===69||P===101)&&A(z_)){L(),G+=D(X),L(),G+=D(X),M="number";while(A(q()))L(),G+=D(X)}else if((P===69||P===101)&&(z_===43||z_===45)&&A(UX)){L(),G+=D(X),L(),G+=D(X),L(),G+=D(X),M="number";while(A(q()))L(),G+=D(X)}let KX=GX(G);return{type:M,value:KX,repr:G}},GX=function(G){return+G},L_=function(){while(L())if(X===41||H())return;else if(n())C(),F();else F()},N_=0;while(!H(q()))if($.push(w()),N_++,N_>J.length*2)throw Error("I'm infinite-looping!");return $}class B{tokenType="";value;toJSON(){return{token:this.tokenType}}toString(){return this.tokenType}toSource(){return""+this}}class o_ extends B{tokenType="BADSTRING"}class X_ extends B{tokenType="BADURL"}class $_ extends B{tokenType="WHITESPACE";toString(){return"WS"}toSource(){return" "}}class t_ extends B{tokenType="CDO";toSource(){return""}}class _J extends B{tokenType=":"}class JJ extends B{tokenType=";"}class XJ extends B{tokenType=","}class g extends B{value="";mirror=""}class ZJ extends g{tokenType="{";constructor(){super();this.value="{",this.mirror="}"}}class $J extends g{tokenType="}";constructor(){super();this.value="}",this.mirror="{"}}class YJ extends g{tokenType="[";constructor(){super();this.value="[",this.mirror="]"}}class QJ extends g{tokenType="]";constructor(){super();this.value="]",this.mirror="["}}class HJ extends g{tokenType="(";constructor(){super();this.value="(",this.mirror=")"}}class Y_ extends g{tokenType=")";constructor(){super();this.value=")",this.mirror="("}}class qJ extends B{tokenType="~="}class GJ extends B{tokenType="|="}class UJ extends B{tokenType="^="}class KJ extends B{tokenType="$="}class WJ extends B{tokenType="*="}class LJ extends B{tokenType="||"}class zJ extends B{tokenType="EOF";toSource(){return""}}class O extends B{tokenType="DELIM";value="";constructor(_){super();this.value=D(_)}toString(){return"DELIM("+this.value+")"}toJSON(){let _=this.constructor.prototype.constructor.prototype.toJSON.call(this);return _.value=this.value,_}toSource(){if(this.value==="\\")return"\\\n";else return this.value}}class N extends B{value="";ASCIIMatch(_){return this.value.toLowerCase()===_.toLowerCase()}toJSON(){let _=this.constructor.prototype.constructor.prototype.toJSON.call(this);return _.value=this.value,_}}class Q_ extends N{constructor(_){super();this.value=_}tokenType="IDENT";toString(){return"IDENT("+this.value+")"}toSource(){return l(this.value)}}class p extends N{tokenType="FUNCTION";mirror;constructor(_){super();this.value=_,this.mirror=")"}toString(){return"FUNCTION("+this.value+")"}toSource(){return l(this.value)+"("}}class MJ extends N{tokenType="AT-KEYWORD";constructor(_){super();this.value=_}toString(){return"AT("+this.value+")"}toSource(){return"@"+l(this.value)}}class jJ extends N{tokenType="HASH";type;constructor(_){super();this.value=_,this.type="unrestricted"}toString(){return"HASH("+this.value+")"}toJSON(){let _=this.constructor.prototype.constructor.prototype.toJSON.call(this);return _.value=this.value,_.type=this.type,_}toSource(){if(this.type==="id")return"#"+l(this.value);else return"#"+yX(this.value)}}class H_ extends N{tokenType="STRING";constructor(_){super();this.value=_}toString(){return'"'+RJ(this.value)+'"'}}class FJ extends N{tokenType="URL";constructor(_){super();this.value=_}toString(){return"URL("+this.value+")"}toSource(){return'url("'+RJ(this.value)+'")'}}class BJ extends B{tokenType="NUMBER";type;repr;constructor(){super();this.type="integer",this.repr=""}toString(){if(this.type==="integer")return"INT("+this.value+")";return"NUMBER("+this.value+")"}toJSON(){let _=super.toJSON();return _.value=this.value,_.type=this.type,_.repr=this.repr,_}toSource(){return this.repr}}class IJ extends B{tokenType="PERCENTAGE";repr;constructor(){super();this.repr=""}toString(){return"PERCENTAGE("+this.value+")"}toJSON(){let _=this.constructor.prototype.constructor.prototype.toJSON.call(this);return _.value=this.value,_.repr=this.repr,_}toSource(){return this.repr+"%"}}class VJ extends B{tokenType="DIMENSION";type;repr;unit;constructor(){super();this.type="integer",this.repr="",this.unit=""}toString(){return"DIM("+this.value+","+this.unit+")"}toJSON(){let _=this.constructor.prototype.constructor.prototype.toJSON.call(this);return _.value=this.value,_.type=this.type,_.repr=this.repr,_.unit=this.unit,_}toSource(){let _=this.repr,J=l(this.unit);if(J[0].toLowerCase()==="e"&&(J[1]==="-"||R(J.charCodeAt(1),48,57)))J="\\65 "+J.slice(1,J.length);return _+J}}function l(_){_=""+_;let J="",Z=_.charCodeAt(0);for(let $=0;$<_.length;$++){let X=_.charCodeAt($);if(X===0)throw new Z_("Invalid character: the input contains U+0000.");if(R(X,1,31)||X===127||$===0&&R(X,48,57)||$===1&&R(X,48,57)&&Z===45)J+="\\"+X.toString(16)+" ";else if(X>=128||X===45||X===95||R(X,48,57)||R(X,65,90)||R(X,97,122))J+=_[$];else J+="\\"+_[$]}return J}function yX(_){_=""+_;let J="";for(let Z=0;Z<_.length;Z++){let $=_.charCodeAt(Z);if($===0)throw new Z_("Invalid character: the input contains U+0000.");if($>=128||$===45||$===95||R($,48,57)||R($,65,90)||R($,97,122))J+=_[Z];else J+="\\"+$.toString(16)+" "}return J}function RJ(_){_=""+_;let J="";for(let Z=0;Z<_.length;Z++){let $=_.charCodeAt(Z);if($===0)throw new Z_("Invalid character: the input contains U+0000.");if(R($,1,31)||$===127)J+="\\"+$.toString(16)+" ";else if($===34||$===92)J+="\\"+_[Z];else J+=_[Z]}return J}function DJ(_){return _.hasAttribute("aria-label")||_.hasAttribute("aria-labelledby")}var OJ="article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]",TX=[["aria-atomic",void 0],["aria-busy",void 0],["aria-controls",void 0],["aria-current",void 0],["aria-describedby",void 0],["aria-details",void 0],["aria-dropeffect",void 0],["aria-flowto",void 0],["aria-grabbed",void 0],["aria-hidden",void 0],["aria-keyshortcuts",void 0],["aria-label",["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]],["aria-labelledby",["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]],["aria-live",void 0],["aria-owns",void 0],["aria-relevant",void 0],["aria-roledescription",["generic"]]];function fJ(_,J){return TX.some(([Z,$])=>{return!$?.includes(J||"")&&_.hasAttribute(Z)})}function CJ(_){return!Number.isNaN(Number(String(_.getAttribute("tabindex"))))}function xX(_){return!mJ(_)&&(vX(_)||CJ(_))}function vX(_){let J=V(_);if(["BUTTON","DETAILS","SELECT","TEXTAREA"].includes(J))return!0;if(J==="A"||J==="AREA")return _.hasAttribute("href");if(J==="INPUT")return!_.hidden;return!1}var gX={A:(_)=>{return _.hasAttribute("href")?"link":null},AREA:(_)=>{return _.hasAttribute("href")?"link":null},ARTICLE:()=>"article",ASIDE:()=>"complementary",BLOCKQUOTE:()=>"blockquote",BUTTON:()=>"button",CAPTION:()=>"caption",CODE:()=>"code",DATALIST:()=>"listbox",DD:()=>"definition",DEL:()=>"deletion",DETAILS:()=>"group",DFN:()=>"term",DIALOG:()=>"dialog",DT:()=>"term",EM:()=>"emphasis",FIELDSET:()=>"group",FIGURE:()=>"figure",FOOTER:(_)=>s(_,OJ)?null:"contentinfo",FORM:(_)=>DJ(_)?"form":null,H1:()=>"heading",H2:()=>"heading",H3:()=>"heading",H4:()=>"heading",H5:()=>"heading",H6:()=>"heading",HEADER:(_)=>s(_,OJ)?null:"banner",HR:()=>"separator",HTML:()=>"document",IMG:(_)=>_.getAttribute("alt")===""&&!_.getAttribute("title")&&!fJ(_)&&!CJ(_)?"presentation":"img",INPUT:(_)=>{let J=_.type.toLowerCase();if(J==="search")return _.hasAttribute("list")?"combobox":"searchbox";if(["email","tel","text","url",""].includes(J)){let Z=G_(_,_.getAttribute("list"))[0];return Z&&V(Z)==="DATALIST"?"combobox":"textbox"}if(J==="hidden")return null;if(J==="file")return"button";return lX[J]||"textbox"},INS:()=>"insertion",LI:()=>"listitem",MAIN:()=>"main",MARK:()=>"mark",MATH:()=>"math",MENU:()=>"list",METER:()=>"meter",NAV:()=>"navigation",OL:()=>"list",OPTGROUP:()=>"group",OPTION:()=>"option",OUTPUT:()=>"status",P:()=>"paragraph",PROGRESS:()=>"progressbar",SEARCH:()=>"search",SECTION:(_)=>DJ(_)?"region":null,SELECT:(_)=>_.hasAttribute("multiple")||_.size>1?"listbox":"combobox",STRONG:()=>"strong",SUB:()=>"subscript",SUP:()=>"superscript",SVG:()=>"img",TABLE:()=>"table",TBODY:()=>"rowgroup",TD:(_)=>{let J=s(_,"table"),Z=J?O_(J):"";return Z==="grid"||Z==="treegrid"?"gridcell":"cell"},TEXTAREA:()=>"textbox",TFOOT:()=>"rowgroup",TH:(_)=>{let J=_.getAttribute("scope");if(J==="col"||J==="colgroup")return"columnheader";if(J==="row"||J==="rowgroup")return"rowheader";let{nextElementSibling:Z,previousElementSibling:$}=_,X=!!_.parentElement&&V(_.parentElement)==="TR"?_.parentElement:void 0;if(!Z&&!$){if(X){let Q=s(X,"table");if(Q&&Q.rows.length<=1)return null}return"columnheader"}if(wJ(Z)&&wJ($))return"columnheader";if(AJ(Z)||AJ($))return"rowheader";return"columnheader"},THEAD:()=>"rowgroup",TIME:()=>"time",TR:()=>"row",UL:()=>"list"};function wJ(_){return!!_&&V(_)==="TH"}function AJ(_){if(!_||V(_)!=="TD")return!1;return!!(_.textContent?.trim()||_.children.length>0)}var NX={DD:["DL","DIV"],DIV:["DL"],DT:["DL","DIV"],LI:["OL","UL"],TBODY:["TABLE"],TD:["TR"],TFOOT:["TABLE"],TH:["TR"],THEAD:["TABLE"],TR:["THEAD","TBODY","TFOOT","TABLE"]};function EJ(_){let J=gX[V(_)]?.(_)||"";if(!J)return null;let Z=_;while(Z){let $=h(Z),X=NX[V(Z)];if(!X||!$||!X.includes(V($)))break;let Q=O_($);if((Q==="none"||Q==="presentation")&&!bJ($,Q))return Q;Z=$}return J}var kX=["alert","alertdialog","application","article","banner","blockquote","button","caption","cell","checkbox","code","columnheader","combobox","complementary","contentinfo","definition","deletion","dialog","directory","document","emphasis","feed","figure","form","generic","grid","gridcell","group","heading","img","insertion","link","list","listbox","listitem","log","main","mark","marquee","math","meter","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","navigation","none","note","option","paragraph","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","strong","subscript","superscript","switch","tab","table","tablist","tabpanel","term","textbox","time","timer","toolbar","tooltip","tree","treegrid","treeitem"];function O_(_){return(_.getAttribute("role")||"").split(" ").map((Z)=>Z.trim()).find((Z)=>kX.includes(Z))||null}function bJ(_,J){return fJ(_,J)||xX(_)}function f(_){let J=O_(_);if(!J)return EJ(_);if(J==="none"||J==="presentation"){let Z=EJ(_);if(bJ(_,Z))return Z}return J}function yJ(_){return _===null?void 0:_.toLowerCase()==="true"}function SJ(_){return["STYLE","SCRIPT","NOSCRIPT","TEMPLATE"].includes(V(_))}function y(_){if(SJ(_))return!0;let J=b(_),Z=_.nodeName==="SLOT";if(J?.display==="contents"&&!Z){for(let X=_.firstChild;X;X=X.nextSibling){if(X.nodeType===1&&!y(X))return!1;if(X.nodeType===3&&I_(X))return!1}return!0}if(!(_.nodeName==="OPTION"&&!!_.closest("select"))&&!Z&&!B_(_,J))return!0;return TJ(_)}function TJ(_){let J=q_?.get(_);if(J===void 0){if(J=!1,_.parentElement&&_.parentElement.shadowRoot&&!_.assignedSlot)J=!0;if(!J){let Z=b(_);J=!Z||Z.display==="none"||yJ(_.getAttribute("aria-hidden"))===!0}if(!J){let Z=h(_);if(Z)J=TJ(Z)}q_?.set(_,J)}return J}function G_(_,J){if(!J)return[];let Z=d_(_);if(!Z)return[];try{let $=J.split(" ").filter((Q)=>!!Q),X=[];for(let Q of $){let U=Z.querySelector("#"+CSS.escape(Q));if(U&&!X.includes(U))X.push(U)}return X}catch($){return[]}}function x(_){return _.trim()}function hX(_){return _.split(" ").map((J)=>J.replace(/\r\n/g,` +`).replace(/[\u200b\u00ad]/g,"").replace(/\s\s*/g," ")).join(" ").trim()}function PJ(_,J){let Z=[..._.querySelectorAll(J)];for(let $ of G_(_,_.getAttribute("aria-owns"))){if($.matches(J))Z.push($);Z.push(...$.querySelectorAll(J))}return Z}function c(_,J){let Z=J==="::before"?T_:J==="::after"?x_:S_;if(Z?.has(_))return Z?.get(_);let $=b(_,J),X;if($){let Q=$.content;if(Q&&Q!=="none"&&Q!=="normal"){if($.display!=="none"&&$.visibility!=="hidden")X=uX(_,Q,!!J)}}if(J&&X!==void 0){if(($?.display||"inline")!=="inline")X=" "+X+" "}if(Z)Z.set(_,X);return X}function uX(_,J,Z){if(!J||J==="none"||J==="normal")return;try{let $=n_(J).filter((W)=>!(W instanceof $_)),X=$.findIndex((W)=>W instanceof O&&W.value==="/");if(X!==-1)$=$.slice(X+1);else if(!Z)return;let Q=[],U=0;while(U<$.length)if($[U]instanceof H_)Q.push($[U].value),U++;else if(U+2<$.length&&$[U]instanceof p&&$[U].value==="attr"&&$[U+1]instanceof Q_&&$[U+2]instanceof Y_){let W=$[U+1].value;Q.push(_.getAttribute(W)||""),U+=3}else return;return Q.join("")}catch{}}function pX(_){let J=_.getAttribute("aria-labelledby");if(J===null)return null;let Z=G_(_,J);return Z.length?Z:null}function cX(_,J){let Z=["button","cell","checkbox","columnheader","gridcell","heading","link","menuitem","menuitemcheckbox","menuitemradio","option","radio","row","rowheader","switch","tab","tooltip","treeitem"].includes(_),$=J&&["","caption","code","contentinfo","definition","deletion","emphasis","insertion","list","listitem","mark","none","paragraph","presentation","region","row","rowgroup","section","strong","subscript","superscript","table","term","time"].includes(_);return Z||$}function xJ(_,J){let Z=J?y_:b_,$=Z?.get(_);if($===void 0){if($="",!["caption","code","definition","deletion","emphasis","generic","insertion","mark","paragraph","presentation","strong","subscript","suggestion","superscript","term","time"].includes(f(_)||""))$=hX(v(_,{includeHidden:J,visitedElements:new Set,embeddedInTargetElement:"self"}));Z?.set(_,$)}return $}var vJ=["application","checkbox","columnheader","combobox","gridcell","listbox","radiogroup","rowheader","searchbox","slider","spinbutton","switch","textbox","tree"];function gJ(_){let J=_.getAttribute("aria-invalid");if(!J||J.trim()===""||J.toLocaleLowerCase()==="false")return"false";if(J==="true"||J==="grammar"||J==="spelling")return J;return"true"}function v(_,J){if(J.visitedElements.has(_))return"";let Z={...J,embeddedInTargetElement:J.embeddedInTargetElement==="self"?"descendant":J.embeddedInTargetElement};if(!J.includeHidden){let Y=!!J.embeddedInLabelledBy?.hidden||!!J.embeddedInDescribedBy?.hidden||!!J.embeddedInNativeTextAlternative?.hidden||!!J.embeddedInLabel?.hidden;if(SJ(_)||!Y&&y(_))return J.visitedElements.add(_),""}let $=pX(_);if(!J.embeddedInLabelledBy){let Y=($||[]).map((K)=>v(K,{...J,embeddedInLabelledBy:{element:K,hidden:y(K)},embeddedInDescribedBy:void 0,embeddedInTargetElement:void 0,embeddedInLabel:void 0,embeddedInNativeTextAlternative:void 0})).join(" ");if(Y)return Y}let X=f(_)||"",Q=V(_);if(!!J.embeddedInLabel||!!J.embeddedInLabelledBy||J.embeddedInTargetElement==="descendant"){let Y=[..._.labels||[]].includes(_),K=($||[]).includes(_);if(!Y&&!K){if(X==="textbox"){if(J.visitedElements.add(_),Q==="INPUT"||Q==="TEXTAREA")return _.value;return _.textContent||""}if(["combobox","listbox"].includes(X)){J.visitedElements.add(_);let z;if(Q==="SELECT"){if(z=[..._.selectedOptions],!z.length&&_.options.length)z.push(_.options[0])}else{let q=X==="combobox"?PJ(_,"*").find((L)=>f(L)==="listbox"):_;z=q?PJ(q,'[aria-selected="true"]').filter((L)=>f(L)==="option"):[]}if(!z.length&&Q==="INPUT")return _.value;return z.map((q)=>v(q,Z)).join(" ")}if(["progressbar","scrollbar","slider","spinbutton","meter"].includes(X)){if(J.visitedElements.add(_),_.hasAttribute("aria-valuetext"))return _.getAttribute("aria-valuetext")||"";if(_.hasAttribute("aria-valuenow"))return _.getAttribute("aria-valuenow")||"";return _.getAttribute("value")||""}if(["menu"].includes(X))return J.visitedElements.add(_),""}}let U=_.getAttribute("aria-label")||"";if(x(U))return J.visitedElements.add(_),U;if(!["presentation","none"].includes(X)){if(Q==="INPUT"&&["button","submit","reset"].includes(_.type)){J.visitedElements.add(_);let Y=_.value||"";if(x(Y))return Y;if(_.type==="submit")return"Submit";if(_.type==="reset")return"Reset";return _.getAttribute("title")||""}if(Q==="INPUT"&&_.type==="file"){J.visitedElements.add(_);let Y=_.labels||[];if(Y.length&&!J.embeddedInLabelledBy)return r(Y,J);return"Choose File"}if(Q==="INPUT"&&_.type==="image"){J.visitedElements.add(_);let Y=_.labels||[];if(Y.length&&!J.embeddedInLabelledBy)return r(Y,J);let K=_.getAttribute("alt")||"";if(x(K))return K;let z=_.getAttribute("title")||"";if(x(z))return z;return"Submit"}if(!$&&Q==="BUTTON"){J.visitedElements.add(_);let Y=_.labels||[];if(Y.length)return r(Y,J)}if(!$&&Q==="OUTPUT"){J.visitedElements.add(_);let Y=_.labels||[];if(Y.length)return r(Y,J);return _.getAttribute("title")||""}if(!$&&(Q==="TEXTAREA"||Q==="SELECT"||Q==="INPUT")){J.visitedElements.add(_);let Y=_.labels||[];if(Y.length)return r(Y,J);let K=Q==="INPUT"&&["text","password","search","tel","email","url"].includes(_.type)||Q==="TEXTAREA",z=_.getAttribute("placeholder")||"",q=_.getAttribute("title")||"";if(!K||q)return q;return z}if(!$&&Q==="FIELDSET"){J.visitedElements.add(_);for(let K=_.firstElementChild;K;K=K.nextElementSibling)if(V(K)==="LEGEND")return v(K,{...Z,embeddedInNativeTextAlternative:{element:K,hidden:y(K)}});return _.getAttribute("title")||""}if(!$&&Q==="FIGURE"){J.visitedElements.add(_);for(let K=_.firstElementChild;K;K=K.nextElementSibling)if(V(K)==="FIGCAPTION")return v(K,{...Z,embeddedInNativeTextAlternative:{element:K,hidden:y(K)}});return _.getAttribute("title")||""}if(Q==="IMG"){J.visitedElements.add(_);let Y=_.getAttribute("alt")||"";if(x(Y))return Y;return _.getAttribute("title")||""}if(Q==="TABLE"){J.visitedElements.add(_);for(let K=_.firstElementChild;K;K=K.nextElementSibling)if(V(K)==="CAPTION")return v(K,{...Z,embeddedInNativeTextAlternative:{element:K,hidden:y(K)}});let Y=_.getAttribute("summary")||"";if(Y)return Y}if(Q==="AREA"){J.visitedElements.add(_);let Y=_.getAttribute("alt")||"";if(x(Y))return Y;return _.getAttribute("title")||""}if(Q==="SVG"||_.ownerSVGElement){J.visitedElements.add(_);for(let Y=_.firstElementChild;Y;Y=Y.nextElementSibling)if(V(Y)==="TITLE"&&Y.ownerSVGElement)return v(Y,{...Z,embeddedInLabelledBy:{element:Y,hidden:y(Y)}})}if(_.ownerSVGElement&&Q==="A"){let Y=_.getAttribute("xlink:title")||"";if(x(Y))return J.visitedElements.add(_),Y}}let W=Q==="SUMMARY"&&!["presentation","none"].includes(X);if(cX(X,J.embeddedInTargetElement==="descendant")||W||!!J.embeddedInLabelledBy||!!J.embeddedInDescribedBy||!!J.embeddedInLabel||!!J.embeddedInNativeTextAlternative){J.visitedElements.add(_);let Y=mX(_,Z);if(J.embeddedInTargetElement==="self"?x(Y):Y)return Y}if(!["presentation","none"].includes(X)||Q==="IFRAME"){J.visitedElements.add(_);let Y=_.getAttribute("title")||"";if(x(Y))return Y}return J.visitedElements.add(_),""}function mX(_,J){let Z=[],$=(Q,U)=>{if(U&&Q.assignedSlot)return;if(Q.nodeType===1){let W=b(Q)?.display||"inline",Y=v(Q,J);if(W!=="inline"||Q.nodeName==="BR")Y=" "+Y+" ";Z.push(Y)}else if(Q.nodeType===3)Z.push(Q.textContent||"")};Z.push(c(_,"::before")||"");let X=c(_);if(X!==void 0)Z.push(X);else{let Q=_.nodeName==="SLOT"?_.assignedNodes():[];if(Q.length)for(let U of Q)$(U,!1);else{for(let U=_.firstChild;U;U=U.nextSibling)$(U,!0);if(_.shadowRoot)for(let U=_.shadowRoot.firstChild;U;U=U.nextSibling)$(U,!0);for(let U of G_(_,_.getAttribute("aria-owns")))$(U,!0)}}return Z.push(c(_,"::after")||""),Z.join("")}var w_=["gridcell","option","row","tab","rowheader","columnheader","treeitem"];function NJ(_){if(V(_)==="OPTION")return _.selected;if(w_.includes(f(_)||""))return yJ(_.getAttribute("aria-selected"))===!0;return!1}var A_=["checkbox","menuitemcheckbox","option","radio","switch","menuitemradio","treeitem"];function kJ(_){let J=dX(_,!0);return J==="error"?!1:J}function dX(_,J){let Z=V(_);if(J&&Z==="INPUT"&&_.indeterminate)return"mixed";if(Z==="INPUT"&&["checkbox","radio"].includes(_.type))return _.checked;if(A_.includes(f(_)||"")){let $=_.getAttribute("aria-checked");if($==="true")return!0;if(J&&$==="mixed")return"mixed";return!1}return"error"}var E_=["button"];function hJ(_){if(E_.includes(f(_)||"")){let J=_.getAttribute("aria-pressed");if(J==="true")return!0;if(J==="mixed")return"mixed"}return!1}var P_=["application","button","checkbox","combobox","gridcell","link","listbox","menuitem","row","rowheader","tab","treeitem","columnheader","menuitemcheckbox","menuitemradio","rowheader","switch"];function uJ(_){if(V(_)==="DETAILS")return _.open;if(P_.includes(f(_)||"")){let J=_.getAttribute("aria-expanded");if(J===null)return;if(J==="true")return!0;return!1}return}var f_=["heading","listitem","row","treeitem"];function pJ(_){let J={H1:1,H2:2,H3:3,H4:4,H5:5,H6:6}[V(_)];if(J)return J;if(f_.includes(f(_)||"")){let Z=_.getAttribute("aria-level"),$=Z===null?Number.NaN:Number(Z);if(Number.isInteger($)&&$>=1)return $}return 0}var C_=["application","button","composite","gridcell","group","input","link","menuitem","scrollbar","separator","tab","checkbox","columnheader","combobox","grid","listbox","menu","menubar","menuitemcheckbox","menuitemradio","option","radio","radiogroup","row","rowheader","searchbox","select","slider","spinbutton","switch","tablist","textbox","toolbar","tree","treegrid","treeitem"];function cJ(_){return mJ(_)||dJ(_)}function mJ(_){return["BUTTON","INPUT","SELECT","TEXTAREA","OPTION","OPTGROUP"].includes(V(_))&&(_.hasAttribute("disabled")||sX(_)||iX(_))}function sX(_){return V(_)==="OPTION"&&!!_.closest("OPTGROUP[DISABLED]")}function iX(_){let J=_?.closest("FIELDSET[DISABLED]");if(!J)return!1;let Z=J.querySelector(":scope > LEGEND");return!Z||!Z.contains(_)}function dJ(_,J=!1){if(!_)return!1;if(J||C_.includes(f(_)||"")){let Z=(_.getAttribute("aria-disabled")||"").toLowerCase();if(Z==="true")return!0;if(Z==="false")return!1;return dJ(h(_),!0)}return!1}function r(_,J){return[..._].map((Z)=>v(Z,{...J,embeddedInLabel:{element:Z,hidden:y(Z)},embeddedInNativeTextAlternative:void 0,embeddedInLabelledBy:void 0,embeddedInDescribedBy:void 0,embeddedInTargetElement:void 0})).filter((Z)=>!!Z).join(" ")}function sJ(_){let J=v_,Z=_,$,X=[];for(;Z;Z=h(Z)){let Q=J.get(Z);if(Q!==void 0){$=Q;break}X.push(Z);let U=b(Z);if(!U){$=!0;break}let W=U.pointerEvents;if(W){$=W!=="none";break}}if($===void 0)$=!0;for(let Q of X)J.set(Q,$);return $}var b_,y_,iJ,lJ,rJ,q_,S_,T_,x_,v_,aJ=0;function nJ(){i_(),++aJ,b_??=new Map,y_??=new Map,iJ??=new Map,lJ??=new Map,rJ??=new Map,q_??=new Map,S_??=new Map,T_??=new Map,x_??=new Map,v_??=new Map}function oJ(){if(!--aJ)b_=void 0,y_=void 0,iJ=void 0,lJ=void 0,rJ=void 0,q_=void 0,S_=void 0,T_=void 0,x_=void 0,v_=void 0;l_()}var lX={button:"button",checkbox:"checkbox",image:"button",number:"spinbutton",radio:"radio",range:"slider",reset:"button",submit:"button"};var aX=0;function eJ(_){let J=_.boxes;if(_.mode==="ai")return{visibility:"ariaOrVisible",refs:"interactable",refPrefix:_.refPrefix,includeGenericRole:!0,renderActive:!_.doNotRenderActive,renderCursorPointer:!0,renderBoxes:J};if(_.mode==="autoexpect")return{visibility:"ariaAndVisible",refs:"none",renderBoxes:J};if(_.mode==="codegen")return{visibility:"aria",refs:"none",renderStringsAsRegex:!0,renderBoxes:J};return{visibility:"aria",refs:"none",renderBoxes:J}}function _X(_,J){let Z=eJ(J),$=new Set,X={root:{role:"fragment",name:"",children:[],props:{},box:i(_),receivesPointerEvents:!0},elements:new Map,refs:new Map,iframeRefs:[]};g_(X.root,_);let Q=(W,Y,K)=>{if($.has(Y))return;if($.add(Y),Y.nodeType===Node.TEXT_NODE&&Y.nodeValue){if(!K)return;let F=Y.nodeValue;if(W.role!=="textbox"&&F)W.children.push(Y.nodeValue||"");return}if(Y.nodeType!==Node.ELEMENT_NODE)return;let z=Y,q=!y(z),L=q;if(Z.visibility==="ariaOrVisible")L=q||__(z);if(Z.visibility==="ariaAndVisible")L=q&&__(z);if(Z.visibility==="aria"&&!L)return;let I=[];if(z.hasAttribute("aria-owns")){let F=z.getAttribute("aria-owns").split(/\s+/);for(let j of F){let w=_.ownerDocument.getElementById(j);if(w)I.push(w)}}let H=L?nX(z,Z):null;if(H){if(H.ref){if(X.elements.set(H.ref,z),X.refs.set(z,H.ref),H.role==="iframe")X.iframeRefs.push(H.ref)}W.children.push(H)}U(H||W,z,I,L)};function U(W,Y,K,z){let L=(b(Y)?.display||"inline")!=="inline"||Y.nodeName==="BR"?" ":"";if(L)W.children.push(L);W.children.push(c(Y,"::before")||"");let I=Y.nodeName==="SLOT"?Y.assignedNodes():[];if(I.length)for(let H of I)Q(W,H,z);else{for(let H=Y.firstChild;H;H=H.nextSibling)if(!H.assignedSlot)Q(W,H,z);if(Y.shadowRoot)for(let H=Y.shadowRoot.firstChild;H;H=H.nextSibling)Q(W,H,z)}for(let H of K)Q(W,H,z);if(W.children.push(c(Y,"::after")||""),L)W.children.push(L);if(W.children.length===1&&W.name===W.children[0])W.children=[];if(W.role==="link"&&Y.hasAttribute("href")){let H=Y.getAttribute("href");W.props.url=H}if(W.role==="textbox"&&Y.hasAttribute("placeholder")&&Y.getAttribute("placeholder")!==W.name){let H=Y.getAttribute("placeholder");W.props.placeholder=H}}nJ();try{Q(X.root,_,!0)}finally{oJ()}return tX(X.root),oX(X.root),X}function tJ(_,J){if(J.refs==="none")return;if(J.refs==="interactable"&&(!_.box.visible||!_.receivesPointerEvents))return;let Z=$X(_),$=Z._ariaRef;if(!$||$.role!==_.role||$.name!==_.name)$={role:_.role,name:_.name,ref:(J.refPrefix??"")+"e"+ ++aX},Z._ariaRef=$;_.ref=$.ref}function nX(_,J){let Z=_.ownerDocument.activeElement===_;if(_.nodeName==="IFRAME"){let K={role:"iframe",name:"",children:[],props:{},box:i(_),receivesPointerEvents:!0,active:Z};return g_(K,_),tJ(K,J),K}let $=J.includeGenericRole?"generic":null,X=f(_)??$;if(!X||X==="presentation"||X==="none")return null;let Q=j_(xJ(_,!1)||""),U=sJ(_),W=i(_);if(X==="generic"&&W.inline&&_.childNodes.length===1&&_.childNodes[0].nodeType===Node.TEXT_NODE)return null;let Y={role:X,name:Q,children:[],props:{},box:W,receivesPointerEvents:U,active:Z};if(g_(Y,_),tJ(Y,J),A_.includes(X))Y.checked=kJ(_);if(C_.includes(X))Y.disabled=cJ(_);if(P_.includes(X))Y.expanded=uJ(_);if(vJ.includes(X)){let K=gJ(_);Y.invalid=K==="false"?!1:K==="true"?!0:K}if(f_.includes(X))Y.level=pJ(_);if(E_.includes(X))Y.pressed=hJ(_);if(w_.includes(X))Y.selected=NJ(_);if(_ instanceof HTMLInputElement||_ instanceof HTMLTextAreaElement){if(_.type!=="checkbox"&&_.type!=="radio"&&_.type!=="file")Y.children=[_.value]}return Y}function oX(_){let J=(Z)=>{let $=[];for(let Q of Z.children||[]){if(typeof Q==="string"){$.push(Q);continue}let U=J(Q);$.push(...U)}if(Z.role==="generic"&&!Z.name&&$.length<=1&&$.every((Q)=>typeof Q!=="string"&&!!Q.ref))return $;return Z.children=$,[Z]};J(_)}function tX(_){let J=($,X)=>{if(!$.length)return;let Q=j_($.join(""));if(Q)X.push(Q);$.length=0},Z=($)=>{let X=[],Q=[];for(let U of $.children||[])if(typeof U==="string")Q.push(U);else J(Q,X),Z(U),X.push(U);if(J(Q,X),$.children=X.length?X:[],$.children.length===1&&$.children[0]===$.name)$.children=[]};Z(_)}var jZ=Symbol("cachedRegex");function JX(_,J=new Map){if(_?.ref)J.set(_.ref,_);for(let Z of _?.children||[])if(typeof Z!=="string")JX(Z,J);return J}function eX(_,J){let Z=JX(J?.root),$=new Map,X=(Q,U)=>{let W=Q.children.length===U?.children.length&&h_(Q,U),Y=W;for(let K=0;K{let Q=J.get(X);if(Q==="same");else if(Q==="skip"){for(let U of X.children)if(typeof U!=="string")$(U)}else Z.push(X)};for(let X of _)if(typeof X==="string")Z.push(X);else $(X);return Z}function U_(_){return" ".repeat(_)}function XX(_,J,Z){let $=eJ(J),X=[],Q={},U=$.renderStringsAsRegex?XZ:()=>!0,W=$.renderStringsAsRegex?JZ:(H)=>H,Y=_.root.role==="fragment"?_.root.children:[_.root],K=eX(_,Z);if(Z)Y=_Z(Y,K);let z=(H,F)=>{if(J.depth&&F>J.depth)return;let j=e(W(H));if(j)X.push(U_(F)+"- text: "+j)},q=(H,F)=>{let j=H.role;if(H.name&&H.name.length<=900){let w=W(H.name);if(w){let E=w.startsWith("/")&&w.endsWith("/")?w:JSON.stringify(w);j+=" "+E}}if(H.checked==="mixed")j+=" [checked=mixed]";if(H.checked===!0)j+=" [checked]";if(H.disabled)j+=" [disabled]";if(H.expanded)j+=" [expanded]";if(H.active&&$.renderActive)j+=" [active]";if(H.invalid==="grammar"||H.invalid==="spelling")j+=` [invalid=${H.invalid}]`;if(H.invalid===!0)j+=" [invalid]";if(H.level)j+=` [level=${H.level}]`;if(H.pressed==="mixed")j+=" [pressed=mixed]";if(H.pressed===!0)j+=" [pressed]";if(H.selected===!0)j+=" [selected]";if(H.ref){if(j+=` [ref=${H.ref}]`,F&&d(H))j+=" [cursor=pointer]"}if($.renderBoxes){let w=$X(H);if(w){let E=w.getBoundingClientRect();j+=` [box=${Math.round(E.x)},${Math.round(E.y)},${Math.round(E.width)},${Math.round(E.height)}]`}}return j},L=(H)=>{return H?.children.length===1&&typeof H.children[0]==="string"&&!Object.keys(H.props).length?H.children[0]:void 0},I=(H,F,j)=>{if(J.depth&&F>J.depth)return;if(H.role==="iframe"&&H.ref)Q[H.ref]=F;if(K.get(H)==="same"&&H.ref){X.push(U_(F)+`- ref=${H.ref} [unchanged]`);return}let w=!!Z&&!F,E=U_(F)+"- "+(w?" ":"")+c_(q(H,j)),S=L(H),m=!!J.depth&&F===J.depth;if(!S&&(!H.children.length||m)&&!Object.keys(H.props).length)X.push(E);else if(S!==void 0)if(U(H,S))X.push(E+": "+e(W(S)));else X.push(E);else{X.push(E+":");for(let[C,k]of Object.entries(H.props))X.push(U_(F+1)+"- /"+C+": "+e(k));let a=!!H.ref&&j&&d(H);for(let C of H.children)if(typeof C==="string")z(U(H,C)?C:"",F+1);else I(C,F+1,j&&!a)}};for(let H of Y)if(typeof H==="string")z(H,0);else I(H,0,!!$.renderCursorPointer);return{text:X.join(` +`),iframeDepths:Q}}function JZ(_){let J=[{regex:/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/,replacement:"[0-9a-fA-F-]+"},{regex:/\b[\d,.]+[bkmBKM]+\b/,replacement:"[\\d,.]+[bkmBKM]+"},{regex:/\b\d+[hmsp]+\b/,replacement:"\\d+[hmsp]+"},{regex:/\b[\d,.]+[hmsp]+\b/,replacement:"[\\d,.]+[hmsp]+"},{regex:/\b\d+,\d+\b/,replacement:"\\d+,\\d+"},{regex:/\b\d+\.\d{2,}\b/,replacement:"\\d+\\.\\d+"},{regex:/\b\d{2,}\.\d+\b/,replacement:"\\d+\\.\\d+"},{regex:/\b\d{2,}\b/,replacement:"\\d+"}],Z="",$=0,X=new RegExp(J.map((Q)=>"("+Q.regex.source+")").join("|"),"g");if(_.replace(X,(Q,...U)=>{let W=U[U.length-2],Y=U.slice(0,-2);Z+=F_(_.slice($,W));for(let K=0;K0.1}var ZX=Symbol("element");function $X(_){return _[ZX]}function g_(_,J){_[ZX]=J}function YX(_){let J=(Z)=>{for(let $ of Array.from(Z.querySelectorAll("*"))){_($);let X=$.shadowRoot;if(X)J(X)}};J(document)}function ZZ(_,J={}){YX((Q)=>{if(Q._ariaRef)delete Q._ariaRef});let Z=_??document.body??document.documentElement,$={mode:"ai",depth:J.depth,boxes:J.boxes},X=_X(Z,$);return XX(X,$).text}function $Z(_){let J=null;return YX((Z)=>{if(!J&&Z._ariaRef?.ref===_)J=Z}),J} diff --git a/packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts b/packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts new file mode 100644 index 000000000..c37e30983 --- /dev/null +++ b/packages/coding-agent/src/tools/browser/aria/aria-snapshot.ts @@ -0,0 +1,103 @@ +import type { ElementHandle, JSHandle, Page } from "puppeteer-core"; +import ariaBundle from "./aria-snapshot.bundle.txt" with { type: "text" }; +// `aria-snapshot.bundle.txt` is a generated, committed artifact: Playwright's +// injected ARIA-snapshot sources (pinned, Apache-2.0) bundled to a CJS module. +// The upstream sources are NOT vendored — regenerate the bundle with: +// bun scripts/generate-aria-snapshot.ts +// (fetches the pinned tag, bundles in a temp dir, rewrites the .txt artifact.) + +export interface AriaSnapshotOptions { + /** Maximum tree depth to render. */ + depth?: number; + /** Append `[box=x,y,w,h]` bounding boxes to each node. */ + boxes?: boolean; +} + +/** + * Page-side evaluators built ONCE here in the worker — never inside the page, so + * page CSP never applies. They run the generated Playwright ARIA-snapshot bundle + * (CJS, see scripts/generate-aria-snapshot.ts) in a throwaway module scope. + * + * Puppeteer serializes these functions to a CDP `Runtime.evaluate` in the page's + * MAIN world (the only world where the bundle's `_ariaRef` ref expandos live — + * isolated-world locators/query-handlers cannot see them). Nothing is installed + * on `window`; the only footprint is the `_ariaRef` markers the snapshot writes, + * which are the price of actionable `[ref=eN]` ids. + */ +function buildEvaluator(params: string, call: string): (...args: unknown[]) => unknown { + return new Function( + ...params.split(",").map(p => p.trim()), + `var module = { exports: {} };\n${ariaBundle}\nreturn module.exports.${call};`, + ) as unknown as (...args: unknown[]) => unknown; +} + +// Handles (root) must stay top-level args: Puppeteer only unwraps JSHandles +// passed positionally to page.evaluate, never ones nested inside an object. +const evaluateAriaSnapshot = buildEvaluator("root, request", "ariaSnapshot(root, request)"); +const evaluateResolveRef = buildEvaluator("ref", "resolveAriaRef(ref)"); + +/** + * Capture a Playwright-format ARIA snapshot of `root` (or the whole document when + * null). Always runs in `ai` mode so every node carries a `[ref=eN]` id; resolve + * those to elements with {@link resolveAriaRefHandle}. Ids are renumbered from e1 + * on each call and remain valid until the next snapshot. + */ +export async function captureAriaSnapshot( + page: Page, + root: ElementHandle | null, + options: AriaSnapshotOptions = {}, +): Promise { + const request = { depth: options.depth, boxes: options.boxes }; + return (await page.evaluate(evaluateAriaSnapshot as never, root as never, request as never)) as string; +} + +/** + * Resolve a `[ref=eN]` id from the latest snapshot to a live `ElementHandle`, or + * null when the ref no longer matches any element. Runs in the main world so it + * sees the `_ariaRef` expandos the snapshot wrote. + */ +export async function resolveAriaRefHandle(page: Page, ref: string): Promise { + const handle = (await page.evaluateHandle(evaluateResolveRef as never, ref as never)) as JSHandle; + const element = handle.asElement(); + if (!element) { + await handle.dispose().catch(() => undefined); + return null; + } + return element as ElementHandle; +} + +const ARIA_REF_PREFIXES = ["aria-ref=", "aria-ref/", "ariaref/"]; + +/** + * Recognize the explicit `[ref=eN]` selector forms and return the bare ref id, + * else null. Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, and + * `ariaref/e5` — lets `tab.click("aria-ref=e5")` etc. act on snapshot refs. A + * bare `e5` is intentionally NOT a ref selector: the cmux backend already uses + * bare `eN`/`@eN` for its own observe ids, so requiring the prefix keeps action + * selectors meaning the same thing on both backends. (`tab.ref("e5")` still + * accepts a bare id directly.) + */ +export function parseAriaRefSelector(selector: string): string | null { + const trimmed = selector.trim(); + for (const prefix of ARIA_REF_PREFIXES) { + if (trimmed.startsWith(prefix)) { + const id = trimmed.slice(prefix.length).trim(); + return /^e\d+$/.test(id) ? id : null; + } + } + return null; +} + +/** + * Build a self-contained expression script that runs the vendored bundle in the + * page and returns the ARIA snapshot YAML. Used by the cmux backend, whose + * `browser.eval` RPC takes a script string and returns the completion value (it + * has no ElementHandle to pass in). The script resolves `selector` via + * `document.querySelector` in-page (CSS selectors only) or falls back to the + * whole document. Like the puppeteer path it installs nothing on `window`. + */ +export function buildAriaSnapshotScript(selector: string | undefined, options: AriaSnapshotOptions = {}): string { + const request = { depth: options.depth, boxes: options.boxes }; + const sel = selector ? JSON.stringify(selector) : "null"; + return `(function(){var module={exports:{}};\n${ariaBundle}\nvar __sel=${sel};var __root=__sel?document.querySelector(__sel):null;if(__sel&&!__root)throw new Error("tab.ariaSnapshot: selector "+__sel+" matched no element");return module.exports.ariaSnapshot(__root,${JSON.stringify(request)});})()`; +} diff --git a/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts b/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts index c0564a22f..4a3a9cbf5 100644 --- a/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts +++ b/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts @@ -10,6 +10,7 @@ import { resizeImage } from "../../../utils/image-resize"; import { resolveToCwd } from "../../path-utils"; import { formatScreenshot } from "../../render-utils"; import { ToolAbortError, ToolError } from "../../tool-errors"; +import { type AriaSnapshotOptions, buildAriaSnapshotScript } from "../aria/aria-snapshot"; import { DEFAULT_VIEWPORT } from "../launch"; import { extractReadableFromHtml, type ReadableFormat } from "../readable"; import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol"; @@ -49,7 +50,7 @@ interface RunContext { type WaitUntil = "load" | "domcontentloaded" | "networkidle0" | "networkidle2"; type DragTarget = string | { readonly x: number; readonly y: number }; -type SelectorKind = "css" | "ref" | "text" | "aria" | "xpath" | "pierce" | "ax"; +type SelectorKind = "css" | "ref" | "aria-ref" | "text" | "aria" | "xpath" | "pierce" | "ax"; interface SelectorSpec { kind: SelectorKind; @@ -124,6 +125,20 @@ const accessibleName = element => const findElement = spec => { if (spec.kind === "css") return document.querySelector(spec.value); if (spec.kind === "pierce") return pierceQuery(document, spec.value); + if (spec.kind === "aria-ref") { + const wanted = spec.value; + const scan = root => { + for (const el of Array.from(root.querySelectorAll("*"))) { + if (el._ariaRef && el._ariaRef.ref === wanted) return el; + if (el.shadowRoot) { + const found = scan(el.shadowRoot); + if (found) return found; + } + } + return null; + }; + return scan(document); + } if (spec.kind === "xpath") { const result = document.evaluate(spec.value, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue instanceof Element ? result.singleNodeValue : null; @@ -352,6 +367,24 @@ export class CmuxTab { return observation; } + async ariaSnapshot(selector?: string, opts?: AriaSnapshotOptions): Promise { + const timeoutMs = Math.min(this.#runContext?.timeoutMs ?? 30_000, 30_000); + const result = (await this.#request( + "browser.eval", + { script: buildAriaSnapshotScript(selector, opts) }, + timeoutMs, + )) as CmuxEvalResult; + return result.value as string; + } + + async ref(id: string): Promise { + const refId = /^e\d+$/.test(id.trim()) ? id.trim() : id.trim().replace(/^(?:aria-ref=|aria-ref\/|ariaref\/)/, ""); + const selector = `aria-ref=${refId}`; + const timeoutMs = this.#runContext?.timeoutMs ?? 30_000; + await this.#waitForSelector(selector, timeoutMs); + return new CmuxElementHandle(this, selector); + } + async click(selector: string): Promise { await this.#selectorAction(selector, "click"); } @@ -893,6 +926,8 @@ export class CmuxTab { else if (normalized.startsWith("p-aria/")) normalized = `aria/${normalized.slice("p-aria/".length)}`; else if (normalized.startsWith("p-xpath/")) normalized = `xpath/${normalized.slice("p-xpath/".length)}`; else if (normalized.startsWith("p-pierce/")) normalized = `pierce/${normalized.slice("p-pierce/".length)}`; + const ariaRef = /^(?:aria-ref=|aria-ref\/|ariaref\/)(e\d+)$/.exec(normalized); + if (ariaRef) return { kind: "aria-ref", value: ariaRef[1]!, raw }; const ref = /^@?e(\d+)$/.exec(normalized); if (ref) return { kind: "ref", value: ref[1]!, raw, ref: `@e${ref[1]}` }; const slash = normalized.indexOf("/"); diff --git a/packages/coding-agent/src/tools/browser/tab-worker.ts b/packages/coding-agent/src/tools/browser/tab-worker.ts index 2eb655d8e..691d5748b 100644 --- a/packages/coding-agent/src/tools/browser/tab-worker.ts +++ b/packages/coding-agent/src/tools/browser/tab-worker.ts @@ -22,6 +22,12 @@ import { resizeImage } from "../../utils/image-resize"; import { resolveToCwd } from "../path-utils"; import { formatScreenshot } from "../render-utils"; import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors"; +import { + type AriaSnapshotOptions, + captureAriaSnapshot, + parseAriaRefSelector, + resolveAriaRefHandle, +} from "./aria/aria-snapshot"; import { applyStealthPatches, applyViewport, @@ -106,6 +112,7 @@ interface TabApi { opts?: { waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2" }, ): Promise; observe(opts?: { includeAll?: boolean; viewportOnly?: boolean }): Promise; + ariaSnapshot(selector?: string, opts?: AriaSnapshotOptions): Promise; screenshot(opts?: ScreenshotOptions): Promise; extract(format?: ReadableFormat): Promise; click(selector: string): Promise; @@ -128,6 +135,7 @@ interface TabApi { opts?: { timeout?: number }, ): Promise; id(n: number): Promise; + ref(id: string): Promise; } function normalizeSelector(selector: string): string { @@ -792,6 +800,28 @@ export class WorkerCore { ); }), observe: opts => op("tab.observe()", quickOpMs, sig => this.#collectObservation({ ...opts, signal: sig })), + ariaSnapshot: (selector, opts) => + op( + selector ? `tab.ariaSnapshot(${JSON.stringify(selector)})` : "tab.ariaSnapshot()", + quickOpMs, + async sig => { + let root: ElementHandle | null = null; + if (selector) { + root = (await untilAborted(sig, () => + page.$(normalizeSelector(selector)), + )) as ElementHandle | null; + if (!root) + throw new ToolError( + `tab.ariaSnapshot: selector ${JSON.stringify(selector)} matched no element`, + ); + } + try { + return await untilAborted(sig, () => captureAriaSnapshot(page, root, opts)); + } finally { + await root?.dispose().catch(() => undefined); + } + }, + ), screenshot: opts => op(describeScreenshot(opts), quickOpMs, sig => this.#captureScreenshot(session, displays, screenshots, sig, opts), @@ -815,25 +845,50 @@ export class WorkerCore { }), click: selector => op(`tab.click(${JSON.stringify(selector)})`, INF, async sig => { + if (parseAriaRefSelector(selector) !== null) { + const handle = await this.#resolveAriaRef(selector); + try { + await untilAborted(sig, () => handle.click()); + } finally { + await handle.dispose().catch(() => undefined); + } + return; + } const resolved = normalizeSelector(selector); if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, timeoutMs, sig); else await untilAborted(sig, () => page.locator(resolved).setTimeout(timeoutMs).click()); }), type: (selector, text) => op(`tab.type(${JSON.stringify(selector)})`, INF, async sig => { - const handle = (await untilAborted(sig, () => - page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(), - )) as ElementHandle; + const handle = await this.#resolveActionHandle(selector, timeoutMs, sig); try { await untilAborted(sig, () => handle.type(text, { delay: 0 })); } finally { - await handle.dispose(); + await handle.dispose().catch(() => undefined); } }), fill: (selector, value) => - op(`tab.fill(${JSON.stringify(selector)})`, INF, sig => - untilAborted(sig, () => page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).fill(value)), - ), + op(`tab.fill(${JSON.stringify(selector)})`, INF, async sig => { + if (parseAriaRefSelector(selector) !== null) { + const handle = await this.#resolveAriaRef(selector); + try { + await untilAborted(sig, () => + handle.evaluate(el => { + const node = el as unknown as { value?: string; focus?: () => void }; + node.focus?.(); + if ("value" in node) node.value = ""; + }), + ); + await untilAborted(sig, () => handle.type(value, { delay: 0 })); + } finally { + await handle.dispose().catch(() => undefined); + } + return; + } + await untilAborted(sig, () => + page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).fill(value), + ); + }), press: (key, opts) => op(`tab.press(${JSON.stringify(key)})`, INF, async sig => { const selector = opts?.selector; @@ -844,13 +899,8 @@ export class WorkerCore { op("tab.scroll()", INF, sig => untilAborted(sig, () => page.mouse.wheel({ deltaX, deltaY }))), drag: (from, to) => op("tab.drag()", INF, sig => this.#drag(from, to, sig)), waitFor: selector => - op( - `tab.waitFor(${JSON.stringify(selector)})`, - INF, - async sig => - (await untilAborted(sig, () => - page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(), - )) as ElementHandle, + op(`tab.waitFor(${JSON.stringify(selector)})`, INF, sig => + this.#resolveActionHandle(selector, timeoutMs, sig), ), evaluate: (fn, ...args) => op("tab.evaluate()", INF, sig => @@ -862,9 +912,7 @@ export class WorkerCore { ) as never, scrollIntoView: selector => op(`tab.scrollIntoView(${JSON.stringify(selector)})`, INF, async sig => { - const handle = (await untilAborted(sig, () => - page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(), - )) as ElementHandle; + const handle = await this.#resolveActionHandle(selector, timeoutMs, sig); try { await untilAborted(sig, () => handle.evaluate(el => { @@ -889,6 +937,7 @@ export class WorkerCore { waitForResponse: (pattern, opts) => op("tab.waitForResponse()", INF, sig => this.#waitForResponse(pattern, opts?.timeout ?? timeoutMs, sig)), id: id => this.#resolveCachedHandle(id), + ref: id => this.#resolveAriaRef(id), }; } @@ -1188,6 +1237,29 @@ export class WorkerCore { } return handle; } + + async #resolveAriaRef(id: string): Promise { + const ref = parseAriaRefSelector(id) ?? id.trim(); + const handle = await resolveAriaRefHandle(this.#requirePage(), ref); + if (!handle) { + throw new ToolError( + `Unknown ARIA ref ${JSON.stringify(ref)}. Run tab.ariaSnapshot() to refresh refs (they renumber each snapshot).`, + ); + } + return handle; + } + + /** + * Resolve a selector to an ElementHandle for handle-based actions. An + * `aria-ref=eN` selector resolves against the latest ariaSnapshot's refs + * (main world); anything else goes through the normal locator wait. + */ + async #resolveActionHandle(selector: string, timeoutMs: number, sig: AbortSignal): Promise { + if (parseAriaRefSelector(selector) !== null) return this.#resolveAriaRef(selector); + return (await untilAborted(sig, () => + this.#requirePage().locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(), + )) as ElementHandle; + } #clearElementCache(): void { if (this.#elementCache.size === 0) { this.#elementCounter = 0; diff --git a/packages/coding-agent/test/tools/browser-aria-snapshot.test.ts b/packages/coding-agent/test/tools/browser-aria-snapshot.test.ts new file mode 100644 index 000000000..3dbd22070 --- /dev/null +++ b/packages/coding-agent/test/tools/browser-aria-snapshot.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "bun:test"; +import { buildAriaSnapshotScript, parseAriaRefSelector } from "@oh-my-pi/pi-coding-agent/tools/browser"; + +describe("parseAriaRefSelector", () => { + it("accepts the explicit aria-ref prefixes and returns the bare id", () => { + expect(parseAriaRefSelector("aria-ref=e5")).toBe("e5"); + expect(parseAriaRefSelector("aria-ref/e12")).toBe("e12"); + expect(parseAriaRefSelector("ariaref/e0")).toBe("e0"); + expect(parseAriaRefSelector(" aria-ref=e7 ")).toBe("e7"); + }); + + it("rejects a bare eN id so action selectors mean the same on both backends", () => { + // cmux already uses bare `eN`/`@eN` for its native observe refs; requiring + // the prefix keeps `tab.click("e5")` from meaning different things per backend. + expect(parseAriaRefSelector("e5")).toBeNull(); + expect(parseAriaRefSelector("@e5")).toBeNull(); + }); + + it("rejects css and other selectors", () => { + expect(parseAriaRefSelector("button#go")).toBeNull(); + expect(parseAriaRefSelector("text/Submit")).toBeNull(); + expect(parseAriaRefSelector("aria-ref=button")).toBeNull(); // not an eN id + expect(parseAriaRefSelector("aria-ref=")).toBeNull(); + }); +}); + +describe("buildAriaSnapshotScript", () => { + it("resolves a CSS root selector in-page and throws on miss", () => { + const script = buildAriaSnapshotScript("main .post"); + expect(script).toContain('var __sel="main .post"'); + expect(script).toContain("document.querySelector(__sel)"); + expect(script).toContain("matched no element"); + // The vendored bundle's entry is invoked against the resolved root. + expect(script).toContain("module.exports.ariaSnapshot(__root,"); + }); + + it("defaults the root to the whole document when no selector is given", () => { + const script = buildAriaSnapshotScript(undefined); + expect(script).toContain("var __sel=null"); + expect(script).toContain("module.exports.ariaSnapshot(__root,"); + }); + + it("threads depth and boxes options into the request payload", () => { + const script = buildAriaSnapshotScript(undefined, { depth: 3, boxes: true }); + expect(script).toContain('"depth":3'); + expect(script).toContain('"boxes":true'); + }); +});