--- 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`.