Files
vscode-cowriting-plugin/docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md
Ben Stull b2de25ff99 design(f11): preview toolbar as the primary interaction surface
Solution Design for Feature #43 (F11): the F10 review preview's header
gains a Pin baseline button and a single adaptive Ask Claude button
(Edit Selection / Edit Document) beside the existing annotations
checkbox, plus a minimal right-click gateway that opens the preview.

Three forks locked in brainstorming session 0036:
- block-level preview-selection -> source mapping (data-src on blocks
  from BlockWithRange; union of intersected live-source blocks)
- document edit diffed into per-hunk F4 proposals (no model change)
- #43 lands a minimal editor/title -> Open Review Preview gateway
  (#41/#42 expand the menus)

Reuses F4 propose/F3 attribution/F6 baseline seams; sealed webview
posts intent only (INV-35/36/37). Resolves the orphaned pinDiffBaseline
reachability gap. Submitted to the content repo at finalize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:15:44 -07:00

603 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: graduated
---
# Solution Design: Preview Toolbar as the Primary Interaction Surface (F11)
| | |
| --- | --- |
| **Author(s)** | Ben Stull (with Claude) |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `draft` |
| **Version** | v0.1.0 |
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#43` (F11, `type/feature`, `priority/P1`) · Epic `#1` (closed) · Capture session `vscode-cowriting-plugin-0035` (2026-06-12) · Brainstorming session `vscode-cowriting-plugin-0036` · Builds on (all shipped): F3 `#6` (live attribution), F4 `#12` (propose/accept), F6 `#17`/`#19` (baseline + diff view), F7 `#21`/`#22` (rendered preview), F9 `#27` (authorship preview), F10 `#29` (interactive review preview), F10-followups `#31` (inline-at-anchor proposals) · Coexists with `#41` (right-click → Open Review Panel) and `#42` (right-click → Ask Claude to Edit), both blocked-by this · Parent specs (graduated): `coauthoring-inner-loop.md`, `coauthoring-attribution.md`, `coauthoring-propose-accept.md`, `coauthoring-diff-view.md`, `coauthoring-rendered-preview.md`, `coauthoring-interactive-review.md` · Lineage: `ben.stull/rfc-app#48` |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-12 | v0.1.0 | Initial draft — brainstorming session 0036 (from the capture in session 0035). Three forks locked with the operator: block-level preview-selection→source mapping; document edit diffed into per-hunk F4 proposals; #43 lands a minimal right-click→open-preview gateway. | Ben Stull + Claude |
---
## 1. Business Context
### 1.1 Executive Summary
F10 made the rendered preview the **single review surface** — clean editor on the
left, annotated review on the right, with an annotations on/off toggle and
inline ✓/✗ on Claude's pending proposals. But the writer still cannot *act* from
the preview beyond accepting/rejecting proposals: to **ask Claude to edit**, they
jump back to the editor and use a selection-gated context-menu item; to **pin a
fresh review baseline** they have *no* reachable control at all (the
`cowriting.pinDiffBaseline` command is registered but `when:false`, orphaned
since `#34` removed its two-pane host); and whole-document editing doesn't exist.
F11 makes the **preview toolbar the primary interaction surface**. Beside the
existing annotations checkbox — the one control the writer already loves — the
toolbar gains a **Pin baseline** button and a **single adaptive "Ask Claude…"
button** that reads *Edit Selection* when text is selected in the preview and
*Edit Document* when nothing is. The writer reads, asks Claude to edit, and
resets the baseline all in one place, mouse-first, without leaving the rendered
document. A minimal right-click entry opens the preview, making it the surface
the `#41`/`#42` gateways will lead into.
### 1.2 Background
The inner loop shipped F2F5 (threads · attribution · propose/accept ·
cross-rung). F6 added the baseline + a native diff toggle; F7 the rendered
track-changes preview; F9 an authorship mode; F10 (`#29`) collapsed those into the
**single interactive review preview** (clean editor; annotations on/off; ✓/✗ on
F4 proposals surfaced in the rendered view); `#31` then placed proposals
**inline at their resolved anchor** in that preview.
Capture session 0035 filed `#43` (this feature) plus `#41` (right-click → Open
Review Panel) and `#42` (right-click → Ask Claude to Edit). The operator's ask:
*"Can we set it up so all interactions — Ask Claude to Edit Selection / Edit
Document, annotations off/on, Pin new baseline — are via the preview window? I
like the annotations checkbox up there; make the others buttons, with one 'Ask
Claude…' button that changes depending on whether some of the markdown preview is
selected."* That session also surfaced the stranded `pinDiffBaseline` command
(`when:false` since `#34`). This spec is the Solution Design for `#43`.
### 1.3 Business Actors / Roles
- **Coauthor (human)** — the markdown writer/engineer (PP-1); F11's sole user.
- **Coauthor (machine)** — Claude via `@cline/sdk`; not a user of F11, but the
target of the toolbar's "Ask Claude…" gesture and the author of the proposals
that result.
### 1.4 Problem Statement
The plugin's interactions are scattered across surfaces and inconsistently
reachable. "Ask Claude to Edit Selection" is only a selection-gated **editor**
context-menu item; **whole-document editing doesn't exist**; and **Pin Review
Baseline** is **unreachable from any UI**. The one control the writer loves — the
annotations on/off checkbox in the preview — proves the toolbar is the natural
home for these gestures, but it stands alone. A writer reviewing in the preview
has to leave it and hunt through editor menus / the palette to act.
### 1.5 Pain Points
- **No edit gesture in the preview** — to ask Claude to change anything, the
writer leaves the review surface for the editor's right-click menu.
- **Whole-document editing is missing** — there is no "edit the whole document"
path at all; only a selection-scoped editor command exists.
- **Pin baseline is stranded** — the command exists but no menu, keybinding, or
palette entry reaches it (`when:false` since `#34`).
- **Mouse-first review is broken mid-flow** — the preview is mouse-driven, but
acting forces a context-switch to keyboard/menus elsewhere.
### 1.6 Targeted Business Outcomes
The preview becomes a **self-contained cockpit** for the inner loop. From its
toolbar a writer can toggle annotations (today), **ask Claude to edit** (selection
or whole document, via one button that adapts to what's selected), and **pin a
fresh baseline** — no context-switching to the editor or command palette. The
interaction model consolidates around the surface the writer already prefers, and
the stranded pin command gets a real home.
### 1.7 Scope (business)
**In scope:** preview-webview toolbar controls — a **Pin baseline** button and a
**single adaptive "Ask Claude…" button** (Edit Selection ⇆ Edit Document) — beside
the existing annotations checkbox; wiring those controls to the **existing** F4
edit seam, F3 attribution, and F6 baseline command; **block-level**
preview-selection → source-range mapping (the central design risk); a **new
whole-document edit path** whose result is **diffed into per-hunk F4 proposals**;
a **minimal right-click → Open Review Preview** entry so the surface is reachable
end-to-end; resolving the pin-baseline reachability gap; unit + host-E2E coverage;
manual webview smoke.
**Out of scope (deferred, not forgotten):** **char-precise sub-block** selection
mapping (block granularity is the locked v1 — §6.7); the **richer `#41`/`#42`
menu sets** (this feature lands only the minimal gateway; `#41`/`#42` expand it);
preview→source **scroll-sync** (`#32`); multi-file / batch editing; the Explorer
tree affordance; any export / print / copy gesture.
**Non-goals (firm):** **no new edit / attribution / proposal *model*** — F11
reuses F3 `spansFor`, the F4 `propose`/`accept` single-range model, and the F6
baseline store; no change to the **sidecar**, the **cross-rung contract**, or
`SCHEMA_VERSION`; **no document mutation from the webview** (INV-20/21/34 hold —
the sealed webview posts intent only); no LLM/network/credential surface added to
the webview (INV-8 untouched — the edit turn runs host-side as today).
### 1.8 Assumptions · Constraints · Dependencies
- **Anchor:** Feature `#43` (F11). Builds directly on shipped work: the F7/F10
rendered preview + annotations toggle + host↔webview message bus, the F3
attribution + F4 propose/accept inner loop (including `#31`'s inline-at-anchor
proposal placement), and the F6 baseline store (`cowriting.pinDiffBaseline`,
currently unreachable — this feature gives it a home).
- **Central design risk (locked):** the preview is a **rendered** sealed webview
(markdown-it HTML, strict CSP — F7 INV-21), so "Edit Selection" must map a
selection in the rendered preview back to a **source markdown range**. The
rendered HTML carries **no** source positions today; only internal block
char-offsets exist (`splitBlocksWithRanges``BlockWithRange.start/end`). The
locked approach is **block-level** mapping: the pure render layer emits
`data-src-start`/`data-src-end` on each rendered block; a selection resolves to
the union of the live-source blocks it intersects (§6.7, fork 1).
- **Constraint (sealed webview):** interactive controls post messages to the
extension host, which applies edits/pins via the existing F4 / F6 paths; the
webview **never** edits the document, sidecar, or baseline directly. The edit
**turn** (LLM call) and the **instruction prompt** run host-side, keeping the
webview free of LLM/network/credential surface.
- **Coexistence:** the native editor context-menu "Ask Claude to Edit Selection"
(`cowriting.editSelection`) stays unchanged; `#41`/`#42` add right-click
gateways *into* the preview. F11 lands only a minimal gateway (§6.7, fork 3).
- No new persisted artifact; nothing in `.threads/`, the contract, or
`SCHEMA_VERSION` changes.
### 1.9 Business Use Cases
- **BUC-1 (edit from the preview)** Reviewing in the preview, the writer selects a
paragraph in the rendered document and clicks **"Ask Claude to Edit
Selection"**; Claude's proposed change appears as a blue ✓/✗ block at that
spot — without the writer ever leaving the preview.
- **BUC-2 (edit the whole document)** With nothing selected, the writer clicks
**"Ask Claude to Edit Document"**, types an instruction, and Claude's rewrite
surfaces as **several** independently-acceptable blue proposal blocks (one per
changed hunk) inline in the preview.
- **BUC-3 (pin a fresh baseline)** After accepting a batch of changes, the writer
clicks **"Pin baseline"**; the change-marks clear and "what changed" now counts
from this moment — the stranded command finally has a button.
---
## 2. Solution Proposal
F11 is a **thin increment** on F10's preview: it adds two controls to the
existing header toolbar and routes their intent through machinery that already
exists. No new model, no new persisted state.
**The pure render layer (`trackChangesModel.ts`) learns one new thing:** a shared
helper wraps each rendered block in `<div data-src-start="N" data-src-end="M">`
using the existing `splitBlocksWithRanges` offsets. It is applied to **both**
render paths — `renderReview` (annotations on) and `renderPlain` (annotations
off) — so selection→source mapping works in either mode. The helper is pure,
vscode-free, DOM-free, and deterministic (extends INV-22/33). The render layer
gains **no** selection or DOM logic.
**The webview (`media/preview.ts` + `.css`)** header becomes:
```
[ ☑ Annotations ] [ ⌖ Pin baseline ] [ ✦ Ask Claude to Edit Document ▾ ]
```
The **Ask-Claude button morphs its own label** on `selectionchange`: a non-empty
selection inside the rendered body → **"Ask Claude to Edit Selection"**; an empty
/ collapsed selection → **"Ask Claude to Edit Document"**. On click it walks the
selection's start and end nodes up to the nearest ancestor carrying
`data-src-start`/`data-src-end` and posts the resolved offsets. That
nearest-ancestor lookup is the webview's **only** mapping duty (manual-smoke
territory); everything downstream is host-side and testable. The webview stays
**sealed** (INV-21): nonce'd inline script, no network, no document mutation.
**New webview→host messages (intent only):**
```ts
type ToolbarMsg =
| { type: "pinBaseline" }
| { type: "askClaude"; scope: "selection"; start: number; end: number }
| { type: "askClaude"; scope: "document" };
```
**The host (`trackChangesPreview.ts`)** routes each intent through the existing
seams:
- **`pinBaseline`** → pins the **previewed** document (calls
`DiffViewController.pin(document)` directly — not the `activeTextEditor`-based
command, which may not point at the previewed doc) → `onDidChangeBaseline`
re-render with cleared marks.
- **`askClaude`** → host `showInputBox` for the instruction (keeps the LLM /
secret surface out of the sealed webview), then one shared host routine
`runEditAndPropose(document, target, instruction)`:
- **selection** → `target` = the block-union range `[firstBlock.start …
lastBlock.end]`; one `runEditTurn` → one F4 `propose()` over that range (the
existing Edit-Selection shape).
- **document** → `target` = whole document; one `runEditTurn` over the full
text → **diff Claude's result against the current text → one `propose()` per
changed hunk** (multiple proposals, each its own blue ✓/✗ block). Reuses the
F4 single-range model N times; no model change.
**The right-click gateway:** `cowriting.showTrackChangesPreview` is added to the
`editor/title` menu (markdown only) so right-clicking the tab opens the preview —
the minimal entry that makes the toolbar surface reachable end-to-end and lets
`#41`/`#42` expand the menu set later.
**Reachability cleanup:** `cowriting.pinDiffBaseline` gets a real palette `when`
(`editorLangId == markdown`), resolving the orphan from the command side too; a
new `cowriting.editDocument` command is registered (document-scoped edit) so
`#42`'s gateway can reuse it.
Everything downstream of *(intent) → (existing seam)* is the existing F4/F6/F3
machinery; the only genuinely new pure code is the block-offset wrapper and the
document-rewrite hunk-diff. Both are unit-testable with no vscode and no webview.
---
## 3. Product Personas
- **PP-1 Inner-loop coauthor** — the human markdown writer/engineer (as F2F10);
the only persona F11 serves.
## 4. Product Use Cases
- **PUC-1 (toolbar present)** Opening the review preview for a markdown document
shows the header with **three** controls: the annotations on/off checkbox
(existing), a **Pin baseline** button, and an adaptive **Ask Claude…** button.
Controls are inert (disabled) for non-authorable documents.
- **PUC-2 (adaptive label)** With a non-empty selection in the rendered preview
body, the Ask-Claude button reads **"Ask Claude to Edit Selection"**; with no
selection it reads **"Ask Claude to Edit Document"**. The label flips live as
the selection changes.
- **PUC-3 (edit selection)** The writer selects rendered text, clicks **Ask
Claude to Edit Selection**, and enters an instruction. The selection resolves to
the union of the source blocks it touches; Claude proposes a change over that
range; a single blue ✓/✗ proposal block appears inline at that anchor (`#31`).
- **PUC-4 (edit document)** With nothing selected, the writer clicks **Ask Claude
to Edit Document**, enters an instruction; Claude rewrites the whole document;
the rewrite is diffed into hunks and surfaces as **N** independent blue ✓/✗
proposal blocks inline. Accepting/rejecting each is the F10 path unchanged.
- **PUC-5 (pin baseline)** The writer clicks **Pin baseline**; the previewed
document's review baseline is pinned to now; the change-marks clear and the
`Since <epoch>` label updates. (No confirmation prompt — matches the existing
command's behavior; re-pinning is the recovery.)
- **PUC-6 (right-click into the preview)** Right-clicking a markdown editor tab
shows **Open Review Preview**; choosing it opens the preview (the gateway
`#41`/`#42` will build upon).
- **PUC-7 (graceful edges)** A selection confined to a deletion (struck) or
proposal block — which carries no live-source range — falls back to **document**
scope. An empty document or a selection that resolves to no live block → the
button stays in **Edit Document** mode. A non-authorable document → toolbar edit
controls are disabled. The LLM turn failing → the existing `runEditTurn`
error handling (no proposal created); the preview is unchanged.
---
## 5. UX Layout
The F10 preview is unchanged except for its **header bar**, which now hosts three
controls in a single row:
- **☑ Annotations** — the existing on/off checkbox (kept first; the operator's
preferred control).
- **⌖ Pin baseline** — a button; pins the previewed document's review baseline to
now and clears the change-marks.
- **✦ Ask Claude to Edit Document ▾** — a single button whose label and behavior
adapt to the preview's selection state (Edit **Selection** when text is
selected, Edit **Document** otherwise). Clicking it opens a host input box for
the instruction.
Buttons are styled with theme CSS variables (light / dark / high-contrast),
matching the existing toolbar chrome; they sit in the same `#cw-toggle` header
region as the annotations checkbox. When the previewed document is not authorable
(F8 `isAuthorable`), the **Pin baseline** and **Ask Claude…** controls render
**disabled** (the annotations toggle stays active — reading is always allowed).
The rendered body is unchanged from F10/`#31`: green human additions, blue
LLM-authored text, struck deletions, and pending Claude proposals as inline blue
blocks with ✓/✗ at their resolved anchors. Proposals produced via the new toolbar
edit gestures appear exactly as proposals do today.
---
## 6. Technical Design
### 6.1 Invariants
Parent invariants INV-1..INV-34 carry over unchanged. F11 adds:
- **INV-35 (toolbar gestures route through existing seams; webview never
mutates)** The Pin baseline and Ask-Claude toolbar controls post **intent**
messages to the host; **all** mutation goes through the existing machinery — pin
via the F6 baseline store (`DiffViewController.pin`), edits via the F4
`propose` → `accept`/`applyAgentEdit` (`WorkspaceEdit`) seam with F3 attribution.
No divergent edit or baseline path is introduced. The sealed webview never
edits the document, sidecar, or baseline directly (INV-20/21/34 hold); the LLM
turn and the instruction prompt run host-side (INV-8 untouched).
- **INV-36 (block-granular preview-selection → source mapping)** The pure render
layer emits `data-src-start`/`data-src-end` (source char offsets from
`BlockWithRange`) on **every** rendered block, in **both** the on (`renderReview`)
and off (`renderPlain`) modes. A preview selection resolves to the **union of
the live-source blocks it intersects** (`[min start … max end]`); blocks with no
live-source range (deletion-only / proposal blocks) are skipped, and a selection
that resolves to no live block falls back to **document** scope. The DOM
selection → nearest-`data-src` lookup is the webview's **sole** mapping duty;
the offsets and everything downstream (fingerprint, turn, propose) are host-side
and testable. The wrapping is deterministic — same inputs → identical HTML
(extends INV-22/33).
- **INV-37 (single adaptive Ask-Claude button; scope-aware)** One toolbar button
serves both scopes. A non-empty live-source selection → **Edit Selection**: one
F4 proposal over the block-union range. An empty selection → **Edit Document**:
one `runEditTurn` over the whole document, its result **diffed into hunks**, one
F4 `propose()` per changed hunk. Both scopes call the same host
`runEditAndPropose` routine and reuse the F4 **single-range** proposal model
(the document case issues multiple single-range proposals — **no new model**).
### 6.2 High-level architecture
```mermaid
flowchart LR
wv["webview header\n☑ Annotations · ⌖ Pin · ✦ Ask Claude (adaptive)"] -- "postMessage{pinBaseline | askClaude(scope,start?,end?)}" --> ctl["trackChangesPreview\n(vscode layer)"]
ctl -- "pin(document)" --> base["F6 DiffViewController\nbaseline store (INV-18)"]
ctl -- "showInputBox → runEditAndPropose" --> turn["runEditTurn\n(host-side LLM turn)"]
turn -- "selection: 1 replacement\ndocument: rewrite" --> ctl
ctl -- "selection → 1 propose()\ndocument → diff → N propose()" --> prop["F4 ProposalController\npropose() (single-range model)"]
prop -- "onDidChangeProposals" --> ctl
base -- "onDidChangeBaseline" --> ctl
ctl -- "(baseline, current, spans, proposals)" --> model["renderReview / renderPlain\n(pure)\n+ wrapBlocksWithSrc (NEW)"]
model -- "annotated HTML w/ data-src on blocks" --> ctl
ctl -- "postMessage{render}" --> wv
```
The dashed-in NEW pieces are: `wrapBlocksWithSrc` (pure), the
`runEditAndPropose` host routine with its document-rewrite hunk-diff, the three
inbound toolbar messages, and the `editor/title` gateway menu. Everything else is
the existing F6/F4/F3/F10 machinery.
### 6.3 Data model & ownership
**No new persisted artifact** (INV-20). F11 adds only transient on-the-wire
messages (the `ToolbarMsg` union in §2) and reuses F10's `RenderMsg`. Baseline is
owned by F6, proposals by F4 (sidecar), attribution by F3 — all untouched. The
block-offset `data-src` attributes are render-time only (not stored).
### 6.4 Interfaces & contracts
- **`trackChangesModel`** (vscode-free, pure): new
`wrapBlocksWithSrc(blocks: BlockWithRange[], renderedPerBlock: string[]):
string` (illustrative) — or, more precisely, both `renderReview` and
`renderPlain` route their per-block rendered HTML through a shared internal
helper that prepends `data-src-start`/`data-src-end` to each block's wrapping
element. Plus `diffToHunks(currentText: string, rewrittenText: string):
Array<{ start: number; end: number; replacement: string }>` — the pure
document-rewrite → per-hunk proposal-range list (vscode-free, deterministic).
- **`TrackChangesPreviewController`** (vscode layer): handles the three new
inbound messages; gains a `runEditAndPropose(document, target: { kind:
"range"; start; end } | { kind: "document" }, instruction)` private routine;
takes (or reaches) the `DiffViewController` to pin the previewed doc and the
edit-turn entry. New test seams as needed (`getLastModel` already exists for
asserting marks without webview DOM).
- **`DiffViewController`** (F6): `pin(document)` is reused as-is (the controller
already exposes pinning by document); `cowriting.pinDiffBaseline`'s
`package.json` `when` flips from `false` to `editorLangId == markdown`.
- **`ProposalController`** (F4): `propose(...)` reused unchanged (called once for
selection, N times for a diffed document). No signature change.
- **`AttributionController`** (F3): unchanged (`applyAgentEdit` reused on accept).
- **Commands / menus (`package.json`):**
- `cowriting.showTrackChangesPreview` — added to `editor/title` with
`when: editorLangId == markdown` (the minimal gateway). Existing palette +
`ctrl+alt+r` kept.
- `cowriting.pinDiffBaseline` — `when` flips to `editorLangId == markdown`
(no longer orphaned).
- `cowriting.editDocument` ("Ask Claude to Edit Document", document-scoped) —
new command registered, routed through `runEditAndPropose({kind:"document"})`;
available for `#42` to reuse. (The preview's selection-scoped edit is driven
by the `askClaude` message carrying webview-resolved offsets, not a command,
since the offsets originate in the webview.)
- **Webview asset** (`media/preview.ts` + `.css`): header gains the two buttons;
a `selectionchange` listener updates the Ask-Claude label; click handlers post
the `ToolbarMsg` intents; the selection→nearest-`data-src` lookup helper. Stays
sealed (nonce'd inline script, CSP unchanged).
### 6.5 PerProduct-Use-Case design
- **PUC-1 (toolbar present):** render the two buttons in the header next to the
annotations checkbox; disable Pin + Ask-Claude when `!isAuthorable(document)`.
- **PUC-2 (adaptive label):** webview `selectionchange` → if the selection is
non-empty and within the rendered body, label = "Edit Selection"; else "Edit
Document". Pure webview-local state.
- **PUC-3 (edit selection):** webview resolves selection → `{start, end}` from the
nearest `data-src` ancestors → `postMessage{askClaude, selection, start, end}` →
host `showInputBox` → `runEditAndPropose({kind:"range", start, end})` →
`runEditTurn` → one `propose()` → `onDidChangeProposals` → re-render (inline
blue block at the anchor, `#31`).
- **PUC-4 (edit document):** `postMessage{askClaude, document}` → host input box →
`runEditAndPropose({kind:"document"})` → `runEditTurn` over full text →
`diffToHunks(current, rewritten)` → one `propose()` per hunk → re-render (N blue
blocks).
- **PUC-5 (pin baseline):** `postMessage{pinBaseline}` → `DiffViewController.pin(
previewedDocument)` → `onDidChangeBaseline` → re-render (marks cleared).
- **PUC-6 (right-click gateway):** `editor/title` entry invokes
`cowriting.showTrackChangesPreview` for the tab's document.
- **PUC-7 (edges):** selection resolving to no live block → document scope;
non-authorable → controls disabled; `runEditTurn` failure → existing error path,
no proposal; empty doc → Edit Document over empty range (no-op-safe).
### 6.6 Non-functional requirements & cross-cutting concerns
The webview stays **sealed** (INV-21): local assets, strict CSP with a per-load
nonce, no network; the new inline handlers only read `data-src`/`data-proposal-id`
and post intent (no eval, no remote, no document mutation). The instruction prompt
and the LLM turn remain **host-side** — the webview gains **no** LLM, network, or
credential surface (INV-8 untouched). `diffToHunks` and the block wrapping are
O(document), run on a host gesture (not per-keystroke), fine at inner-loop scale.
No telemetry, nothing persisted.
### 6.7 Key decisions & alternatives considered
| Decision | Chosen | Alternatives rejected |
| --- | --- | --- |
| **Preview-selection → source mapping granularity** | **Block-level** — pure layer emits `data-src-start/end` from existing `BlockWithRange`; selection → union of intersected live-source blocks. Robust, reuses what exists, ships the full adaptive button now. *(Operator decision, session 0036.)* | **Char-precise** sub-block mapping — needs per-inline-token source offsets markdown-it doesn't reliably give; rendered text ≠ source (syntax stripped) → fragile, risks the whole feature on the hardest part. **Document-only first** — defers the headline adaptive button; punts the risk. |
| **Document-edit proposal granularity** | **Diff Claude's rewrite into hunks → one F4 proposal per changed hunk** — independent ✓/✗ per change; reuses the single-range model N times (no model change). *(Operator decision, session 0036.)* | **One whole-document proposal** — a single giant blue block, all-or-nothing accept/reject; poor UX for a real rewrite. |
| **`#43` vs `#41`/`#42` scope** | **`#43` lands a minimal right-click → Open Review Preview gateway** (`editor/title`), so the toolbar surface is reachable end-to-end and its E2E is real; `#41`/`#42` expand the menu set. *(Operator decision, session 0036.)* | **Toolbar only; all menus in `#41`/`#42`** — `#43`'s "a right-click entry opens the preview" acceptance/E2E couldn't be satisfied within `#43`. |
| **Instruction prompt location** | **Host `showInputBox`** — keeps LLM/secret surface out of the sealed webview; reuses the existing edit-turn flow. | **In-webview text field** — pushes prompt handling toward the sandbox; no benefit. |
| **Pin button target** | **The previewed document** (`DiffViewController.pin(document)`) — the preview knows its bound doc. | **`activeTextEditor`-based command** — may not point at the previewed doc; the source of the orphan. |
| **Pin confirmation** | **No confirm** — matches the existing command; re-pinning recovers. | **Confirm dialog** — friction for a routine, recoverable gesture. |
### 6.8 Testing strategy
- **Unit (vitest, vscode-free):** `data-src-start/end` present and correct on every
block for **both** `renderReview` and `renderPlain` (offsets equal the
`BlockWithRange` ranges; determinism — same inputs → identical HTML);
`diffToHunks` over fixtures — a single-hunk rewrite → one range; a multi-hunk
rewrite → the expected disjoint ranges with correct replacements; an unchanged
rewrite → zero hunks; whole-document replacement → one full-range hunk.
- **Host E2E (`@vscode/test-electron`, no LLM, extends the F10 suite):** open a
markdown fixture → `cowriting.showTrackChangesPreview`. Simulate
`{type:"pinBaseline"}` → `getLastModel` shows cleared change-marks + advanced
epoch. Simulate `{type:"askClaude", scope:"selection", start, end}` with a
stubbed edit turn → exactly **one** proposal over the resolved range, anchored
inline. Simulate `{type:"askClaude", scope:"document"}` with a stubbed
multi-hunk rewrite → **N** proposals matching the hunks. Invoke the
`editor/title` gateway command → panel opens. Non-authorable document → toolbar
edit controls disabled (asserted via the model/flags the host exposes). The
webview DOM, real button clicks, the `selectionchange` label flip, and the
selection→`data-src` lookup are **not** E2E-asserted (sealed sandbox) — manual
smoke.
- **Live smoke (manual — `docs/MANUAL-SMOKE-F11.md`):** open a markdown doc; open
the review preview; confirm the three header controls; select a paragraph →
button reads "Edit Selection", click → enter instruction → a blue ✓/✗ block
appears at that paragraph; clear the selection → button reads "Edit Document",
click → instruction → several blue blocks appear; click **Pin baseline** → marks
clear, `Since` label updates; right-click the tab → **Open Review Preview** opens
the panel; verify light/dark theming and that `git status` shows nothing
unexpected.
### 6.9 Failure modes, rollback & flags
A selection that resolves to no live-source block → **Edit Document** scope (never
an error). `runEditTurn` failing → existing error handling, no proposal created,
preview unchanged. `diffToHunks` producing zero hunks (rewrite == current) → no
proposals, a brief "no changes proposed" host notice. Webview disposed mid-gesture
→ the host routine completes against the document; the next open re-renders.
**No feature flag** — the toolbar controls are additive UI; nothing persists.
Rollback is reverting the PR with **zero** data migration (nothing persisted; the
F6 baseline, F4 sidecar, F3 attribution data are untouched; the unhidden pin
command and the gateway menu simply disappear).
---
## 7. Delivery Plan
### 7.1 Approach / strategy
One planning-and-executing session (F11 = `#43`), plan written just-in-time from
this spec — the F2F10 precedent. Host-E2E tier (a VS Code extension has no
browser/deploy stage); no LLM in CI (edit turns stubbed). The webview's visual
rendering, the adaptive label, and the selection→source DOM lookup are verified by
the manual smoke; the automated seams are the pure block-wrapping + `diffToHunks`
model and the host's message→seam wiring.
### 7.2 Slicing plan
- **SLICE-1 — Pin baseline button + reachability.** Webview header **Pin
baseline** button → `{type:"pinBaseline"}` → host `DiffViewController.pin(
previewedDoc)`; unhide `cowriting.pinDiffBaseline` (`when: editorLangId ==
markdown`). Host E2E: pin message clears marks. *(Immediate win — homes the
orphaned command.)*
- **SLICE-2 — Block-offset emission.** Shared pure helper wrapping each block with
`data-src-start/end` in `renderReview` **and** `renderPlain`; vitest for both
modes + determinism. No UI yet. (INV-36 data layer.)
- **SLICE-3 — Edit Document button + hunk path.** Webview **Ask Claude to Edit
Document** button (no-selection state) → `{type:"askClaude", scope:"document"}`;
host `runEditAndPropose({document})` → `runEditTurn` → `diffToHunks` → N
`propose()`; register `cowriting.editDocument`; vitest for `diffToHunks`; host
E2E for the N-proposal path. (INV-37 document half.)
- **SLICE-4 — Adaptive Edit Selection.** Webview `selectionchange` label flip +
selection→nearest-`data-src` resolution → `{type:"askClaude", scope:"selection",
start, end}`; host single-range `propose()`. Host E2E for the selection message →
one anchored proposal. (INV-37 selection half; INV-36 consumer.)
- **SLICE-5 — Gateway, edges, tests & docs.** `editor/title` → Open Review Preview
gateway; non-authorable disabling; host E2E (gateway opens panel; controls
inert on non-authorable); `docs/MANUAL-SMOKE-F11.md`; README F11 section.
E2E are first-class plan tasks (handbook §9/§4); this app's required tier is host
E2E (the F2F10 precedent).
### 7.3 Rollout / launch plan
Non-shippable (no marketplace publish). "Done" = `#43` acceptance: the preview
toolbar hosts the annotations checkbox + a Pin baseline button + a single adaptive
Ask-Claude button (Edit Selection ⇆ Edit Document) that route through the existing
F4/F3/F6 machinery; edits surface as proposals (one for a selection, per-hunk for
a document rewrite); a right-click entry opens the preview; the pin command is no
longer orphaned; unit + host E2E green; live smoke performed once.
### 7.4 Risks & mitigations
| Risk | Mitigation |
| --- | --- |
| Block-level selection feels coarse vs the editor's char-precise Edit Selection | Locked v1 decision (§6.7); a rendered surface is naturally block-grained; char-precise is a deferred follow-up if the coarseness bites |
| `data-src` attributes perturb markdown-it output or the F10 proposal/diff rendering | Wrapping is applied at the block boundary (outside inline parsing); covered by determinism + both-mode unit tests; per-block `try/catch` error chip (F7) on render failure |
| `diffToHunks` produces awkward hunk boundaries on a large rewrite | Pure + unit-tested over fixtures; hunks are line/block-aligned; worst case is more/fewer blocks, all independently ✓/✗-able — never wrong, just granular |
| Selection inside a deletion/proposal block has no live-source range | Falls back to Document scope by design (INV-36); manual-smoke verified |
| The webview selection→`data-src` lookup isn't E2E-testable (sealed) | The host half (offsets→fingerprint→propose) is E2E'd via simulated messages; the DOM lookup is the only manual-smoke-only seam, kept deliberately thin |
| Unhiding pin / adding `editDocument` widens the command surface | Both guard on `editorLangId == markdown`; both route through existing seams; no new model or persisted state |
---
## 8. Traceability matrix
| Requirement (`#43`) | Use case | Design | Slice |
| --- | --- | --- | --- |
| Pin baseline button in the preview toolbar | PUC-5 | INV-35, §6.4 (`DiffViewController.pin`) | SLICE-1 |
| Resolve the orphaned `pinDiffBaseline` reachability | PUC-5 | §6.4 (`when` flip) | SLICE-1 |
| Single adaptive Ask-Claude button (Selection ⇆ Document) | PUC-2/3/4 | INV-37, §6.2 | SLICE-3/4 |
| Preview-selection → source range mapping (block-level) | PUC-3 | INV-36, §6.7 | SLICE-2/4 |
| Edit Document path (new whole-document edit) | PUC-4 | INV-37, §6.5 (hunk diff) | SLICE-3 |
| Edits route through existing F4/F3 (no divergent path) | PUC-3/4 | INV-35, §6.4 | SLICE-3/4 |
| Right-click entry opens the preview (minimal gateway) | PUC-6 | §6.4 (`editor/title`) | SLICE-5 |
| Controls only active for supported (authorable) docs | PUC-1/7 | §6.5 | SLICE-5 |
| Sealed webview, no document mutation / LLM surface | — | INV-21/35, §6.6 | all |
| No new edit/attribution/proposal model | — | §1.7, INV-37 | all |
| Unit + host E2E + right-click-opens-preview coverage | — | §6.8 | SLICE-1..5 |
## 9. Open Questions & Decisions log
- **RESOLVED (session 0036, operator):** preview-selection → source mapping =
**block-level** (`data-src` attributes from `BlockWithRange`; union of
intersected blocks); document edit = **diffed into per-hunk F4 proposals**;
`#43` **lands a minimal right-click → Open Review Preview gateway** (`#41`/`#42`
expand the menus).
- **RESOLVED (this spec, autonomous):** instruction prompt = **host `showInputBox`**
(LLM/secrets stay out of the webview); Pin targets the **previewed document**
(`DiffViewController.pin`); **no confirmation** on pin (matches existing); a new
`cowriting.editDocument` command is registered for `#42` reuse; `pinDiffBaseline`
is unhidden (`editorLangId == markdown`).
- **OPEN → later:** **char-precise** sub-block selection mapping (deferred — block
granularity is v1); the **richer `#41`/`#42` menu sets** (this lands only the
minimal gateway); preview→source **scroll-sync** (`#32`); whether a large
document rewrite should cap/segment its hunks (only if real rewrites prove
noisy); the repo rename to `vscode-markdown-cowriting-plugin` (`#35`, deferred).
## 10. Glossary & References
- **Preview toolbar** — the review preview's header row: the annotations on/off
checkbox (existing) plus F11's Pin baseline and adaptive Ask-Claude buttons.
**Adaptive Ask-Claude button** — one button reading "Edit Selection" (non-empty
preview selection) or "Edit Document" (none). **Block-level selection mapping** —
resolving a rendered-preview selection to the union of source blocks it
intersects, via `data-src-start/end` attributes emitted by the pure render
layer. **Hunk-diffed document edit** — Claude's whole-document rewrite split
into changed hunks, each surfaced as its own F4 proposal. **Pin baseline** — F6's
`DiffViewController.pin` applied to the previewed document. **Gateway** — a
right-click entry that opens the preview (this feature lands the minimal one;
`#41`/`#42` expand them).
- Feature `#43` (F11) · Epic `#1` · builds on F3 `#6`, F4 `#12`, F6 `#17`/`#19`,
F7 `#21`/`#22`, F9 `#27`, F10 `#29`/`#31` · coexists with `#41`/`#42` · parent
specs `coauthoring-inner-loop.md`, `coauthoring-attribution.md`,
`coauthoring-propose-accept.md`, `coauthoring-diff-view.md`,
`coauthoring-rendered-preview.md`, `coauthoring-interactive-review.md` · capture
session 0035 · lineage `ben.stull/rfc-app#48`.