Files
vscode-cowriting-plugin-con…/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md
T

37 KiB
Raw Blame History

status
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 (splitBlocksWithRangesBlockWithRange.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):

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):
    • selectiontarget = the block-union range [firstBlock.start … lastBlock.end]; one runEditTurn → one F4 propose() over that range (the existing Edit-Selection shape).
    • documenttarget = 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 proposeaccept/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

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.pinDiffBaselinewhen 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 showInputBoxrunEditAndPropose({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})runEditTurndiffToHunks → 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.