import type { Driver } from "../../core/ports.js"; import type { ConsoleMessage, Evidence, NetworkRequest, PageElement, SettleOptions, Target } from "../../core/types.js"; export interface ChromeDriverOptions { command?: string; args?: string[]; /** Per-MCP-call timeout (ms). A hung tool call rejects instead of wedging the run. Default 30s. */ timeoutMs?: number; /** Timeout for the initial browser launch/connect (ms). Default 60s (first run may download). */ connectTimeoutMs?: number; /** Surface roleless `cursor:pointer` regions as clickable controls in the listing (#132). Default on; * set false to see only the raw a11y tree. */ promoteClickables?: boolean; } export declare class ChromeDevToolsDriver implements Driver { private readonly opts; private client?; private transport?; private initialUrl?; private snapshotCache?; private readonly seenPages; private closed; private crashed; private lastRaw?; private lastClickable?; constructor(opts?: ChromeDriverOptions); private trackPages; /** If the last action opened a new tab, switch to it — else later actions silently hit the wrong page. */ private followNewTab; /** Reject after `ms` if `p` hasn't settled — so a hung MCP/subprocess never wedges the caller. */ private withTimeout; private ensureConnected; private call; goto(url: string): Promise; click(target: Target): Promise; doubleClick(target: Target): Promise; /** * Run an interactive MCP action, accepting any JS dialog it triggers. A `confirm`/`alert`/`prompt` * opens a dialog the MCP can't interact through (no per-action hook) — the call errors and the run * would wedge. The action's own handler already fired, so accept the dialog and treat it as done * (#17). Generic over the action, so a dialog from a click, a form submit (Enter), a select, etc. * is handled the same way — no per-verb special-casing. */ private callAccepting; hover(target: Target): Promise; type(target: Target, text: string): Promise; select(target: Target, value: string): Promise; /** The `value`'s `option` row among rows that appeared AFTER the dropdown opened — the watermark * (`before`) keeps a native ` (vs a custom ARIA combobox that shares * the a11y role but no-ops on `fill`). Decided by the element's tag, not its a11y role — both render * as `combobox` — via an in-page probe. Best-effort: an unreachable probe treats it as non-native. */ private isNativeSelect; pressKey(key: string): Promise; scroll(direction?: "down" | "up"): Promise; screenshot(): Promise; /** Cache the page snapshot so resolve + the discover loop don't both re-fetch it; actions invalidate it. */ private getSnapshot; snapshot(): Promise; /** Labels of roleless `cursor:pointer` regions (#132), one per region (de-nested), capped. * Candidates = named StaticText rows (a region's visible label); the DOM probe reports each one's * clickable-region id. Best-effort — a failed probe promotes nothing. Uses only web-universal * signals (invariant #1). */ private probeClickableLabels; settle(options?: SettleOptions): Promise; observe(): Promise; close(): Promise; locate(target: Target): Promise; private resolveUid; /** * `resolveTargetUid`, plus a hit test when the name is ambiguous ACROSS roles (#176). That case * resolves by tree order today — deliberately, because an a11y wrapper pair (link "X" over * StaticText "X") is two rows for one element — but the same shape covers a real failure: a * modal's button and a background nav link share a name, and tree order picks the background * link, navigating away instead of submitting. `role=dialog` is absent on plenty of real modals, * so the reachable candidate is the signal: a backdrop-covered or hidden element fails a * center-point hit test, the wrapper pair still answers with its own element's role. * * The probe reports a ROLE, not an element: the MCP text interface has no uid → DOM mapping, so * the answer is fed back through the existing role narrowing instead of joined positionally. * Nothing reachable, an unmapped role, or a page that refuses the script leaves the current * tree-order behavior untouched (fail-safe), and replay stays model-free either way (invariant #4). */ private resolveVisible; /** Roles of the same-named elements a center-point hit test actually reaches; [] on any failure. */ private probeReachableRoles; /** Resolve a CSS selector to a snapshot uid: read the element's accessible name in-page, then * join it back to the a11y snapshot (the MCP text interface has no direct CSS→uid mapping). */ private resolveSelectorUid; } /** True if an MCP error means a click opened a JS dialog (confirm/alert/prompt) that now blocks. */ export declare function isOpenDialog(err: unknown): boolean; /** `uid=1_3 link "Learn more" …` → {role:"link", name:"Learn more"} for named rows, with form * state (#93) parsed from the attribute tail: booleans render bare (`checked`, `disabled` — not * the `checkable`/`disableable` capability tokens), strings as `attr="…"`. */ export declare function parseElements(snapshot: string): PageElement[]; export interface SnapshotRow { uid: string; role: string; name: string; } /** `uid=1_3 link "Learn more" …` → ordered {uid, role, name} rows (the role-adjacent quoted name). */ export declare function parseSnapshotRows(snapshot: string): SnapshotRow[]; /** * Multi-locator resolution. Prefers the accessible name (exact over substring, role-aware if * known); `nth` addresses the Nth name match when several elements carry the same name (#92). * If the name no longer matches, falls back to role + structural index so a renamed control still * resolves WITHOUT the LLM — but only when that fallback is unambiguous (P3): with several same-role * candidates a reorder would silently select the wrong element, so it yields nothing and lets * self-heal pick by intent instead. */ export declare function resolveTargetUid(rows: SnapshotRow[], target: Target): string | undefined; /** * The distinct roles of the exact name matches that `resolveTargetUid` would settle by TREE ORDER — * the cross-role ambiguity it deliberately does not refuse (#127). Empty when the target already * says which element it means (`role`/`nth`), when the name resolves without a guess, or when the * matches repeat a role (that class is refused, not guessed). */ export declare function crossRoleCandidates(rows: SnapshotRow[], target: Target): string[]; /** * The one role to narrow a cross-role ambiguity by — and only on evidence. Narrowing requires that * every candidate role was accounted for by the probe, that none of them is merely unmeasured, and * that exactly one is reachable. Anything else abstains and the caller keeps the existing * tree-order behavior, which is what this did before #176. */ export declare function probedRole(candidates: readonly string[], probe: { reachable: readonly string[]; occluded?: readonly string[]; unknown?: readonly string[]; }): string | undefined; /** * In-page probe over the elements named `text`, sorted by what a center-point hit test can say: * `reachable` (the point lands on it), `occluded` (it lands on something else — a backdrop), and * `unknown` (below the fold, zero-size, nothing at the point, or scrolled out of its own * `overflow` container). The split matters because the driver clicks through puppeteer's `Locator`, * which scrolls the target into view first — the window and any clipping ancestor alike — and the * a11y snapshot it narrows is unfiltered by viewport. So a control the viewer cannot see right now * is still a perfectly good click target, and treating it as unreachable would narrow onto a * visible decoy instead. Only being covered where it does sit is evidence. `value` is read for inputs, since `` * is the common modal submit and carries its name nowhere else. * * All three buckets are returned, including the one the caller then ignores: `occluded` is how the * caller tells "the page hid it" from "this probe never saw it". The probe's reach is narrower than * the a11y tree in four known ways — it does not enter shadow roots or iframes, its name is a * short approximation (no `aria-labelledby`, no `