Skip to content

fix(web): put on-screen text in the snapshot tree instead of losing it - #3515

Merged
thymikee merged 3 commits into
feat/snapshot-optional-refsfrom
fix/web-snapshot-refless-text
Oct 13, 2026
Merged

thymikee merged 3 commits into
feat/snapshot-optional-refsfrom
fix/web-snapshot-refless-text

Conversation

@thymikee

@thymikee thymikee commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

On --platform web, text that is on the screen reached the reader only when the backend happened to mint a ref for it. Two independent causes, both fixed here. Stacked on #3547 (feat(snapshot)!: make a node's ref optional and mint it per producer), which owns the contract and the address surfaces; this PR owns the web capture. 9 files, 750 gross lines.

1. The capture asked for --compact, and that flag deletes text along with its container. agent-browser's compaction removes a ref-less element together with the text under it. Same page, same moment:

raw      - status\n  - StaticText "Hello Zed"     ← the announcement is there
compact  (both lines gone)

Role census on a 408-ref page, raw vs compact: ListMarker 200 → 0, status 1 → 0, every ref-bearing role unchanged. The flag that exists to hide noise was what hid a status region that had just announced something.

2. The projection dropped every line without a ref (if (!ref) continue), so static text, a paragraph, a label element and a status announcement never became nodes at all.

--compact is no longer requested. The pruning it existed to do is owned by the projection, which can tell an empty container from the text inside it: a ref-less line carrying no text (form, list, paragraph, a silent status) and a presentational role (listmarker) produce no node, and a dropped line's children re-parent to its parent rather than to whichever node was kept last.

Text with no ref becomes a node carrying that text on value, not label: the backend spells a control's visible string twice — as the control's accessible name and as its own text line — and a second label claiming it makes an ordinary label="Name" read refuse as ambiguous. Measured through the real pipeline before choosing: text="Name" → ambiguous when both carry it as label, resolves the textbox when the text line carries it as value. Every text door already reads value (wait text, find text, get text, text=, the scope filter, the printed line).

Suppression of a text line is a structural relation read off the tree, not a page-global denylist: a line is a duplicate when it renders inside a <label> element whose control reports the string as its name, or sits inside the control that reports it as its name or value. Anything else keeps its own node, which is what preserves the content this capture exists to publish — an announcement that happens to repeat a button's label, or a table cell echoing a field's contents, is content. extractQuotedText parses and unescapes an escaped quoted payload; the trailing : value form stays raw, because that is what the backend prints and unescaping it would publish a value the page does not hold.

A ref is now read only from a ref=eN annotation or an @eN token outside quoted text: StaticText "retry in e3 seconds" names no element, and used to be handed the ref of something else.

agent-device snapshot            #    [text] "Hello Zed"   (was: absent)
agent-device wait text "Hello Zed"   # exit 0              (was: timeout)
agent-device get text text="Hello Zed" # Hello Zed         (was: no match)

Validation

Tested at 20b6693ae, rebased on #3547 e67b761af (which is rebased on origin/main). pnpm check:affected --run --base feat/snapshot-optional-refs on that head under the machine-wide gate lock: all runnable checks passed.

The rule this PR exists for is now pinned through the router. The emulated web page (test/integration/provider-scenarios/web-world.ts:232) ends with the line a real capture prints for content it cannot address — text on value, no ref, no box — and web-desktop.test.ts asserts, through the request router, that snapshot --json publishes it with no ref key, find text <it> list prints = (no ref) [text] "Order saved", wait text <it> answers it, and click text=<it> refuses with selector_not_found. Mutation-checked: deleting the snapshotRefMinting(data) argument at snapshot-state.ts:83 mints the node e7 and fails the first assertion. That test also found a real defect — toDaemonFindData re-added an own-property ref whose value was not a ref, which the CLI formatter, the MCP pin merge and a Node consumer all read — fixed on #3547. This head rebases ddf73fdc5 onto #3547's rebased head and adds the two review commits above; the capture and the projection are unchanged by the move, and the minting mode that decides this tree's refs is unchanged for the only pair the web interactor can produce ({ backend: 'web', producer: 'agent-browser' } → backend-issued). Projection tests are driven by payloads captured from the real backend (form after submit, role vocabulary, list with markers, empty form), not hand-written approximations: the announcement keeps its text and loses its address, a label's text is subsumed under the control that carries it as its name, empty containers and list markers drop out with children re-parented, geometry attaches only to measurable nodes, and ref-shaped text stays content.

Live before/after — local page on port 8766, managed agent-browser 0.27.1, fill label="Name" Zed then click role=button label="Submit", base origin/main vs this head:

command base head
snapshot 8 nodes, no announcement 9 nodes, [text] "Hello Zed"
wait text "Hello Zed" times out exit 0
find text "Hello Zed" exists no match Found: true
find text "Hello" list 0 matches = (no ref) [text] "Hello Zed"
get text text="Hello Zed" no match Hello Zed
click text="Hello Zed" refuses refuses (unchanged, correctly)

label="Name" still resolves uniquely to the textbox with a visible <label> on the page, and is visible label="Name" still works before and after a fill.

Documented limitation

A ref-less node has no rect — the backend measures boxes for refs and has no handle for this content — so is visible answers for it through its nearest visibility anchor (verified live: is visible passes, is hidden refuses) and it is never a tap target. snapshot -i still shows interactive elements only. Recorded in website/docs/docs/commands.md, next to the iOS text-pair note that users read for the same question.

Review round

All five cubic threads on this PR are replied to and resolved: four fixed in 0ab9b9ddd (structural suppression instead of a name denylist, extractQuotedText unescaping, the attachRefs JSDoc, the kernel's snapshotRefBodies replacing the selector-side ref filter) and the fifth answered with live-backend evidence in a1d1eb862 rather than changed — the trailing : value form is raw on the wire, so unescaping it would publish a value the page does not hold, and the dedup it worried about already happens.

The branch was then split for the 1,000-line budget: the contract and address surfaces moved to #3547, which this branch now stacks on. Content is unchanged by the split (git diff --stat across both branches reproduces the old head's file set), and the review threads' commits remain reachable from this PR's history.

Closes #3509

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 29 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-web/src/agent-browser-snapshot.ts Outdated
Comment thread packages/platform-web/src/agent-browser-snapshot.ts
Comment thread packages/kernel/src/snapshot.ts
Comment thread packages/selectors/src/target-evidence.ts Outdated
@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from 0da0d7e to 0ab9b9d Compare October 12, 2026 16:27
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.19 MB 5.19 MB +1.6 kB
Package (unpacked) 5.19 MB 5.19 MB +1.6 kB
Package (download) 1.56 MB 1.56 MB +551 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.8 ms 29.8 ms +1.0 ms
CLI --help 81.0 ms 77.9 ms -3.2 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-web/src/agent-browser-snapshot.ts
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3515/

Built to branch gh-pages at 2026-10-13 13:05 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@thymikee

Copy link
Copy Markdown
Member Author

The code has one gap to close before merge: nothing tests the rule that a web snapshot publishes only backend-issued refs, as reviewed at a1d1eb8. Without that test, a later change to snapshot-state can bring back the wrong-element ref collisions from #2283 on web, and no test would fail.

The only thing that stops a web capture from minting dense refs is the snapshotRefMinting(data) argument to attachRefs in snapshot-state.ts. No test drives buildSnapshotState or the daemon route with web provenance and a ref-less node. The kernel tests call attachRefs and snapshotRefMinting directly. The integration web world was changed to give every node a ref, so dense and backend-issued modes give the same tree there (web-world.ts). If the argument is deleted, all tests still pass, and a ref-less text at index 1 is minted e2, the same ref as the backend's button. Please add one ref-less node (no ref, value text, no rect) to webSnapshotNodes and assert through the router that snapshot --json shows it without a ref key, find text <it> list prints (no ref), wait text succeeds, and click text=<it> refuses with the selector-not-found reason. The rule to pin: every web tree that passes through buildSnapshotState publishes only backend-issued refs.

Not blocking, take or leave: the PR body and #3509 say the ref-less limitation is recorded in docs/agents/web-backend.md, but that file is not touched, so add the paragraph or point the body at website/docs/docs/commands.md; the keyboard-occlusion.ts import rewrite to import { type …, type … } keeps a runtime side-effect import under verbatimModuleSyntax, and the unused signal on runJson in agent-browser-provider.ts has no caller, so both hunks can be reverted; nothing reads WebSnapshotOptions.raw now that buildSnapshotArgs ignores it, so delete it or document that it no longer changes capture; and ref-less text is published on value, so on web a bare value="X" also matches page text that repeats a control's value, which makes is and get attrs fail with AMBIGUOUS_MATCH where they resolved before, so note in commands.md to use text= for page text or publish it on a field value= does not read.

Of the open inline threads, the label denylist thread and the quoted-text escape thread are fixed at this head, as are the attachRefs doc and the target-evidence thread, so please resolve those four. The trailing value unescape thread does not apply, because the raw wire form is pinned by the tests 'matches a value across the two spellings' and 'preserves a backslash in a value the backend printed raw', so please resolve it too.

I did not re-run the live before/after table and took it from the PR body. I did not run the mutation that removes snapshotRefMinting(data), so the claim that no test fails rests on searching the tests. I did not trace how replay treats a recorded is or get step on a ref-less web node, where the record-time check returns 'unverifiable'. I did not measure how much larger the default web snapshot gets without --compact.

CI is still running: Smoke Tests is queued, and Repo Guards, Compatibility & Provenance and Integration Tests are in progress, with no failure logs yet. A failure in Integration Tests would likely relate to this change, since it runs the changed web-provider.test.ts and web-world.ts. Repo Guards could also relate through the new kernel exports and the import rewrite. There are no conflicts. The next step before merge is the router test above.

@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from a1d1eb8 to 6a883af Compare October 12, 2026 20:18
@thymikee
thymikee changed the base branch from main to feat/snapshot-optional-refs October 12, 2026 20:18
@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from 6a883af to 4adcae0 Compare October 12, 2026 20:48
@thymikee

Copy link
Copy Markdown
Member Author

Split for the size budget

The coordinator flagged this branch at 1,031 gross lines (git diff --stat origin/main...HEAD), over the repo's 1,000-line budget in docs/agents/pull-requests.md. It is now two branches, and the contract half moved out:

  • feat(snapshot)!: make a node's ref optional and mint it per producer #3547 feat(snapshot): make a node's ref optional and mint it per producer — the kernel contract (ref?, SnapshotRefMinting, attachRefs's minting mode, hasSnapshotRef, snapshotRefBodies, snapshotRefMinting) plus every surface that offers a node as an address: overlay labels, the printed snapshot line, settle tails, MCP ref pins, the touch-point competitor disclosure, keyboard occlusion, find/get reads, and the record-/replay-time target classifiers. 388 gross lines.
  • this PR — the web capture that produces such a node, based on that branch: 656 gross lines against its base.

The split is at the seam, not through a hunk: the presentation work cannot stand apart from making ref optional, because that is what stops those sites typechecking, so it travels with the contract. This branch keeps everything that closes #3509 (dropping --compact, the ref-less projection, the structural dedup rule, the escaping fix) and its tests.

Content is unchanged by the split. Against the same merge-base, the old head a1d1eb862 and the two branches together touch an identical 29-file set with identical per-file insert/delete counts on 27 of them (git diff --numstat of each side, sorted and diffed); the two exceptions are the JSDoc edits made in response to this round's review. The earlier heads (0ab9b9ddd, a1d1eb862) are no longer ancestors of this branch — the split rewrote history — but GitHub still serves them (gh api repos/callstack/agent-device/commits/a1d1eb862 answers), because the review threads reference them, so every link in those threads still resolves.

Review round 2

Round 1's five threads are replied to and resolved, and the two fixes are on this branch: suppression of a ref-less text line is a structural relation read off the tree instead of a page-global denylist, and extractQuotedText parses and unescapes an escaped quoted payload. The attachRefs JSDoc thread and the selector-side ref-filter duplication thread now live on #3547, where those files are. The second round's extractValue report is answered with live-backend evidence rather than changed: the trailing : value form is raw on the wire, so unescaping it would publish a value the page does not hold; the dedup it worried about already happens. Both spellings are pinned by regression tests.

@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from 4adcae0 to ddf73fd Compare October 12, 2026 22:33
@thymikee

Copy link
Copy Markdown
Member Author

The earlier findings at a1d1eb8 are mostly fixed at ddf73fd, and one test gap remains. The fixes cover the page-global text denylist (now the structural isTextAlreadyReported), escaped quotes in extractQuotedText, the attachRefs doc comment, and the snapshotRefBodies duplicate. All 16 checks pass at ddf73fd.

The ref-less path is still not pinned by a test, as in the earlier review. Only the snapshotRefMinting(data) argument to attachRefs at https://github.com/callstack/agent-device/blob/ddf73fd/packages/capture-kit/src/snapshot-state.ts#L83 stops a web capture from getting dense refs. Every agent-browser fixture in selector-runtime.test.ts, wait-runtime.test.ts, snapshot-state.test.ts and web-provider.test.ts still gives each node a ref. The kernel tests call snapshotRefMinting directly, and agent-browser-snapshot-refless.test.ts stops at normalizeAgentBrowserSnapshot. By reading the fixtures, deleting that argument would fail no test. This PR is the first producer of ref-less web nodes, so the ref-less text it emits would get minted eN refs that can collide with backend-issued refs (#2283). Please add one ref-less node (no ref, text on value, no rect) to the web provider scenario fixture. Then assert through the router that snapshot --json shows it with no ref key, find text <it> list prints (no ref), wait text <it> succeeds, and click text=<it> refuses with the selector-not-found reason. The rule to pin is that every tree with agent-browser provenance that passes through buildSnapshotState publishes only backend-issued refs. The test can land on #3547, which owns the call, or here, but the stack needs it before merge.

The Cubic threads on the page-global denylist (#3515 (comment)), escaped quotes (#3515 (comment)), the attachRefs doc (#3515 (comment)) and the snapshotRefBodies duplicate (#3515 (comment)) no longer apply at ddf73fd, so please resolve them. The trailing : value thread (#3515 (comment)) is benign: both spellings are pinned by tests, so please resolve it too.

I reviewed ddf73fd by reading the code and the test fixtures. I did not re-run the live before/after table on agent-browser 0.27.1, so that table is taken from the PR body. I did not run the mutation that deletes snapshotRefMinting(data). I confirmed that the platform-web files are identical between a1d1eb8 and ddf73fd.

@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from ddf73fd to ae84b04 Compare October 13, 2026 09:52
@thymikee

thymikee commented Oct 13, 2026 •

Copy link
Copy Markdown
Member Author

Review round 3: the ref-less path is pinned through the router

Head ae84b0429 (stack restacked: #3547 → this → #3533).

Required: the rule now has a test

test/integration/provider-scenarios/web-world.ts:225-240 — the emulated page ends with the line a real agent-browser capture prints for content it cannot hand an action to: text on value, no ref, no box. web-desktop.test.ts:81-120 then follows it through the request router, and :222 the click:

step assertion
snapshot --json (unscoped) the node is in data.nodes and 'ref' in node === false
find text "Order saved" list one match, no ref key, and the CLI line for that response body is exactly = (no ref) [text] "Order saved"
wait text "Order saved" 100 answers text: 'Order saved'
click text="Order saved" refuses with details.reason === 'selector_not_found'

Two notes on how it had to be written.

The print assertion goes through the formatter over the router's own response body. The provider-scenario harness is a daemon request router — there is no stdout to capture — so the step asserts the response's matches[0] has no ref key and then renders that same body through interactionCliOutputFormatters.find, the function the CLI runs on it. That is the shape limrun-ios-snapshot-owner.test.ts:136 already uses for the same reason, and it kept the assertion honest: the run found a real defect, because the daemon was not printing (no ref) — see below.

The ref-less line is left out of the interactive capture, deliberately. The interactive arm of the scenario asks the provider interactiveOnly: true, which on web is snapshot --interactive — a backend answer that lists only the lines carrying a handle. Minting the fixture a ref to make it survive that flag would be the thing this PR forbids, so the node is present in the unscoped capture (which is what all four assertions read) and absent from the -i one. The pre-existing labels assertion for snapshot -i is therefore unchanged at six nodes.

Mutation-checked as the review asked: delete the snapshotRefMinting(data) argument at snapshot-state.ts:83 and the scenario fails on the first step, with the exact collision named — the published node becomes {"index":6,…,"ref":"e7"}. On this branch that is 'ref' in refLess === true; on a real page, where the backend's own numbering is non-dense, that minted ref names a different element (#2283). #3547 carries the matching kernel-level case per side of the mode, so the argument is pinned both where it is decided and where the tree is published.

Found by the new test, fixed on #3547

The daemon's own projection was still adding the property back: toDaemonFindData rebuilt the response with ref: match.ref, so a ref-less match kept an own-property named ref whose value was undefined — invisible in --json (JSON.stringify drops an undefined value), but the response data object is what the CLI formatter, the MCP pin merge and a Node consumer read, and the scenario's 'ref' in matches[0] === false assertion is an in-memory check that was red before the fix. Fixed in src/daemon/selector-recording.ts on #3547 (that is this contract's PR), with a unit test per arm; the scenario's 'ref' in assertions pass because of it.

Non-blocking, all five taken

  • The doc paragraph is there — website/docs/docs/commands.md:516, which is what the body's "Documented limitation" section names. docs/agents/web-backend.md is deliberately not touched: it is the setup-and-diagnostics document for the managed install (spawn hardening, state dir, doctor output) and has no ref-namespace section to extend; the user-facing question lives in commands.md next to the iOS text-pair note. The actionable half of your point was the missing consequence, and that is now added — see the value= bullet below.
  • keyboard-occlusion.ts is back to import type { … }. Checked rather than assumed: the file reads no value from @agent-device/kernel/snapshot (grep -c 'normalizeRef\|formatRole\|snapshotRefBodies\|hasSnapshotRef\|attachRefs' → 0), so the runtime side-effect import was dead.
  • The unused signal is gone. runJson at agent-browser-provider.ts:48 already accepted one on main; only the two helper parameter lists grew a pass-through no caller used, and those two hunks are reverted. (No behavior was riding on them: nothing passed a signal into either helper.)
  • WebSnapshotOptions.raw deleted rather than documented. buildSnapshotArgs was its only reader and nothing sets it — src/core/interactors/web.ts forwards the daemon's capture options and never carries raw — and a field that looks like it changes capture depth while doing nothing is worse than an absent one. tsc -b is clean without it and packages/platform-web is 97/97.
  • The value= interaction is now a sentence in commands.md:516: the text is published on value, so value="X" resolves against page text as well as a field holding that text, and a predicate that needs one element answers AMBIGUOUS_MATCH; text= (or editable=true with value=) is the way out. packages/selectors/src/internal/match.ts:28 is the arm that makes it so.

On what you said you did not check

  • Replay over a recorded is or get attrs step on a ref-less web node. Traced, and it is divergence-free: resolveSelectorTargetMatches refuses to name a winner for a ref-less match, and resolveRefTargetMatches does the same for the recorded-ref case. What a ref-less web node cannot reach is a mutating recorded step in the first place — every acting policy row declares requireRect: true (packages/selectors/src/internal/resolution-policy.ts:55,85) and the capture never measures a rect for a line with no handle, so no such step can record. The rows that can carry a ref-less winner are the verify-without-acting ones — is <predicate> and get attrs route through readUnique, which declares requireRect: false (resolution-policy.ts:68-71) — so such a step can record on web, and on replay it names no winner and reports the divergence rather than acting. No test asserts that path; the honest state is traced-but-unpinned, and a provider-scenario replay of an is step over the ref-less node is the test to add if you want it in this stack rather than after.
  • The snapshot growth without --compact. Measured here, not live: packages/platform-web/src/__tests__/agent-browser-snapshot-refless.test.ts pins what the projection keeps and drops over a real capture shape (including that every ref-less node arrives without a rect), but the "how much bigger is the default web tree on a real page" number is still the PR body's live table and nothing more. The structural rules (PRESENTATIONAL_REFLESS_ROLES, the empty-container drop, isTextAlreadyReported) are what bound it; a live byte count on the 408-ref page is worth having and is not in this stack.

CI result for this commit is in the body. All five inline threads from the earlier rounds are resolved.

@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from ae84b04 to 67f9bfe Compare October 13, 2026 10:04
@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from 67f9bfe to 724d305 Compare October 13, 2026 10:44
@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready. The earlier findings from ddf73fd are fixed at 724d305: the structural isTextAlreadyReported check replaces the page-global text denylist, escaped quotes are handled and pinned by tests, the attachRefs doc now describes the dense and backend-issued modes, and there is one snapshotRefBodies in packages/kernel/src/snapshot.ts. Nothing remains from that round.

Not blocking, and you can take or leave it: the click text="Order saved" step in web-desktop.test.ts is refused by the acting row's requireRect, and the ref-less node has no rect whether or not a ref is minted, so that step would stay green if the snapshotRefMinting argument were deleted. Only the snapshot and find steps pin the minting rule, so a small comment rewording would say what the step really pins.

Of the open inline threads, the four cubic threads on the text denylist, escaped quotes, the attachRefs doc and the duplicate snapshotRefBodies are fixed at this head, so please resolve them: #3515 (comment), #3515 (comment), #3515 (comment), #3515 (comment). The cubic thread on the trailing : value parsing is benign, because both spellings are pinned by tests in agent-browser-snapshot-refless.test.ts, so please resolve it too: #3515 (comment). No other threads still apply.

I did not run the mutation that deletes snapshotRefMinting(data); the claim that the scenario goes red is reasoned from the attachRefs default and the interactor's producer stamp. I also did not re-run the live agent-browser 0.27.1 before/after table, which is taken from the PR body. CI for 724d305 is still queued or running, with 11 jobs not yet finished and none failed. Integration Tests, Coverage, Lint & Format and Prose all cover files changed in this round, so they need to finish green. Base #3547 must also merge first.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 13, 2026
The web capture asked agent-browser for `--compact`, and that flag removes a ref-less
element TOGETHER WITH the text under it: on one page the role census goes ListMarker
200 -> 0 and status 1 -> 0, so a `status` region that just announced something never
reaches the projection at all. Measured on the page this fix was developed against:
`wait text "Hello Zed"` times out on text the user can read off the screen, and
`find text`/`get text` cannot answer for it either. The flag's pruning is owned here
instead, by a projection that can tell an empty container from the text inside it.

What survives the projection is now decided per line. Text the backend issued no ref
for becomes a node carrying that text on `value`, in the ref-less shape the layer above
already models (see `SnapshotRefMinting`). A string rides on `value` rather than `label`
because the backend spells a control's visible string twice — as the control's name and
as its own text line — and a second `label` claiming it would refuse every `label="Name"`
read as ambiguous.

Suppression of a text line is a structural relation read off the tree, not a page-global
denylist: a line is a duplicate when it renders inside a `<label>` element whose control
reports the string as its name, or sits inside the control that reports it as its name or
value. Anything else keeps its own node, which is what preserves the content this capture
exists to publish — an announcement that happens to repeat a button's label, or a table
cell echoing a field's contents, is content. `extractQuotedText` parses and unescapes an
escaped quoted payload; the trailing `: value` form stays raw, because that is what the
backend prints and unescaping it would publish a value the page does not hold.

Closes #3509
The one thing standing between a web capture and dense numbering was the minting-mode argument at snapshot-state.ts:83, and no test drove it through the router: every agent-browser fixture gave every node a ref, so deleting the argument minted e7 onto ref-less page text with the whole suite green (#2283 is the collision that pays for it). The scenario page now has a ref-less line as the backend prints one - text on value, no ref, no box - and four router assertions follow it: snapshot --json publishes it with no ref key, find text <it> list prints it as (no ref), wait text answers it, and click text=<it> refuses with selector_not_found.
Ref-less on-screen text is published on value, so value="X" resolves against page text that repeats a control's value and a predicate needing one element answers AMBIGUOUS_MATCH. commands.md names text= and editable=true as the way out. Also the take-it-or-leave-it round: the paragraph the body claimed for docs/agents/web-backend.md was already in commands.md, so the body points there; keyboard-occlusion's import goes back to type-only (nothing here reads a value from kernel/snapshot); WebSnapshotOptions.raw goes, since buildSnapshotArgs no longer reads it and no caller sets it; and the two runJson signature hunks that added a signal nothing passes are reverted.
@thymikee
thymikee force-pushed the fix/web-snapshot-refless-text branch from 724d305 to 20b6693 Compare October 13, 2026 12:49
@thymikee

Copy link
Copy Markdown
Member Author

Took your non-blocking item at 20b6693ae (new head — the stack moved twice today because main kept merging; files stayed disjoint). The comment above the click text="Order saved" step now says what the step really pins: the acting row's requireRect, which refuses whether or not a ref was minted, and that the minting rule itself is pinned by the snapshot --json and find text … list assertions above it. You were right that the old wording implied the click step pinned minting; deleting the snapshotRefMinting(data) argument leaves it green.

On your verification note, to keep the record honest: I just re-ran the mutation on this exact head. Deleting that argument at snapshot-state.ts:83 makes the ref-less fixture node come back "ref":"e7", and the scenario fails at the snapshot --json "no ref key" assertion (AssertionError: {"index":6,…,"value":"Order saved",…,"ref":"e7",…}). The find text … list step would print = @e7 for the same reason, though the run stops before reaching it. The click step stays green for exactly the reason you gave, which is why the reworded comment now points at the two steps that actually bite.

All five threads you named were already resolved (the replies landed when the fixes landed); nothing open on the PR. Comment-only delta against 724d305, no behavior change; pnpm check:affected --run --base feat/snapshot-optional-refs re-run green on this head, and CI is running. Base #3547 still merges first.

@thymikee

Copy link
Copy Markdown
Member Author

The code in 20b6693 looks good to me. The earlier review of 724d305 found no problems, and the logical patch is the same apart from one test comment.

The dedup fix for ref-less lines (context from the nearest ref-carrying ancestor) and the escaped-quote handling in extractQuotedText are now in. The two cubic-dev-ai threads on the kernel JSDoc and the snapshotRefBodies use in selectors (#3515 (comment) and #3515 (comment)) cover code that now lives in base #3547. The two threads on dedup context (#3515 (comment)) and on quoted text (#3515 (comment)) are fixed at this head. The extractValue thread (#3515 (comment)) does not apply. The trailing : value form is printed raw, so unescaping it would corrupt values. All four fixed threads and the extractValue one can be resolved.

I did not run the tests or a live web session on this commit. The regression and live evidence rest on the earlier review of 724d305. I also did not re-check the live agent-browser 0.27.1 capture behind the extractValue thread.

Smoke Tests is queued, not failed. It runs device smoke lanes, and this PR has no route overlap with them: the production diff is limited to web snapshot parsing, the web provider args, and a type-only import in keyboard-occlusion.ts. The provider-scenario integration test covers the web behavior. There are no conflicts. Merge waits on base PR #3547 landing and on the queued Smoke Tests job finishing green.

@thymikee
thymikee added this pull request to stack #3582 October 13, 2026 14:24
@thymikee
thymikee merged commit dcf650b into main Oct 13, 2026
20 of 22 checks passed
@thymikee
thymikee deleted the fix/web-snapshot-refless-text branch October 13, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web: on-screen text reaches no reader — the capture asks for --compact and the projection drops ref-less lines

1 participant