feat(desktop): element-resolved pointing — the crop names an element (D4) - #488
Conversation
28dc9d6 to
665f20d
Compare
…(D4)
D2 gave the agent pixels of the region the user dragged over. When that
region is a <webview> guest the bridge can do better: the same
accessibility machinery browser_snapshot uses already names the elements
under those pixels, so the message carries a STRUCTURAL pointer beside
the image — { tab_id, ref: "@e42", role: "button", name: "Deploy" }.
"Fix this button" stops being ambiguous.
The chain: hit-test the rect centre (elementFromPoint, then closest() to
the nearest interactive ancestor — the user drags over a button's LABEL
and means the button) → requestNode/describeNode for the backendNodeId →
the tab's AX tree compacted by the SAME compactAxTree browser_snapshot
uses, which mints the @en → getPartialAXTree for role and name.
Four decisions:
- The ref is LIVE. The compaction registers its refs against the tab
(setSnapshotRefs), so the @e42 in the message is one browser_click
resolves — otherwise "actionable" would be decorative.
- `actionable` is a field, not an assumption. Only bridge:'full'
partitions are action-drivable; on kimiweb/rerunweb the note says the
ref is for reference, because promising a click there is a lie the
agent discovers only on refusal.
- The pointer rides IN the note. postAgentInput carries a body and
images; a structured field beside them would be dropped on the floor.
So the line lands in the user's draft, above the send button, where
they can see and edit what the agent will be told — and a chip in the
target row shows it before that.
- Every failure degrades to no pointer. The crop is the deliverable. No
debugger, no AX tree, a canvas app with nothing to name: the capture
returns unchanged, and role falls back to the DOM tag name rather than
inventing one.
Accessible names are flattened and clipped to 80 chars — the pointer is
a reference, not a content channel (ADR-062 D-2).
14 new tests (10 electron driving the whole CDP sequence against a fake
sender, 4 frontend over the two renderings): 293 electron pass, 380
frontend state/ssh pass. The live hit-test needs the desktop pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7fd13a0 to
d89b004
Compare
|
Review of D4 (verified against head Major M1 — the pointer never reaches the agent on the kimi-web path (the plan's first target)
Worse, the pre-send target-row chip and its tooltip ("the element the agent will be pointed at") render on both paths, so the UI promises what the kimi path doesn't deliver. The D4-as-shipped note discusses only Minimal fixes, increasing effort: (a) document the limitation in the plan + suppress/relabel the chip when only the kimi target exists; (b) inject the pointer line into the kimi composer alongside the file (main-side, same user-gesture posture as the file injection). Major M2 —
|
…stead of clobbering Review fixes for #488 (D4): - M1: annotation_attach_kimi now delivers the composed note (user words + pointer line) into the kimi composer's text box beside the crop — DOM.focus + Input.insertText, same user-gesture posture as the file injection; failure is reported (note_injected:false → distinct toast + audit flag) instead of silently dropping what the chip promised. The overlay composes with the SAME appendPointerNote the companion path uses. - M2: registering the pointer's ref no longer replaces the tab's ref map (which renumbered agent-held refs so a stale @E5 could click a different element with no REF_STALE). registerAnnotationRef merges: reuse the ref the agent's last snapshot minted for that node, else mint a namespaced @an beside the existing entries; ref-less resolutions never touch the map. resolvePointer now returns the node to register instead of a map. - m3/m4: comments — the hit-test races the capture (pre-send chip is the mitigation); straddling rects hit-test the clipped rect's centre. - m5: the agent line frames the accessible name as page-derived ("the page labels …") — provenance rides with the label. - Nits: a/an article agreement, page-authored quotes escaped in the quoted span, the chip's tab fallback is localizable (en+zh key). - Tests: 3 kimi-note injection tests, 2 registerAnnotationRef merge/no-clobber regressions, quote-escape + article cases; existing pointer tests updated to the new resolution shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Second review pass (agent): both majors confirmed against code and fixed on the branch — commit M1 (fixed, option b): M2 (fixed, the stronger variant): ref registration never replaces the tab map. Minors: m3/m4 comments added (hit-test races the capture; clipped-rect centre); m5 — the agent line frames the name as page-derived ( Tests: 3 note-injection, 2 registerAnnotationRef regressions, quote/article cases; electron 307/307, frontend state/ssh green, tsc clean. The store-level 🤖 Generated with Claude Code |
…490) The stacked PRs squash-merged into their base branches (the GitHub stack cascade), so main received only D3 (#487); this merge brings the cascaded j8-d5-remote-desktop-ui head — element-resolved pointing (D4), hub-relayed desktop UI context (D5), and agent pointing (D6), each including its review-fix commit — onto main. Conflicts (main's D3-squash regions vs the branch's D5/D6-extended versions of the same code) resolved by taking the branch side, which is a strict superset; the plan's status block updated to all-shipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D4 of
docs/plans/desktop-ui-context-and-pointing.md(§3.4 step 5). Stacked on #487 (D3) — review the diff againstj8-d3-ui-screenshot.D2 gave the agent pixels of the region the user dragged over. When that region is a
<webview>guest, the bridge can do better: the same accessibility machinerybrowser_snapshotuses already names the elements under those pixels, so the message carries a structural pointer beside the image —{ tab_id, ref: "@e42", role: "button", name: "Deploy" }. "Fix this button" stops being ambiguous.The chain
elementFromPoint, thenclosest()to the nearest interactive ancestor (the user drags over a button's label and means the button)DOM.requestNode→DOM.describeNodefor thebackendNodeIdcompactAxTreebrowser_snapshotuses — which is what mints the@eNAccessibility.getPartialAXTreefor role + nameFour decisions
setSnapshotRefs), so the@e42in the message is onebrowser_clickresolves. Otherwise the plan's "actionable" claim would be decorative.actionableis a field, not an assumption. Onlybridge: 'full'partitions are action-drivable; on kimiweb/rerunweb the note says the ref is for reference, because promising a click there is a lie the agent discovers only on refusal.postAgentInputcarries a body and images — a structured field beside them would be dropped on the floor. So the line lands in the user's draft, above the send button, where they can see and edit what the agent will be told; a chip in the target row shows it before that.Accessible names are flattened and clipped to 80 chars — the pointer is a reference, not a content channel (ADR-062 D-2).
Files
src/state/uiPointer.ts(new)electron/src/uipointer.ts(new)annotation_host.tsAnnotationOverlay.tsx/state/annotation.tsVerification
npm test(electron): 293 pass — D4 adds 10 driving the whole CDP sequence against a fake sendernode --test src/state/*.test.ts src/ssh/*.test.ts: 380 pass — D4 adds 4tsc --noEmitclean both sides ·lint-desktop-tokens.shclean (baseline 65, new.annot-pointerrule added, no phantom class) ·lint-docs.shcleanStack: D3 (#487) → D4 → D5 → D6.
🤖 Generated with Claude Code