feat(desktop,hub): agent pointing — ref-chips + ui_highlight (D6) - #490
Conversation
The missing half of the symmetry (ADR-062 D-5): the user has been able to point at the agent since D2; now the agent can point back. Two primitives, both NON-ACTUATING — nothing here focuses, scrolls, clicks or types, and the user's click stays the only actuator. The wedge needed one thing the plan left implicit: a written form for a UIRef an AGENT CAN TYPE. The ADR defines the JSON shape, but a reply is prose, so the wire form is the URI spelling of the same fields — ui://replay?dataset_id=ds_1&cursor=1234 — on the scheme already minted for ui://focus. One grammar (state/uiRef.ts), two spellings: ui_highlight accepts either, so a transcript chip and a tool call point at the same thing by construction. - REF-CHIPS. linkifyUiRefs rewrites bare tokens into markdown links so the transcript's existing link renderer paints them as chips. A string pre-pass rather than a markdown plugin, because it must be provable without a renderer — including the one thing it must never do, which is touch code: an agent explaining a URI inside a fence is showing it, not pointing with it. Fenced blocks and inline spans pass through. - FOCUS DISPATCH, honest about its depth. Switching surface always works; entity focus works where a store already exposes a setter (Replay's dataset/episode, an Inspect tab already open on that path) and stops at the surface where none does. A ref this build cannot focus still renders — a reference is worth reading — just not as a control. It runs ONLY from a click handler; there is deliberately no path from an agent message to it. - ui_highlight RAISES NO CARD, and the hub had to be told. D-5 puts its consent in the sharing toggle plus the policy bit, so the hub class list gained a third class (`annotate`) between read and action: routed immediately, but named distinctly so nobody later reads "routes without a card" as "is a read". Its safety lives where the plan's risk section put it — the `highlight` column, a per-agent rate limit (6/minute), a TTL that is ours not the caller's, always-present attribution, and an audit entry per call. - The overlay sits BELOW the modal tier. An agent must not be able to cover the Attention dock or a consent dialog with its own marker. Refusals do not consume rate budget: the limit exists to stop a loop, not to punish an agent that mistyped a ref. Tests: 9 electron (policy bit, attribution, TTL clamp, note clip, rate window, refusal ordering) + 6 frontend (grammar round-trip, both spellings, the code-fence rule) + 2 Go (highlight routes cardless; the class lists are exhaustive and disjoint). 308 electron pass, 386 frontend pass, go test ./... green, 12 lints clean. Everything needing a screen is unexercised here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92dabaa to
b9802e3
Compare
6158024 to
a84a948
Compare
|
Review of D6 (verified against the rebased head BLOCKER — ref-chips never fire on agent replies
case 'text': {
const body = str(p, 'text') ?? '';
if (!isFoldable(body)) return <Markdown text={body} />; // no uiRefs
return (<ClampText><Markdown text={body} /></ClampText>); // no uiRefs
}
MAJOR —
|
…ger crashes the transcript Review fixes for #490 (D6): - BLOCKER: uiRefs now rides both Markdown instances of the 'text' feed kind — agent prose is the primary carrier of ref-chips, and it was the one kind that never got them (only the user's input echo and the unknown-kind fallback did). - MAJOR: parseUiRefUri guarded decodeURIComponent (safeDecode) — a lone % or %zz in agent prose ('100% done') threw during Markdown render and crashed the transcript surface on every re-mount (persistent, agent-controlled). A pair that fails to decode is junk; the ref survives. Regression tests pin both linkify and the direct link path. - MAJOR (descope recorded): the corner marker vs §3.4b's positional glow is now stated as a deliberate v1 descope in the plan amendment, with the positional half owed to a follow-up wedge — plus the local-loop coarse-attribution caveat. - 'Never covers the Attention dock' is now structurally true: the dock region stacks one step above the fixed highlight layer (still below menus/modals), so a marker card can't paint over or click-intercept the dock's bottom strip where desktop_action cards render. - Audit ring: ui_highlight's note is clipped and its ref stringified+capped in redactBridgeArgs — no raw agent-authored structures retained for 50 entries. - Hardening/nits: renderer re-clamps ttl/note at the IPC boundary (asHighlightOrder); sharing-off clears live markers (clear() is no longer dead); '{agent}' substitution uses a function replacement so $& in a handle renders literally; dead 'via' dropped from UiHighlightRequest (the ring records the leg); a chip click that stops at the surface tier reports it in a toast (en+zh). - Tests: uiRefFocus.test.ts (the plan §5 'ref-chip parse + dispatch' gap; stores proven pure — .ts-extension imports fixed in uiRefFocus/inspect), parser-crash regressions. EventCard's prop pass-through and the overlay render remain owed to the Playwright pass, as the plan records. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Second review pass (agent): blocker + crash confirmed live and fixed on the branch — commit BLOCKER (fixed): MAJOR — parser crash (fixed): MAJOR — positional glow: recorded in the plan's D6-as-shipped amendment as a deliberate v1 descope — the shipped marker names the surface + attribution + user-actuated "Go there"; positional payloads parse but don't place; the glow-over-region/AX-element half is owed to a follow-up wedge. Also recorded: the local-loop coarse-attribution caveat (static mcp.json sends no Minors, all fixed:
Verified: electron 322/322, frontend state/ssh 423/423, tsc clean both sides, go suite green, lint-docs/arb/hardcoded-strings clean. 🤖 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>
D6 of
docs/plans/desktop-ui-context-and-pointing.md(§3.4b) — the last wedge; the plan is code-complete. Stacked on #489 (D5) → #488 (D4) → #487 (D3).The missing half of the symmetry (ADR-062 D-5): the user has been able to point at the agent since D2; now the agent can point back. Both primitives are non-actuating — nothing focuses, scrolls, clicks or types, and the user's click stays the only actuator.
The thing the plan left implicit
A UIRef an agent can type. The ADR defines the JSON shape, but a reply is prose — so the wire form is the URI spelling of the same fields,
ui://replay?dataset_id=ds_1&cursor=1234, on the scheme already minted forui://focus. One grammar (state/uiRef.ts), two spellings:ui_highlightaccepts either, so a transcript chip and a tool call point at the same thing by construction.Ref-chips
linkifyUiRefsrewrites bare tokens into markdown links so the transcript's existing link renderer paints them. A string pre-pass rather than a markdown plugin, so it is provable without a renderer — including the one thing it must never do: touch code. An agent explaining a URI inside a fence is showing it, not pointing with it; fenced blocks and inline spans pass through untouched.Focus dispatch is honest about its depth. Switching surface always works; entity focus works where a store already exposes a setter (Replay's dataset/episode, an Inspect tab already open on that path) and stops at the surface where none does. A ref this build cannot focus still renders — a reference is worth reading — just not as a control. It runs only from a click handler; there is deliberately no path from an agent message to it.
ui_highlightraises no card — and the hub had to be toldD-5 puts its consent in the sharing toggle plus the policy bit, so the hub class list gained a third class,
annotate, between read and action: routed immediately, but named distinctly so nobody later reads "routes without a card" as "is a read".Its safety lives where the plan's risk section put it:
highlightcolumn ofui_policy.tsThe overlay sits below the modal tier: an agent must not be able to cover the Attention dock or a consent dialog with its own marker.
Refusals deliberately do not consume rate budget — the limit exists to stop a loop, not to punish an agent that mistyped a ref.
Verification
node --testpass (D6 adds 9), 386 frontend state/ssh pass (D6 adds 6),go test ./...green (D6 adds 2)tsc --noEmitclean both sides · all 12 repo lints clean ·lint-desktop-tokens.shat baseline 65 with the new.agent-highlight*/.md-uirefrules present (no phantom classes)Stack complete: D3 (#487) → D4 (#488) → D5 (#489) → D6.
🤖 Generated with Claude Code