124 lines
5.7 KiB
Markdown
124 lines
5.7 KiB
Markdown
# 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**
|
|
|
|
1. **Host message routing** (`src/trackChangesPreview.ts`): extract the inline
|
|
`onDidReceiveMessage` body into a private `handleWebviewMessage(document, m)`
|
|
method; add a `pinBaseline` branch that calls `this.diffView.pin(document)`
|
|
(the *previewed* document — not `activeTextEditor`). The existing
|
|
`onDidChangeBaseline` subscription already re-renders with cleared marks.
|
|
2. **Test seam**: add `receiveMessage(uriString, m)` that resolves the doc and
|
|
calls `handleWebviewMessage`, so host E2E can simulate the raw webview message
|
|
and exercise the real routing.
|
|
3. **Webview** (`media/preview.ts` + `.css`): add a `⌖ Pin baseline` button in
|
|
`#cw-header`; click → `postMessage({ type: "pinBaseline" })`; theme-aware CSS.
|
|
4. **Shell HTML** (`shellHtml`): add the `<button id="cw-pin">` to the header row.
|
|
5. **Reachability** (`package.json`): flip `cowriting.pinDiffBaseline`'s
|
|
`commandPalette` `when` from `false` to `editorLangId == 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**
|
|
|
|
1. `src/trackChangesModel.ts`: a shared internal helper that prepends
|
|
`data-src-start="N" data-src-end="M"` to each block's wrapping element, routed
|
|
from both render paths using the existing `splitBlocksWithRanges` offsets.
|
|
2. 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**
|
|
|
|
1. `src/trackChangesModel.ts`: pure `diffToHunks(currentText, rewrittenText):
|
|
Array<{ start; end; replacement }>` — vscode-free, deterministic.
|
|
2. `src/trackChangesPreview.ts`: `runEditAndPropose(document, target, instruction)`
|
|
private routine; `askClaude`/`document` branch → host `showInputBox` →
|
|
`runEditTurn` over full text → `diffToHunks` → one F4 `propose()` per hunk.
|
|
3. `package.json`: register `cowriting.editDocument` (document-scoped), routed
|
|
through `runEditAndPropose({kind:"document"})`; for `#42` reuse.
|
|
4. Webview: `✦ Ask Claude to Edit Document` button (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**
|
|
|
|
1. Webview: `selectionchange` listener flips the Ask-Claude label (Edit Selection
|
|
⇆ Edit Document); selection→nearest-`data-src` ancestor resolution →
|
|
`postMessage({ type:"askClaude", scope:"selection", start, end })`.
|
|
2. Host: `askClaude`/`selection` branch → `runEditAndPropose({kind:"range",
|
|
start, end})` → one `runEditTurn` → one F4 `propose()` over the block-union.
|
|
3. 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**
|
|
|
|
1. `package.json`: add `cowriting.showTrackChangesPreview` to `editor/title` with
|
|
`when: editorLangId == markdown` (minimal right-click gateway).
|
|
2. Non-authorable disabling: Pin + Ask-Claude controls render disabled when
|
|
`!isAuthorable(document)`; annotations toggle stays active.
|
|
3. Host E2E: gateway command opens the panel; controls inert on non-authorable.
|
|
4. `docs/MANUAL-SMOKE-F11.md` (live smoke script per spec §6.8).
|
|
5. 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.
|