Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .beads/interactions.jsonl

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ attn daemon is stopped. Watch mode uses the local daemon socket and exits with
an actionable error if that daemon disconnects. Agents edit the original files
with their normal tools; attn's file watcher refreshes the open document.

Margin actions also support **Copy**, **Copy selected**, and **Copy all** with
file or project scope. These actions keep threads open. Native For-agent marks
Each comment card has a **Copy** icon next to its time badge, and a bar pinned
to the bottom of the rail copies **all** agent-assigned comments with file or
project scope. These actions keep threads open. Native For-agent marks
are visible to the local CLI; hosted-browser marks remain private to that
browser and can be copied there.

Expand Down
2 changes: 1 addition & 1 deletion planning/collab/agent-margin-feedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The agent checks the quote against the current file, makes changes under its exi
## Keep manual copying immediate

- **For agent** is a durable personal mark in attn's storage. The human remains the author; room visibility stays the same.
- **Copy**, **Copy selected**, and **Copy all** write directly to the clipboard with the same contextual records. Inline checkboxes and document/workspace scope determine the batch. No review, preview or confirmation step.
- **Copy** (per comment, next to the time badge) and **Copy all** (the bar pinned to the rail bottom) write directly to the clipboard with the same contextual records. Document/workspace scope determines the batch; there is no per-comment selection ("Copy selected" was dropped 2026-09-10). Success is shown by the icon turning into a check, never by text.
- Exclude resolved/unmarked threads. Show a selectable-text fallback only when clipboard access fails. Never silently truncate a batch.

## Implementation and boundaries
Expand Down
6 changes: 5 additions & 1 deletion planning/collab/amendments.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,14 @@ The product is reframed as **agentic collaboration** — primary use case is an

**20. The HTML document frame is untrusted input; page scripts keep running.** The frame may *propose* anchors and *report* geometry; it may never create, mutate, or resolve review state. Comment bodies and the submit action originate in shell-owned UI. This is required because the document is agent-authored or peer-supplied and its own scripts share a JS context with the injected runtime — a hostile page can tamper with the runtime and forge protocol messages, so the trust boundary must sit at the shell rather than inside the frame. Given that boundary, the document's own scripts are deliberately left enabled (a nonce CSP admitting only the runtime was rejected): self-contained AI-generated HTML routinely needs charting/animation libraries, and a review surface that renders every chart blank is not a review surface. The residual risk is bounded to a misleading anchor *preview*, which the user sees in the composer before committing. HTML anchors are correspondingly **unverified-by-authority** — Rust cannot check them without an HTML parser, and a bad anchor yields a misplaced highlight, not corrupted review state. attn-z64t applies the same decision separately to the native owner, hosted author, and hosted reviewer: all are share-first, all retain `allow-scripts` without `allow-same-origin`, and the hosted author gets no additional confirmation beyond their explicit share action. Pins: `html-annotation.md` §3, §4, §7.

### HTML annotation, continued (decision 21 — 2026-09-08)

**21. Annotate mode on an HTML document is off by default and entered through a shell-owned toggle.** Until 2026-09-08 every shell derived "this HTML document is under review" and immediately sent the frame `inspect: true`, so the injected runtime swallowed every click on a hovered element and painted hover chrome for the whole session — the artefact's own links, buttons, tabs and inputs stopped working the moment it was shared. Interactive artefacts (dashboards, prototypes, demos such as `planning/collab/agent-loop-roadmap.html`) are precisely what this surface exists to review, and a review surface that breaks the thing under review is not one. The decision (Angus, 2026-09-08, epic `attn-wrf3`): annotate mode is **off** for every HTML document, a shared one included; a small note button pinned to the bottom-right of the document viewport turns it on (hover outline, breadcrumb chip, click-to-comment exactly as before) and off again (interactivity restored); the mode persists until the person turns it off, so submitting or cancelling a note does not exit it; `⌘⇧N` toggles it and `Esc` leaves it beneath any composer or dialog. The toggle is **shell-owned chrome rendered outside the untrusted frame**, consistent with #19/#20 — the document can neither draw nor press it, so it cannot put itself into a mode that swallows its own clicks. The shell sends `inspect: enabled` only as `annotatable && mode`, the frame boots with the surface off, the bridge re-states the mode (explicitly, `false` included) after every handshake, and the runtime refuses to emit a `scopePicked` proposal by any route once the mode is off. What does not change: existing pins render in both modes (`renderAnchors` is independent of `inspect`), the rail / replies / resolve are untouched, the text-selection Comment pill stays available in both modes, and the script posture is unchanged (the runtime is injected whenever the document is annotatable, not when the toggle is on). Pins: `html-annotation.md` §Goal, §5 `inspect`; supersedes the "with no mode switch" clause of the 2026-08-04 note.

### Inconsistencies Fixed

- `data-model.md` line 202: `"attn file"` → `"attn file v2"` to match the v2-suffix convention used everywhere else in the key derivation tree (`crypto-spec.md` uses the v2 form).

---

**Total: 20 decisions, all previously-open questions closed.** Open implementation work is now bounded by the work itself, not by undecided design.
**Total: 21 decisions, all previously-open questions closed.** Open implementation work is now bounded by the work itself, not by undecided design.
43 changes: 34 additions & 9 deletions planning/collab/html-annotation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# HTML document annotation — design note

Status: **locked** (2026-08-04). Phase 0 of epic `attn-61t`.
Status: **locked** (2026-08-04; §Goal and §5 `inspect` amended 2026-09-08 by `attn-wrf3`). Phase 0 of epic `attn-61t`.
Prototype: [`prototypes/html-annotation.html`](prototypes/html-annotation.html) (validated UX — do not redesign).
Supersedes: the "Design B / distinct localhost origin" decision recorded on `attn-61t` 2026-06-18. See §1.

Expand All @@ -10,7 +10,14 @@ Supersedes: the "Design B / distinct localhost origin" decision recorded on `att

Bring attn's review surface — comments, replies, resolve, and (later) AI suggestions —
to **rendered HTML documents**, for both *text-range* and *whole-element* targets
(including the `cell ‹ row ‹ table` scope chain), with no mode switch.
(including the `cell ‹ row ‹ table` scope chain).

Element annotation is entered through one explicit switch — a shell-owned note toggle
pinned to the document viewport — and is **off by default**, a document under review
included (`attn-wrf3`, 2026-09-08). Text-range commenting via the selection pill needs no
switch and works in both modes. The original note read "with no mode switch"; that
coupled *under review* with *every click is a comment*, which broke the interactive
artefacts (dashboards, prototypes, demos) this surface most needs to review.

Read-only HTML *sharing* already ships (`attn-qgd`): `DocType::Html` snapshots publish
over the encrypted transport and reviewers render them. This note covers the missing
Expand Down Expand Up @@ -210,7 +217,7 @@ ignored by both sides so the protocol can extend without a lockstep upgrade.
| `setAnchorState` | `{ anchorId, state }` | `default` / `active` / `resolved` |
| `focusAnchor` | `{ anchorId, scrollIntoView }` | rail card → document |
| `dismissSelection` | `{}` | composer cancelled |
| `inspect` | `{ enabled }` | this document is under review; annotate it (see below) |
| `inspect` | `{ enabled }` | the person's annotate mode, gated by the shell (see below) |
| `theme` | `{ mode, tokens }` | PAPER/INK switch |

`explicit` separates *the person asked* (Comment pill, element click, breadcrumb entry)
Expand All @@ -219,17 +226,35 @@ from *the person is merely dragging a selection*. A shell must answer an explici
explicit proposal is the failure mode attn-yqun.3 exists to close.

`inspect` gates the whole element-annotation surface: hover outline, breadcrumb chip, and
click-to-comment all appear together and only once the shell says the document can
actually take a comment.
click-to-comment all appear together. It is **gated by the person's annotate toggle,
default off**: the shell sends `enabled: true` only when the document is annotatable
(shared, capability declared) **and** the person has turned the mode on, and sends
`false` the moment either half stops holding. The frame boots with the surface off, and
the bridge re-states the current mode after every handshake, so a reloaded or
republished frame lands in the mode the shell holds rather than whatever it last
remembered. (Amended 2026-09-08, `attn-wrf3`; previously "under review" alone turned it
on.)

They are one switch rather than two because the chip is opaque and painted *over* the
page. An always-visible chip occludes whatever sits above the hovered element and, being
clickable, swallows clicks there — so on a document that is merely being read it would
break the page's own links to offer an affordance that could answer nothing but "share
this first". Under review, that same interception is the point: clicking a link means
commenting on it, not following it. Before then the document behaves exactly like a
document, and the text-selection pill remains the way to ask — it answers, with the
reason.
this first". And a document under review is, until someone asks to annotate it, still a
document being read: a dashboard's tabs, a prototype's buttons and a demo's links have
to keep working, or the artefact cannot be reviewed *as* the thing it is. Once the
person asks, that same interception is the point: clicking a link means commenting on it,
not following it. The text-selection pill is independent of the switch and remains the
way to ask in either mode — it answers, with the reason.

The switch itself is **shell-owned chrome**: a small note button pinned to the
bottom-right of the document viewport (`HtmlAnnotateToggle.svelte`,
`data-slot="html-annotate-toggle"`), rendered *outside* the frame, so the document can
neither draw it nor press it (amendments.md #19/#20). It is shown only when the document
is annotatable; `⌘⇧N` (`Ctrl+Shift+N`) toggles it and `Esc` leaves the mode when no
composer or dialog sits above it. The mode stays on until the person turns it off —
submitting or cancelling a note does not exit it. Turning it off mid-hover takes the chip
and outline down; turning it off with a composer open keeps the composer (the person may
be typing) and the frame refuses to propose again until the mode returns.

Two invariants hold inside the frame. A click commits only to the element the chip is
currently naming, so nothing is ever anchored to something the person could not see was
Expand Down
142 changes: 136 additions & 6 deletions scripts/test-html-annotation-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
# 5. Assert the runtime booted inside the opaque-origin frame, that the
# comment margin mounts for an annotatable HTML doc, and that the
# capability survived the encrypted round-trip.
# 6. Annotate mode (attn-wrf3): on both sides, the page's own button keeps
# working under review; pressing the pinned note toggle turns the same
# click into a comment composer; pressing it again hands the click back.
#
# Why the assertions look indirect: the document renders in a cross-origin
# sandboxed iframe, so the automation bridge (which evaluates in the SHELL's
Expand All @@ -21,15 +24,23 @@
# capability on the received snapshot, the shell's own annotation wiring, and
# the mounted margin — rather than pretending to inspect the frame's DOM.
#
# THE IN-FRAME INTERACTION LAYER IS NOT TESTED HERE, and pretending otherwise is
# how the hover/click layer once shipped green while being unusable by hand.
# Hovering, the label chip, element clicks and the Comment pill are driven with
# a real mouse against a real opaque-origin frame in
# THE IN-FRAME INTERACTION LAYER IS MOSTLY NOT TESTED HERE, and pretending
# otherwise is how the hover/click layer once shipped green while being unusable
# by hand. Hovering, the label chip, element clicks and the Comment pill are
# driven with a real mouse against a real opaque-origin frame in
# web/e2e/html-annotation-runtime.spec.ts (`npm run test:e2e:html-annotation`).
# What this script uniquely proves is the part that needs two daemons and a
# relay: that the capability, the bytes and the runtime survive the encrypted
# round trip, and that both sides' shells wire an annotatable frame up.
#
# The one in-frame behaviour it does exercise is the annotate-mode switch, and
# it does so through the FIXTURE: tests/fixtures/interactive.html answers a
# postMessage from the shell by hovering and clicking its own button with the
# event sequence the runtime listens to, then reports its DOM state back. That
# is the only way to observe "the button's handler ran" from outside an opaque
# frame; the shell-side halves (the toggle's aria-pressed, the composer) are
# asserted directly.
#
# Honors ATTN_SKIP_HTML_ANNOTATION_E2E=1 as a CI escape hatch (relay + webview
# need a display + loopback, flaky on some headless infra).
#
Expand All @@ -50,8 +61,8 @@ cd "$PROJECT_DIR"

: "${ATTN_RELAY_URL:=http://localhost:8787}"
: "${ATTN_BIN:=$PROJECT_DIR/target/debug/attn}"
FIXTURE="$PROJECT_DIR/tests/fixtures/sample.html"
MARKER="Hello from an HTML file"
FIXTURE="$PROJECT_DIR/tests/fixtures/interactive.html"
MARKER="Interactive HTML fixture"
# The injected runtime carries this attribute; it is the only reliable
# shell-visible proof that injection happened.
RUNTIME_MARKER="data-attn-runtime"
Expand Down Expand Up @@ -267,6 +278,125 @@ __attn_dual_wait_one "$ATTN_DUAL_REVIEWER" '[data-slot="review-margin"]' 15000 \
&& pass "comment margin mounts for the annotatable HTML doc" \
|| pend "review margin not observed (attn-7ev: needs the rail expanded)"

# ---- annotate mode: off by default; the pinned toggle enters and leaves it ---
#
# Both shells are the same App.svelte, so the same walk runs on the owner
# (path-mode frame, runtime spliced by the attn:// handler) and on the reviewer
# (srcdoc frame, runtime spliced by the shell). The fixture reports its own
# state over postMessage — see the header comment for why.

TOGGLE='[data-slot="html-annotate-toggle"]'
COMPOSER='[data-slot="html-comment-composer"]'

# $1: home. Evaluate JS in that shell; print the JSON result (or 'err').
shell_eval() {
ATTN_HOME="$1" "$ATTN_BIN" --eval "$2" 2>/dev/null | jq -r . 2>/dev/null || echo 'err'
}

# Install the shell-side listener that captures the fixture's state reports.
install_fixture_listener() {
shell_eval "$1" "(() => { if (!window.__attn_fixture_listener) { window.__attn_fixture_listener = true; window.__attn_fixture_state = null; window.addEventListener('message', (e) => { if (e.data && e.data.type === 'attn-fixture:state') window.__attn_fixture_state = { pressed: e.data.pressed, hash: e.data.hash }; }); } return true; })()" >/dev/null
}

# $1: home, $2: CSS selector inside the fixture. Hover + click it, as the
# runtime would see a real pointer do, and wait for the state report.
fixture_press() {
shell_eval "$1" "(() => { window.__attn_fixture_state = null; document.querySelector('[data-slot=\"html-viewer\"] iframe')?.contentWindow?.postMessage({ type: 'attn-fixture:press', target: '$2' }, '*'); return true; })()" >/dev/null
local i=0
while [ $i -lt 30 ]; do
local got
got="$(shell_eval "$1" "window.__attn_fixture_state !== null")"
[ "$got" = "true" ] && return 0
sleep 0.1; i=$((i + 1))
done
return 1
}

fixture_pressed() { shell_eval "$1" "window.__attn_fixture_state?.pressed ?? -1"; }
toggle_pressed() { shell_eval "$1" "document.querySelector('$TOGGLE')?.getAttribute('aria-pressed') ?? 'missing'"; }
composer_open() { shell_eval "$1" "document.querySelector('$COMPOSER') !== null"; }

# $1: home, $2: label
assert_annotate_mode() {
local home="$1" who="$2"

__attn_dual_wait_one "$home" "$TOGGLE" 15000 \
&& pass "$who: pinned annotate toggle shown for the annotatable doc" \
|| { fail "$who: annotate toggle never rendered"; return; }
case "$(toggle_pressed "$home")" in
false) pass "$who: annotate mode is OFF by default" ;;
*) fail "$who: annotate mode not off by default (aria-pressed=$(toggle_pressed "$home"))" ;;
esac

install_fixture_listener "$home"

# (1) Off: the page's own handler runs and no composer appears.
if fixture_press "$home" '#action'; then
local pressed; pressed="$(fixture_pressed "$home")"
if [ "$pressed" = "1" ] && [ "$(composer_open "$home")" = "false" ]; then
pass "$who: (1) with the mode off, the page's button runs its handler and no composer opens"
else
fail "$who: (1) mode off — pressed=$pressed composer=$(composer_open "$home")"
fi
else
fail "$who: (1) fixture never reported after the press"
fi

# (2) On: the same click opens the composer and the handler does NOT run.
ATTN_HOME="$home" "$ATTN_BIN" --click "$TOGGLE" >/dev/null 2>&1 \
|| fail "$who: could not click the annotate toggle"
sleep 0.4
case "$(toggle_pressed "$home")" in
true) pass "$who: toggle reports pressed after the click" ;;
*) fail "$who: toggle did not flip on (aria-pressed=$(toggle_pressed "$home"))" ;;
esac
local mode; mode="$(shell_eval "$home" "window.__attn_html_debug__?.annotateMode === true")"
[ "$mode" = "true" ] \
&& pass "$who: shell mirrors annotate mode on" \
|| fail "$who: shell debug mirror says annotateMode=$mode"
if fixture_press "$home" '#action'; then
local pressed; pressed="$(fixture_pressed "$home")"
if __attn_dual_wait_one "$home" "$COMPOSER" 5000 && [ "$pressed" = "1" ]; then
pass "$who: (2) with the mode on, the same click opens the composer and the handler does not run"
else
fail "$who: (2) mode on — pressed=$pressed composer=$(composer_open "$home")"
fi
else
fail "$who: (2) fixture never reported after the press"
fi

# Cancel the composer (by its label, not its position), then leave the mode.
shell_eval "$home" "(() => { const b = [...document.querySelectorAll('$COMPOSER button')].find((el) => el.textContent.trim() === 'Cancel'); b?.click(); return Boolean(b); })()" >/dev/null
sleep 0.2
[ "$(composer_open "$home")" = "false" ] \
&& pass "$who: cancelling the composer closes it" \
|| fail "$who: composer still open after Cancel"
ATTN_HOME="$home" "$ATTN_BIN" --click "$TOGGLE" >/dev/null 2>&1 \
|| fail "$who: could not click the annotate toggle a second time"
sleep 0.4
case "$(toggle_pressed "$home")" in
false) pass "$who: toggle reports released after the second click" ;;
*) fail "$who: toggle did not flip off (aria-pressed=$(toggle_pressed "$home"))" ;;
esac

# (3) Off again: the handler runs and nothing proposes.
if fixture_press "$home" '#action'; then
local pressed; pressed="$(fixture_pressed "$home")"
if [ "$pressed" = "2" ] && [ "$(composer_open "$home")" = "false" ]; then
pass "$who: (3) with the mode off again, the page's button runs its handler"
else
fail "$who: (3) mode off again — pressed=$pressed composer=$(composer_open "$home")"
fi
else
fail "$who: (3) fixture never reported after the press"
fi
}

log "Annotate mode walk — owner"
assert_annotate_mode "$ATTN_DUAL_OWNER" "owner"
log "Annotate mode walk — reviewer"
assert_annotate_mode "$ATTN_DUAL_REVIEWER" "reviewer"

echo
if [ "$FAILURES" -eq 0 ]; then
echo "RESULT: html-annotation e2e passed"
Expand Down
Loading
Loading