Skip to content

Add native accessibility tree snapshot via Chrome CDP - #26

Open
compnew2006 wants to merge 1 commit into
mainfrom
claude/friendly-mayer-ehy11a
Open

compnew2006 wants to merge 1 commit into
mainfrom
claude/friendly-mayer-ehy11a

Conversation

@compnew2006

Copy link
Copy Markdown
Owner

Summary

Adds support for browser_snapshot source:"native" to retrieve the accessibility tree directly from Chrome's accessibility engine (via CDP Accessibility.getFullAXTree) instead of approximating it from the DOM. This provides agents with the exact tree that assistive technology sees, including proper handling of ARIA, native widget roles, and accessibility state.

Key Changes

  • New module extension/lib/ax-native.js: Pure utility functions to transform Chrome's CDP accessibility nodes into the same compact/full tree shape the DOM walker produces, enabling agents and ref tools to work unchanged:

    • shapeAxTree(): Converts flat CDP nodes into a nested tree with configurable depth, character budget, and compact/full modes
    • mergeFrameTrees(): Grafts child frame accessibility trees under their owner iframe nodes with proper ID namespacing
    • applyBindings(): Settles the tree after page binding, attaching hrefs and removing unbound refs
    • pathOfTarget() and backendIdAtPath(): Compute structural paths through shadow roots and iframes to resolve CDP node IDs to live elements
    • Role and state extraction helpers (axProps(), stateOf(), valueOf())
  • New handler extension/handlers/ax-snapshot.js: Orchestrates the native snapshot pipeline:

    • Fetches the accessibility forest from Chrome (main frame + same-process child frames)
    • Resolves CDP backendNodeId values to live page elements via the main world
    • Batches CDP calls per frame (Chrome rejects mixing JS contexts)
    • Binds refs to the page's shared registry and records smart-selector fallbacks
    • Falls back to DOM snapshot if accessibility tree is unavailable (protected pages, timeout)
  • Integration in extension/handlers/inspection.js: Routes source:"native" snapshots to the new handler while preserving DOM snapshot as the default

  • Comprehensive test coverage:

    • tests/ax-native.test.ts: Unit tests for tree shaping, frame merging, binding, and edge cases (truncation, depth limits, ref caps, scoping)
    • tests/extension-ax-snapshot.test.ts: End-to-end tests with fake DOM and mocked CDP, verifying element binding across shadow roots and iframes, fallback generation, and selector scoping
  • Schema and documentation updates: Added source parameter to browser_snapshot tool schema, updated agent guidance and README to document the native option

Notable Implementation Details

  • Pure functions: All tree transformation logic is side-effect-free and unit-testable, with CDP and page plumbing isolated in handlers
  • Frame batching: Respects Chrome's constraint that one Runtime.callFunctionOn cannot mix JS contexts; resolves paths per frame
  • Ref binding without mutation: Elements are registered via structural path walking (through closed shadow roots and iframes) without touching attributes or triggering DOM mutations
  • Graceful degradation: Falls back to DOM snapshot if Chrome's accessibility tree is unavailable (timeout, debugger attach failure, protected pages)
  • Fingerprinting: Tracks role|name pairs to mark newly appeared nodes with isNew: true for incremental updates
  • Configurable output: Supports compact mode (interactive + landmarks + headings), full mode (all meaningful nodes), depth limits, and character budgets

https://claude.ai/code/session_0197iXdHWDwMNUNW9NP9nTP5

…native"

browser_snapshot gains an opt-in `source: "native"`. Instead of the DOM
walker it reads the tree Chrome itself computes (CDP
Accessibility.getFullAXTree): exact roles, accessible names (aria-labelledby,
<label>, native widget semantics), states (checked, expanded, invalid,
heading level) and aria-hidden/inert exclusion. The default stays
source:"dom" and never touches the debugger.

The output has the same shape as the DOM snapshot and its refs work with
every ref tool. Each AX node is bound to its real element in the existing
page-side ref registry by a structural path (resolved through the main world,
walked in the isolated world, tag-verified), so closed shadow roots and
same-origin iframes work, same-named siblings stay distinct, and the
smart-selector fallback and isNew behave as before. No page mutation.

- per-frame AX trees grafted under their Iframe node; ids namespaced
- bridge batched per frame (Chrome rejects mixing JS contexts in one call)
- scoping by selector or ref (resolved in the page, located in a pierced
  DOM.getDocument dump with the same path grammar)
- browser-internal controls with no element (a date input's sub-fields) are
  omitted from the compact tree and counted in unreachableNodes
- if the debugger cannot attach, the DOM tree is returned with
  nativeUnavailable explaining why

Verified in headless Chromium with the real extension, and through the real
MCP client and daemon. Unit and integration tests added.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197iXdHWDwMNUNW9NP9nTP5
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ad491972-fe5d-45a5-9315-5352fbda059a
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Owner Author

build (20) is red, but not because of this PR's code. It fails at npm run security:audit (npm audit --omit=dev --audit-level=high): critical advisory GHSA-jqcg-44mw-7w3h in proxy-addr@2.0.7 (reached via @modelcontextprotocol/sdk → express@5.2.1). build (22) was cancelled by matrix fail-fast, not a separate failure; lint, typecheck and the 492 tests all pass locally.

Why it isn't this PR's:

  • package.json and package-lock.json are byte-identical to main; the PR touches no dependency.
  • It reproduces locally on the same lockfile. main's last CI run (Sep 30) was green but predates the advisory, so main would fail this step too.
  • No open PR or main commit carries a fix.

Fix (verified in a scratch copy, not pushed here to keep this PR scoped): lockfile-only bump of proxy-addr 2.0.7 → 2.0.8, which is inside express's ^2.0.7 range. With it npm run security:audit reports 0 vulnerabilities.

Suggested paths: land that bump on main (Dependabot or a one-line PR), then merge main into this branch; or say so and I'll add the bump to this branch instead.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants