Skip to content

List and act on controls inside shadow roots as d refs - #107

Merged
senamakel merged 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/deep-shadow-refs
Oct 17, 2026
Merged

senamakel merged 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/deep-shadow-refs

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

Controls inside open shadow roots now get refs (d1, d2, …) in a snapshot, and actions on those refs work.

Pages built from web components keep their controls inside shadow roots. On one live booking page, almost everything sits in an <ola-app> element's 60 open shadow roots. The log-in link and every ride row there are plain span/divs with a pointer cursor and a click listener: no role, no tab stop, no onclick attribute. Chrome's accessibility tree has them, and DOM.getDocument({pierce: true}) reaches them. But agent-browser's snapshot gives refs only to native/ARIA controls and to pointer elements its cursor pass finds, and that pass walks the light DOM only. So none of these controls got a ref. In compact mode, rows after the last ref were dropped entirely: an agent saw "LOG IN" as bare text and no ride list at all.

The agent-browser fork cannot take this change, so it lives in tinycomputer-browser.

How

  • deep/ (deep.js, mod.rs, merge.rs): after agent-browser's snapshot, a look walks open shadow roots and picks the controls the tree gave no ref.
    • What counts: an own pointer cursor, onclick, a tab stop, or an editable region.
    • It skips what agent-browser already refs, keeps the outermost of nested controls, and stops at 40. Each one is marked data-tc-deep.
    • place turns the line that shows a control's words into - clickable "LOG IN" [ref=d1]. Controls with no such line go into a trailing group that a max_chars cut never drops.
    • Numbers are per session and never reused. A live element keeps its number across snapshots.
    • A page with no such controls comes out byte-identical.
  • sessions/deep.rs: actions on d refs.
    • The element is found by its mark; a gone element is a stale ref.
    • It is scrolled into view and hit-tested through shadow roots. If something lies over its middle, the action fails as not actionable and names what covers it.
    • It is pressed with the browser's own pointer events (convert::press_at, shared with the surface's point press), never element.click().
    • Type and Fill press into the field and keep the focus guarantees of focus.rs: keys only where the action points.
    • Focus, Press, Hover, GetText and IsVisible work too. Any other action is InvalidInput.
  • sessions/page.rs: snapshot runs the look (capped at 3 s; a look that fails or runs late hands out no d refs), and perform routes d refs to the deep path. Page/title reporting and the origin checks are shared by both paths.
  • convert: the raw mouse press moves into convert::press_at / move_to. BrowserSurface::press_point uses it and sends the same events as before.
  • Docs: the crate README, a section in interacting.md, and a sentence in the bus browser.md.

Testing

  • Unit tests: 205/205 without the agent-browser feature, 239/239 with it (34 new).
  • Live tests (TINYCOMPUTER_BROWSER_EXECUTABLE set to a regular Chrome 155): the 4 new ones pass. The fixture is a web component with a "LOG IN" pointer span, a nested list with "Mini" / "3 min" rows and a nameless row, trailing text, a contenteditable, and a closed root. They check:
    • the d refs and their names appear;
    • a click on "Mini" reaches its handler with isTrusted === true;
    • after a re-render the old ref is stale;
    • typing lands in the contenteditable;
    • the closed root causes no error;
    • a light-DOM page's snapshot is unchanged.
  • All 36 live tests passed in one run (32 existing + 4 new).
  • rustfmt, clippy (pedantic, -D warnings, all targets, with the feature) and rustdoc -D warnings are clean.

Notes

  • Not detected:
    • controls whose only sign is a click listener (no pointer cursor, tab stop or onclick);
    • closed shadow roots (scripts cannot reach them);
    • anything inside frames.
  • A d-ref press whose click handler opens a JavaScript dialog can block until the engine's 30 s timeout. The surface's existing point press has the same limit.
  • Callers that parse ref spellings need to accept d\d+. Today only @d3 passes Target::parse as a ref.
  • The workspace-wide checks and the per-file coverage job were not run locally (disk). Like the existing live test files, the new one is mostly unexecuted without a browser.

Since the review (28121790)

An independent review found one serious placement bug and several smaller risks. All are fixed:

  1. Wrong-row placement. A control went on the first matching line after the last placed one. An earlier, unlisted line with the same words (a sold-out row, a hidden hover button, a heading) shifted every later ref up one row: with three "Select" rows and the first not listed, choosing A pressed B. deep.js now counts each control's place among the page's text with the same words, in the tree's order. merge.rs uses that place only when the tree shows the words the same number of times; otherwise the control goes to the trailing group.
  2. Reused nodes. A ref followed its DOM node even after a recycled list made the node show another item. The session now keeps each number's words: a number survives only while its element shows them, and aim, focus and text return StaleRef on a mismatch. Editable regions are named by their label, not by typed content.
  3. Gap between aim and press.
    • The aim now waits for the element to hold still for two frames.
    • It picks a point that reaches the element rather than a control nested inside it.
    • A capture-phase guard cancels any press whose path misses the element and reports NotActionable.
  4. Lost look. Numbering advances by 40 when a look's reply is lost, so numbers are never reused.
  5. Keys.
    • Focus is checked two frames after the press.
    • Type presses End first where the caret can't be set (email and number fields).
  6. Frames. No look while a raw frame command is active.
  7. Smaller fixes.
    • Per-action scans and text reads have node budgets.
    • A pointer <label> whose field already has a ref is not listed again.

Tests now:

  • Unit: 218/218 without the feature, 257/257 with it.
  • Live, regular Chrome: 9/9, 5 of them new:
    • the A/B/C "Select" case;
    • a node rewritten in place goes stale;
    • a card pressed beside its own button;
    • a layer raised on hover stops the press;
    • a field with no settable caret takes text at its end.

BrowserSurface::press_point built the raw mouse events of a press
inline. They move to convert::press_at, with convert::move_to for the
move alone, so every press the crate sends by position is the same
trusted move, button down, and button up.
Pages built from web components keep their controls inside open shadow
roots, often as plain spans and divs drawn with a pointer and wired to
a click by the component's script. agent-browser looks for such
elements in the light DOM only, so they came out as bare text, or, in a
compact snapshot, not at all: there was nothing to press.

Browser::snapshot now follows agent-browser's snapshot with one look
through the open shadow roots (deep/deep.js) for what a person can
press or type into that the tree has no ref for: an own pointer, an
onclick, a tab stop, or an editable region, drawn and outermost, at
most 40. Each is marked data-tc-deep and listed as dN, on the line
that shows its words or in a group after the tree that a max_chars cut
keeps whole. A number is never reused in a session, and an element
keeps it across snapshots. A page with none comes out unchanged.

Browser::perform acts on a d ref itself: the element is found by its
mark (gone: StaleRef), brought into view, hit-tested through open
shadow roots (covered: NotActionable naming the cover), and pressed
with trusted mouse events. Type and Fill press into a field and keep
the focus check; Focus, Press, Hover, GetText, and IsVisible work too,
and any other action is InvalidInput.
@tinysweeper

tinysweeper Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 5 lane(s) and found 0 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Incomplete
Priority: none
Reviewed head: 28121790dd63
Updated: 2026-10-15T20:48:54Z

Review snapshot

Change surface Files Review signal Count
Production 11 Active findings 0
Tests 9 Noted findings 0
Documentation 4 Resolved findings 0
Configuration 0 Pending checks/questions 20

Completeness: Incomplete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

No active actionable findings.

Could not review: crates/tinycomputer-browser/src/deep/deep.js, crates/tinycomputer-browser/src/deep/deep_tests/live_results_tests.rs, crates/tinycomputer-browser/src/deep/deep_tests/live_tests.rs, crates/tinycomputer-browser/src/deep/merge.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/sessions/deep.rs, crates/tinycomputer-browser/src/sessions/focus.rs, crates/tinycomputer-browser/src/sessions/mod.rs, crates/tinycomputer-browser/src/sessions/page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_keys_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_tests.rs, crates/tinycomputer-browser/src/surface/cover.rs, crates/tinycomputer-browser/src/surface/surface_tests/perception_tests.rs, docs/crates/tinycomputer-browser/README.md, docs/crates/tinycomputer-browser/interacting.md, docs/crates/tinycomputer-bus/browser.md

Before merge

  • Complete the critique review for crates/tinycomputer-browser/src/deep/deep_tests/live_tests.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/sessions/deep.rs, crates/tinycomputer-browser/src/sessions/focus.rs, crates/tinycomputer-browser/src/sessions/mod.rs, crates/tinycomputer-browser/src/sessions/page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_keys_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_tests.rs, crates/tinycomputer-browser/src/deep/deep.js, crates/tinycomputer-browser/src/deep/deep_tests/live_results_tests.rs, crates/tinycomputer-browser/src/surface/cover.rs, crates/tinycomputer-browser/src/surface/surface_tests/perception_tests.rs, docs/crates/tinycomputer-browser/README.md, docs/crates/tinycomputer-browser/interacting.md, docs/crates/tinycomputer-bus/browser.md.
  • Complete the security review for crates/tinycomputer-browser/src/deep/merge.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/deep/deep.js.

How this fits together

flowchart LR
  n0["Browser<br/>changed"]:::changed
  n1["Session<br/>changed"]:::changed
  n2["sent"]:::impacted
  n3["open"]:::impacted
  n4["...ake_is_refused_before_the_page_is_touched"]:::impacted
  n5["page"]:::impacted
  n6["..._the_deep_controls_with_refs_of_their_own"]:::impacted
  n0 -->|uses| n1
  n3 -->|uses| n0
  n4 -->|calls| n2
  n4 -->|tests| n2
  n4 -->|calls| n3
  n4 -->|tests| n3
  n4 -->|calls| n5
  n4 -->|tests| n5
  n6 -->|calls| n2
  n6 -->|tests| n2
  n6 -->|calls| n3
  n6 -->|tests| n3
  n6 -->|calls| n5
  n6 -->|tests| n5
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: incomplete; unanswered: crates/tinycomputer-browser/src/deep/deep_tests/live_tests.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/sessions/deep.rs, crates/tinycomputer-browser/src/sessions/focus.rs, crates/tinycomputer-browser/src/sessions/mod.rs, crates/tinycomputer-browser/src/sessions/page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_keys_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_tests.rs, crates/tinycomputer-browser/src/deep/deep.js, crates/tinycomputer-browser/src/deep/deep_tests/live_results_tests.rs, crates/tinycomputer-browser/src/surface/cover.rs, crates/tinycomputer-browser/src/surface/surface_tests/perception_tests.rs, docs/crates/tinycomputer-browser/README.md, docs/crates/tinycomputer-browser/interacting.md, docs/crates/tinycomputer-bus/browser.md
  • Lane summary: Reviewed 7 files; 0 findings. 17 files could not be reviewed: crates/tinycomputer-browser/src/deep/deep_tests/live_tests.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/sessions/deep.rs, crates/tinycomputer-browser/src/sessions/focus.rs, crates/tinycomputer-browser/src/sessions/mod.rs, crates/tinycomputer-browser/src/sessions/page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_keys_tests.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_page.rs, crates/tinycomputer-browser/src/sessions/sessions_tests/deep_tests.rs, crates/tinycomputer-browser/src/deep/deep.js, crates/tinycomputer-browser/src/deep/deep_tests/live_results_tests.rs, crates/tinycomputer-browser/src/surface/cover.rs, crates/tinycomputer-browser/src/surface/surface_tests/perception_tests.rs, docs/crates/tinycomputer-browser/README.md, docs/crates/tinycomputer-browser/interacting.md, docs/crates/tinycomputer-bus/browser.md. _The code index is behind this pull request (indexed at `89d51c12d10c`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: incomplete; unanswered: crates/tinycomputer-browser/src/deep/merge.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/deep/deep.js
  • Lane summary: Reviewed 17 files; 0 findings. 3 files could not be reviewed: crates/tinycomputer-browser/src/deep/merge.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/deep/deep.js. 4 files were not security-reviewed: crates/tinycomputer-browser/README.md (prose or tabular data), docs/crates/tinycomputer-browser/README.md (prose or tabular data), docs/crates/tinycomputer-browser/interacting.md (prose or tabular data), docs/crates/tinycomputer-bus/browser.md (prose or tabular data). _The code index is behind this pull request (indexed at `89d51c12d10c`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: _The code index is behind this pull request (indexed at `89d51c12d10c`), so retrieved context may be out of date._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The description matches the diff: every mechanism it claims (the deep.js look and its limits, `d` numbering and staling rules, the `place`/trailing-group behaviour, `convert::press_at` shared with the surface's point press, the session routing in `sessions/page.rs`, the docs edits, and the stated not-detected cases) is present and described accurately, and I found nothing significant in the diff the body fails to mention. The test-count and live-run claims in the Testing section are not checkable from the diff alone, but nothing in it contradicts them. _The code index is behind this pull request (indexed at `89d51c12d10c`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, glm-5.3-flash,
  • Spend: $0.017498
  • Tokens: 1232007 input · 38741 output · 125440 cached · 738 embedding
  • Continuity: summary cache chain restarted at the storage ceiling.
Head State Pass summary
28121790dd63 incomplete 0 active finding(s), 0 resolved finding(s) (at 2026-10-15T20:48:54Z)

tinysweeper 0.1.2

@coderabbitai

coderabbitai Bot commented Oct 15, 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: 6eef379f-04c4-434b-8d33-ade1ebaf1884
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

clippy's assert_is_empty (Rust 1.99, the CI toolchain) refuses
assert!(x.is_empty()); the rest of the workspace states these as
assert_eq!(x.len(), 0).
Review fixes for the d refs of controls inside shadow roots:

- Placement: deep.js now counts where each control's words show among
  the page's text, the way the tree shows its lines, and merge.rs puts
  a control on that occurrence only when the tree shows the words as
  many times; otherwise it goes to the trailing group. An unlisted row
  with the same words (a sold-out "Select") no longer moves every later
  control up a row.
- Identity: Minted keeps the words each number was handed out with. A
  number survives a look only while its element shows the same words,
  and aim, focus, and text refuse a node re-drawn in place as StaleRef.
  An editable region is named by its label, never its typed content.
- Pressing: the aim waits for the element to hold still for two
  frames, picks a point that reaches the element and not a control
  nested in it (a field, when aiming for text), and arms a capture
  guard that stops any part of the press whose path misses the
  element, reported as NotActionable.
- A look whose reply is lost advances the numbering by 40, so the
  numbers it may have marked are never handed out.
- Text: end/select wait two frames before the focus check, and Type
  presses End where a field's caret cannot be set (email, number).
- No look while a raw frame command has agent-browser read a frame.
- Node budgets on per-action scans and text reads; a pointer label
  whose field the tree already refs is not listed again.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking, but could not review everything, so this is not an approval: crates/tinycomputer-browser/src/deep/deep.js, crates/tinycomputer-browser/src/deep/deep_tests/live_results_tests.rs, crates/tinycomputer-browser/src/deep/deep_tests/live_tests.rs, crates/tinycomputer-browser/src/deep/merge.rs, crates/tinycomputer-browser/src/lib.rs, crates/tinycomputer-browser/src/sessions/deep.rs, crates/tinycomputer-browser/src/sessions/focus.rs, crates/tinycomputer-browser/src/sessions/mod.rs and 10 more.

             $0.0175 · 1,232,007 in / 38,741 out · 125,440 cached (10%) · ladder/vectors-oai3, glm-5.3-flash,  · 738 embedded
critique:    $0.0023 · 235,620 in   / 4,443 out  · 0 cached (0%)        · glm-5.3-flash,
security:    $0.0069 · 735,151 in   / 20,766 out · 71,680 cached (10%)  · glm-5.3-flash
tests:       $0.0006 · 68,983 in    / 7 out      · 0 cached (0%)        · glm-5.3-flash
description: $0.0017 · 127,703 in   / 9,162 out  · 53,760 cached (42%)  · glm-5.3-flash

@senamakel
senamakel merged commit d715ee3 into tinyhumansai:main Oct 17, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants