Solution Design for Feature #43 (F11): the F10 review preview's header gains a Pin baseline button and a single adaptive Ask Claude button (Edit Selection / Edit Document) beside the existing annotations checkbox, plus a minimal right-click gateway that opens the preview. Three forks locked in brainstorming session 0036: - block-level preview-selection -> source mapping (data-src on blocks from BlockWithRange; union of intersected live-source blocks) - document edit diffed into per-hunk F4 proposals (no model change) - #43 lands a minimal editor/title -> Open Review Preview gateway (#41/#42 expand the menus) Reuses F4 propose/F3 attribution/F6 baseline seams; sealed webview posts intent only (INV-35/36/37). Resolves the orphaned pinDiffBaseline reachability gap. Submitted to the content repo at finalize. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 KiB
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 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:falsesince#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 emitsdata-src-start/data-src-endon 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/#42add 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, orSCHEMA_VERSIONchanges.
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 (callsDiffViewController.pin(document)directly — not theactiveTextEditor-based command, which may not point at the previewed doc) →onDidChangeBaseline→ re-render with cleared marks.askClaude→ hostshowInputBoxfor the instruction (keeps the LLM / secret surface out of the sealed webview), then one shared host routinerunEditAndPropose(document, target, instruction):- selection →
target= the block-union range[firstBlock.start … lastBlock.end]; onerunEditTurn→ one F4propose()over that range (the existing Edit-Selection shape). - document →
target= whole document; onerunEditTurnover the full text → diff Claude's result against the current text → onepropose()per changed hunk (multiple proposals, each its own blue ✓/✗ block). Reuses the F4 single-range model N times; no model change.
- selection →
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 <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/#42will 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
runEditTurnerror 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 F4propose→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 fromBlockWithRange) 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-srclookup 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
runEditTurnover the whole document, its result diffed into hunks, one F4propose()per changed hunk. Both scopes call the same hostrunEditAndProposeroutine 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): newwrapBlocksWithSrc(blocks: BlockWithRange[], renderedPerBlock: string[]): string(illustrative) — or, more precisely, bothrenderReviewandrenderPlainroute their per-block rendered HTML through a shared internal helper that prependsdata-src-start/data-src-endto each block's wrapping element. PlusdiffToHunks(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 arunEditAndPropose(document, target: { kind: "range"; start; end } | { kind: "document" }, instruction)private routine; takes (or reaches) theDiffViewControllerto pin the previewed doc and the edit-turn entry. New test seams as needed (getLastModelalready exists for asserting marks without webview DOM).DiffViewController(F6):pin(document)is reused as-is (the controller already exposes pinning by document);cowriting.pinDiffBaseline'spackage.jsonwhenflips fromfalsetoeditorLangId == markdown.ProposalController(F4):propose(...)reused unchanged (called once for selection, N times for a diffed document). No signature change.AttributionController(F3): unchanged (applyAgentEditreused on accept).- Commands / menus (
package.json):cowriting.showTrackChangesPreview— added toeditor/titlewithwhen: editorLangId == markdown(the minimal gateway). Existing palette +ctrl+alt+rkept.cowriting.pinDiffBaseline—whenflips toeditorLangId == markdown(no longer orphaned).cowriting.editDocument("Ask Claude to Edit Document", document-scoped) — new command registered, routed throughrunEditAndPropose({kind:"document"}); available for#42to reuse. (The preview's selection-scoped edit is driven by theaskClaudemessage 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; aselectionchangelistener updates the Ask-Claude label; click handlers post theToolbarMsgintents; the selection→nearest-data-srclookup 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 nearestdata-srcancestors →postMessage{askClaude, selection, start, end}→ hostshowInputBox→runEditAndPropose({kind:"range", start, end})→runEditTurn→ onepropose()→onDidChangeProposals→ re-render (inline blue block at the anchor,#31). - PUC-4 (edit document):
postMessage{askClaude, document}→ host input box →runEditAndPropose({kind:"document"})→runEditTurnover full text →diffToHunks(current, rewritten)→ onepropose()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/titleentry invokescowriting.showTrackChangesPreviewfor the tab's document. - PUC-7 (edges): selection resolving to no live block → document scope;
non-authorable → controls disabled;
runEditTurnfailure → 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/endpresent and correct on every block for bothrenderReviewandrenderPlain(offsets equal theBlockWithRangeranges; determinism — same inputs → identical HTML);diffToHunksover 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"}→getLastModelshows 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 theeditor/titlegateway command → panel opens. Non-authorable document → toolbar edit controls disabled (asserted via the model/flags the host exposes). The webview DOM, real button clicks, theselectionchangelabel flip, and the selection→data-srclookup 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,Sincelabel updates; right-click the tab → Open Review Preview opens the panel; verify light/dark theming and thatgit statusshows 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"}→ hostDiffViewController.pin( previewedDoc); unhidecowriting.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/endinrenderReviewandrenderPlain; 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"}; hostrunEditAndPropose({document})→runEditTurn→diffToHunks→ Npropose(); registercowriting.editDocument; vitest fordiffToHunks; host E2E for the N-proposal path. (INV-37 document half.) - SLICE-4 — Adaptive Edit Selection. Webview
selectionchangelabel flip + selection→nearest-data-srcresolution →{type:"askClaude", scope:"selection", start, end}; host single-rangepropose(). 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-srcattributes fromBlockWithRange; union of intersected blocks); document edit = diffed into per-hunk F4 proposals;#43lands a minimal right-click → Open Review Preview gateway (#41/#42expand 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 newcowriting.editDocumentcommand is registered for#42reuse;pinDiffBaselineis unhidden (editorLangId == markdown). - OPEN → later: char-precise sub-block selection mapping (deferred — block
granularity is v1); the richer
#41/#42menu 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 tovscode-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/endattributes 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'sDiffViewController.pinapplied to the previewed document. Gateway — a right-click entry that opens the preview (this feature lands the minimal one;#41/#42expand 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 specscoauthoring-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 · lineageben.stull/rfc-app#48.