5.7 KiB
Implementation Plan: F11 — Preview Toolbar as the Primary Interaction Surface (#43)
Spec: docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md
Anchor: Feature benstull/vscode-cowriting-plugin#43 (F11, type/feature, priority/P1)
Session: vscode-cowriting-plugin-0037
This plan transcribes the spec's §7.2 slicing plan into concrete, file-level tasks. Each slice is independently green (unit + host E2E) before the next. Host E2E is this app's required tier (no browser/deploy stage — a VS Code extension); no LLM in CI (edit turns stubbed). The webview's visual rendering, the adaptive label, and the selection→source DOM lookup are manual-smoke only.
SLICE-1 — Pin baseline button + reachability (the immediate win)
Homes the orphaned cowriting.pinDiffBaseline command and gives the writer a
reachable Pin control in the preview toolbar.
Tasks
- Host message routing (
src/trackChangesPreview.ts): extract the inlineonDidReceiveMessagebody into a privatehandleWebviewMessage(document, m)method; add apinBaselinebranch that callsthis.diffView.pin(document)(the previewed document — notactiveTextEditor). The existingonDidChangeBaselinesubscription already re-renders with cleared marks. - Test seam: add
receiveMessage(uriString, m)that resolves the doc and callshandleWebviewMessage, so host E2E can simulate the raw webview message and exercise the real routing. - Webview (
media/preview.ts+.css): add a⌖ Pin baselinebutton in#cw-header; click →postMessage({ type: "pinBaseline" }); theme-aware CSS. - Shell HTML (
shellHtml): add the<button id="cw-pin">to the header row. - Reachability (
package.json): flipcowriting.pinDiffBaseline'scommandPalettewhenfromfalsetoeditorLangId == markdown.
Host E2E (test/e2e/suite/trackChangesPreview.test.ts or new f11 suite):
open a markdown fixture with a divergent baseline → some block marked; simulate
{type:"pinBaseline"} via receiveMessage → getLastModel shows every block
unchanged (marks cleared) and getBaseline(key).reason === "pinned".
Unit: none new (pure layer untouched this slice).
SLICE-2 — Block-offset emission (INV-36 data layer)
Shared pure helper wrapping each rendered block with data-src-start/-end
(source char offsets from BlockWithRange) in both renderReview and
renderPlain. vscode-free, DOM-free, deterministic (extends INV-22/33). No UI.
Tasks
src/trackChangesModel.ts: a shared internal helper that prependsdata-src-start="N" data-src-end="M"to each block's wrapping element, routed from both render paths using the existingsplitBlocksWithRangesoffsets.- Skip blocks with no live-source range (deletion-only / proposal blocks).
Unit (test/trackChangesModel.test.ts): data-src-start/end present and
correct on every block for both modes (offsets equal BlockWithRange ranges);
determinism (same inputs → identical HTML).
SLICE-3 — Edit Document button + hunk path (INV-37 document half)
Tasks
src/trackChangesModel.ts: purediffToHunks(currentText, rewrittenText): Array<{ start; end; replacement }>— vscode-free, deterministic.src/trackChangesPreview.ts:runEditAndPropose(document, target, instruction)private routine;askClaude/documentbranch → hostshowInputBox→runEditTurnover full text →diffToHunks→ one F4propose()per hunk.package.json: registercowriting.editDocument(document-scoped), routed throughrunEditAndPropose({kind:"document"}); for#42reuse.- Webview:
✦ Ask Claude to Edit Documentbutton (no-selection state) →postMessage({ type: "askClaude", scope: "document" }).
Unit: diffToHunks over fixtures (single hunk → one range; multi-hunk →
disjoint ranges + correct replacements; unchanged → zero hunks; whole-doc
replacement → one full-range hunk).
Host E2E: simulate {askClaude, scope:"document"} with a stubbed multi-hunk
rewrite → N proposals matching the hunks.
SLICE-4 — Adaptive Edit Selection (INV-37 selection half; INV-36 consumer)
Tasks
- Webview:
selectionchangelistener flips the Ask-Claude label (Edit Selection ⇆ Edit Document); selection→nearest-data-srcancestor resolution →postMessage({ type:"askClaude", scope:"selection", start, end }). - Host:
askClaude/selectionbranch →runEditAndPropose({kind:"range", start, end})→ onerunEditTurn→ one F4propose()over the block-union. - Edge: selection resolving to no live block → fall back to document scope.
Host E2E: simulate {askClaude, scope:"selection", start, end} with a stubbed
edit turn → exactly one proposal over the resolved range, anchored inline.
SLICE-5 — Gateway, edges, tests & docs
Tasks
package.json: addcowriting.showTrackChangesPreviewtoeditor/titlewithwhen: editorLangId == markdown(minimal right-click gateway).- Non-authorable disabling: Pin + Ask-Claude controls render disabled when
!isAuthorable(document); annotations toggle stays active. - Host E2E: gateway command opens the panel; controls inert on non-authorable.
docs/MANUAL-SMOKE-F11.md(live smoke script per spec §6.8).- README F11 section.
Done = #43 acceptance (spec §7.3)
Preview toolbar hosts annotations checkbox + Pin baseline + single adaptive Ask-Claude (Edit Selection ⇆ Edit Document) routing through existing F4/F3/F6; 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.