From b2de25ff99e66ea1739605acdff9485090f2b0a3 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Fri, 12 Jun 2026 12:15:30 -0700 Subject: [PATCH] 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) --- ...f11-preview-toolbar-interaction-surface.md | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md diff --git a/docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md b/docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md new file mode 100644 index 0000000..d1a065c --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md @@ -0,0 +1,602 @@ +--- +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 F2–F5 (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 `
` +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 F2–F10); + 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 ` 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 Per–Product-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 F2–F10 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 F2–F10 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`.