The accessibility-tree (AX) subsystem is the primary page-interaction path for the agent. It replaces the older index-based get_interactive_elements for almost all flows.
Two content scripts loaded in order:
accessibility-tree.js— the tree builder andref_idregistrycontent.js— the tool handlers that use the tree
Both are injected into <all_urls> pages at document_idle.
Builds the tree with an internal generateAccessibilityTree(...) (invoked by
the agent via executeScript) and installs a ref-resolution API on window
(__wbElementMap, __wb_ax_lookup, __wb_ax_release):
Walks the DOM and emits a flat, indented text tree:
dialog "Add a product" [ref_166]
heading "Add a product" [ref_167]
button "Close" [ref_169]
textbox "Name" [ref_170] type="text" placeholder="Product name" value="namaz"
combobox "Billing period" [ref_180] type="button"
Parameters:
| Parameter | Default | Description |
|---|---|---|
filter |
'all' |
'all' (whole DOM), 'visible' (in-viewport, visible nodes), 'interactive' (clickable/typeable only) |
maxDepth |
15 |
Max tree depth to descend |
maxChars |
— | Hard cap on output length (auto-slices with autoDegraded:true if exceeded) |
ref_id |
— | Anchor at a specific element's subtree instead of document.body |
page |
— | 1-based chunk number for paginated results when tree is truncated |
Output format:
role "accessible name" [ref_id] href="..." type="..." placeholder="..." value="..."
Indentation is 1 space per tree-depth level (skipped generic containers don't bump depth).
Resolves a ref_N string back to the live DOM Element. Returns null if the element was removed from the DOM.
When a lookup misses, returns up to n nearby still-valid ref_ids so the error message can guide the model back on track.
- A plain object (
Object.create(null)) keyed byref_Nstrings - Each value is a
WeakRefto the DOM element - A monotonic counter (
window.__wbRefCounter) assigns the nextref_N - The map is partially cleared at the start of every tree build: entries whose
deref()returnsnullare swept. Live entries survive across calls.
- Within a turn: a
ref_idfetched fromget_accessibility_treeis guaranteed to resolve in the same turn. - Across turns: a
ref_idresolves as long as the element survives in the DOM. Elements removed by navigation or DOM manipulation become unresolvable (the tool returns a clear "not found" error and suggests re-reading the tree). - After SPA navigation: the map survives, but most elements from the old route are gone → their refs will miss. The agent is expected to re-call
get_accessibility_treeafter navigation.
Without WeakRef, the map would pin every element it ever indexed, preventing garbage collection and leaking memory on long-lived pages (SPAs, chat apps). With WeakRef, the browser can GC removed elements naturally. The cost is that deref() can return null even for elements that exist if the GC ran — but in practice this is rare within a single agent turn (sub-second) and the agent re-reads the tree on navigation anyway.
The primary page-reading tool. Returns the rendered tree string plus metadata (truncated, hasMore, autoDegraded, notice).
The agent uses this as its first action on almost every turn — it's faster and cheaper than a screenshot, and works on text-only models.
- Resolves
ref_idvia__wb_ax_lookup() - Scrolls into view (
scrollIntoView({block: 'center'})) - Focuses the element
- Dispatches
el.click()
Returns {success, method, tag, rect, name, href?, navigates?, hint?}.
Both builds first preserve the compatible synthetic el.click() path. On Chrome only, a safe generic target may receive one guarded CDP Input.dispatchMouseEvent fallback after the page and target remain stable through two observation intervals. A URL change, handler-driven focus change, synchronous target-local mutation, or delayed semantic target state such as aria-current proves progress. Whole-page churn and delayed target name/class/style/child changes are retained only as diagnostic hints, because unrelated chat previews, unread badges, and timestamps are common. A nearby mutating XHR or non-telemetry beacon, new tab, or download makes the result inconclusive and vetoes the retry; background reads and obvious telemetry/heartbeat traffic are ignored. CSS-hidden, pointer-disabled, native, stateful/toggle, form, download, and potentially mutating targets are never auto-retried. Firefox remains synthetic-only.
No finite observation window can prove application-internal state that produces no URL, focus, semantic target, network, tab, download, or other visible signal. The generic-target gates, delayed settle, and one-shot rule minimize—but cannot completely eliminate—the possibility that a silent successful synthetic handler receives one trusted second activation.
- Resolves
ref_id - Uses
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(el, value)to bypass React/Vue controlled-component wrappers - Dispatches
input+changeevents - For contenteditable: sets
textContent+ dispatchesbeforeinput+input
Rejects non-typeable input types (checkbox, radio, submit, file) with a clear error.
Atomically focuses + (optionally clears) + types + (optionally submits). The one-shot equivalent of click_ax + type_ax.
Combobox-aware submit: when submit:true, the tool detects if the field is a combobox/autocomplete (role=combobox, aria-autocomplete, aria-controls pointing at a listbox, or a visible listbox on the page). If so, dispatches ArrowDown → Enter with small delays to commit the highlighted option. Otherwise falls back to form.requestSubmit() or a plain Enter key press.
When building the tree, open dialogs, listboxes, menus, and [aria-expanded=true] comboboxes are emitted under an [open overlays] banner at the top of the tree — before the rest of the page content. This ensures portal-rendered popups (React, Radix, Stripe) survive the 3,000-char soft cap seen by the model.
getAccessibleName(el) follows this order:
<select>selected option's textaria-labelaria-labelledby— concatenates all referenced ids' textplaceholdertitlealt<label for>lookup- Input
value(submit/button/reset only — never for text inputs) - Direct text content
innerTextfallback for buttons, links, summary- Preceding sibling text (unlabeled form fields pattern: "Every 1 month(s)" → the preceding text is the label)
- Direct text fallback
The CDP client (cdp-client.js) can pierce closed shadow roots via Runtime.evaluate:
await cdpClient.evaluate(tabId, `
(() => {
const host = document.querySelector('my-component');
return host.shadowRoot ? 'open' : 'closed';
})()
`);For deeper queries, shadow_dom_query uses CDP's DOM.getDocument + DOM.querySelector to reach into closed roots.
Tool exposure is tiered: get_shadow_dom, shadow_dom_query, and get_frames
are Full Act fallbacks, and Dev mode also adds them for Mid-tier providers so
page-debugging runs can inspect Web Component and iframe structure without
giving Mid normal Act the whole Full UI fallback surface.
Only open shadow roots (element.shadowRoot) are accessible. Closed roots cannot be read through the content script. execute_js is exposed in Dev mode on both builds, but ordinary page JavaScript still cannot obtain a closed root, and the tree builder cannot reach it.
get_frames, iframe_read, iframe_click, and iframe_type work with cross-origin iframes because the extension injects content scripts directly into each frame, bypassing the same-origin policy.
The tree builder does not recurse into iframes by default. The agent must explicitly call iframe_read or get_frames to discover and read iframe content.
| Failure | Symptom | Fix |
|---|---|---|
| Element removed from DOM | click_ax returns "not found" |
Re-read the tree; the page may have re-rendered |
| Stale ref after SPA nav | All refs miss | Agent should read the tree again after /navigate or wait_for_stable |
| Shadow DOM closed root | Tree shows <my-component> but not its children |
Use get_shadow_dom + shadow_dom_query on Chrome; Firefox cannot pierce a closed root |
| iframe not in tree | Agent can't find iframe content | Call get_frames then iframe_read / iframe_click |
| Truncated tree | truncated: true + hasMore: true |
Call get_accessibility_tree with page: nextPage or ref_id to zoom in |
| Portaled overlay not visible | Tree shows the combobox but not the dropdown | The overlay is hoisted to the [open overlays] section — re-read with filter: 'all' |
- The tree output is visible in verbose mode (side panel toggle)
window.__wbElementMapin the page console lists all live refswindow.__wb_ax_lookup('ref_42')tests a specific ref- The deep-verbose debug log (
Shift+clickverbose button) dumps the last 200 LLM request/response pairs including AX tool results