From 118bc293ad58d7876350ae3090f329acf185e24c Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 11 Jun 2026 21:52:37 -0700 Subject: [PATCH] add spec ./specs/coauthoring-interactive-review.md (status: graduated) --- specs/coauthoring-interactive-review.md | 609 ++++++++++++++++++++++++ 1 file changed, 609 insertions(+) create mode 100644 specs/coauthoring-interactive-review.md diff --git a/specs/coauthoring-interactive-review.md b/specs/coauthoring-interactive-review.md new file mode 100644 index 0000000..1280e6e --- /dev/null +++ b/specs/coauthoring-interactive-review.md @@ -0,0 +1,609 @@ +--- +status: graduated +--- +# Solution Design: Interactive Track-Changes Review in the Markdown Preview (F10) + +| | | +| --- | --- | +| **Author(s)** | Ben Stull (with Claude) | +| **Reviewers / approvers** | Ben Stull | +| **Status** | `draft` | +| **Version** | v0.1.0 | +| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#29` (F10, `type/feature`, `priority/P1`) · Epic `#1` (closed) · Capture session `vscode-cowriting-plugin-0028` (2026-06-11) · Brainstorming session `vscode-cowriting-plugin-0029` · Builds on (all shipped): F3 `#6` (live attribution), F4 `#12` (propose/accept), F6 `#17`/`#19` (baseline + diff view), F7 `#21`/`#22` (rendered preview + intra-diagram mermaid), F9 `#27` (authorship preview) · Parent specs (graduated): `coauthoring-inner-loop.md`, `coauthoring-attribution.md`, `coauthoring-propose-accept.md`, `coauthoring-diff-view.md`, `coauthoring-rendered-preview.md`, `2026-06-11-authorship-preview-design.md` · Lineage: `ben.stull/rfc-app#48` | + +**Change log** + +| Date | Version | Change | By | +| --- | --- | --- | --- | +| 2026-06-11 | v0.1.0 | Initial draft — brainstorming session 0029 (from the capture in session 0028, which followed the session-0027 ideation where the operator found the F6 two-pane diff + F3 attribution overlap confusing). | Ben Stull + Claude | + +--- + +## 1. Business Context + +### 1.1 Executive Summary + +The plugin shipped its review signals as **several overlapping surfaces that +collide on the same text**: F6's two-pane red/green raw diff (`ctrl+alt+d`), F3's +in-editor attribution decorations (green human border / blue Claude fill), F4's +in-editor amber proposal threads, and F7/F9's rendered preview (two modes). In +manual testing the operator opened the F6 diff and saw F3's blue attribution +bleeding into it at the same time — *"I don't understand this."* + +F10 resolves this into **one mental model: write on the left, review on the +right.** The editor becomes a **clean, zero-annotation** markdown pane. The +**rendered preview becomes the single review surface**, with an **annotations +on/off** toggle. With annotations **on**, the preview paints every change since +the F6 baseline in one visual language — **green = human-authored, blue = +LLM-authored, strikethrough = deleted** — and lets the human **accept (✓) or +reject (✗) Claude's pending proposals, and only those, right inside the +preview.** F10 is **mostly assembly** of shipped features (F3/F4/F6/F7/F9) plus +three new pieces: strip the editor decorations, collapse F9's two preview modes +into the one toggle, and make the preview interactive. + +### 1.2 Background + +The inner loop shipped F2–F5 (threads · attribution · propose/accept · +cross-rung). F6 added the baseline + a native two-pane diff toggle (`#17`), +broadened to any file (`#19`). F7 added the rendered track-changes preview +(`#21`) with full mermaid support, refined by F7.1 intra-diagram mermaid diffing +(`#22`). F9 added an **Authorship** mode to that preview (`#27`), switched by a +segmented `[ Track changes | Authorship ]` header control — and F9 **explicitly +chose two separate modes, never combined** (F9 INV-26). + +The friction the operator hit is the *multiplicity* of surfaces, not any one of +them: the two-pane raw diff is low-altitude for prose; the in-editor decorations +clutter the writing surface (the human authored most of the document, so +attribution paints everywhere); and there is no single place to answer "what +changed, who changed it, and do I keep the LLM's edits?" Capture session 0028 +filed Feature `#29` locking the product vision (markdown-only; clean editor; +preview as the single review surface). This spec is the Solution Design for it. + +### 1.3 Business Actors / Roles + +- **Coauthor (human)** — the writer/engineer (PP-1); F10's sole user. +- **Coauthor (machine)** — Claude via `@cline/sdk`; not a user of F10, but its + proposals are what the human reviews in the preview, and its landings advance + the F6 baseline. + +### 1.4 Problem Statement + +A markdown writer reviewing a coauthoring session has **no single, intuitive +surface** that answers "what changed since the last coauthoring moment, who +changed it, and do I keep Claude's edits?" The signal is fragmented across a raw +two-pane diff, in-editor decorations, and a two-mode preview that shows changes +**or** authorship but never both, and never lets the human act on Claude's +proposals from inside the rendered view. + +### 1.5 Pain Points + +- **Surface collision** — F6's raw diff and F3's attribution paint the same text + simultaneously and read as noise ("I don't understand this"). +- **Cluttered editor** — F3 attribution + F4 proposal threads decorate the + writing pane; since the human authored most of the document, attribution paints + nearly everywhere. +- **Two-pane raw diff is low-altitude for prose** — you read markdown source, not + the rendered document. +- **Changes XOR authorship** — F9's preview shows *what changed* (track-changes) + or *who authored* (authorship) but never both in one glance, and neither lets + you accept/reject from the rendered view. + +### 1.6 Targeted Business Outcomes + +One clean model: **write on the left, review on the right.** The editor is +distraction-free (zero annotations). The preview, with annotations on, shows +every change since the baseline in one visual language **and** lets the human +keep or revert Claude's proposals without leaving that pane — so "what did the +LLM change, and do I want it?" is answered **in place**, in the rendered +document, not in a raw diff or a cluttered editor. + +### 1.7 Scope (business) + +**In scope:** a clean (zero-annotation) markdown editor; the rendered preview as +the **single** review surface; an **annotations on/off** toggle (off = clean +rendered markdown, built-in-preview parity; on = the combined annotated view); +the unified per-author + strikethrough annotation language (green human / blue +LLM / struck deletions, reconstructed from the F6 baseline); **interactive +accept/reject of Claude's pending proposals from inside the preview** (✓/✗); +deprecating F6's two-pane view (command + keybinding hidden) and stripping the +editor's F3/F4 decorations; unit + host-E2E coverage; manual webview smoke. + +**Out of scope (deferred, not forgotten):** **non-markdown review** (F6's +any-file reach `#19` is dropped *as a user goal* — see §6.7); the **repo rename** +to `vscode-markdown-cowriting-plugin` (the markdown-only identity is affirmed, +but the rename — Gitea repo, `app.json`, content repo, session-history paths — is +a separate deliberate gesture); preview→source **scroll-sync**; export / print / +copy-as-clean; **editing in the preview** beyond the ✓/✗ proposal controls; +intra-emphasis sentinel hardening (the F9 deferred refinement). + +**Non-goals (firm):** any change to the **sidecar** persistence, the **F4 +propose/accept model** (proposals stay pending-by-default — F10 surfaces and acts +on them, it does **not** change how Claude's edits enter the system), the +**cross-rung contract**, or `SCHEMA_VERSION`; a general-purpose markdown +previewer; a WYSIWYG editor; combining anything with the LLM/network/credential +surface (INV-8 untouched). + +### 1.8 Assumptions · Constraints · Dependencies + +- **Anchor:** Feature `#29` (F10). **Mostly assembly** of shipped features: + - **F6 baseline** (`#17`/`#19`) — kept as the **data layer** ("what changed + since when"; auto-advances at every machine landing, INV-18); F6's two-pane + *view* is deprecated as a user surface (command/keybinding hidden, code kept). + - **F7 render/diff engine** (`#21`/`#22`) — the rendered preview, block/word + diff, strikethrough deletions, atomic code/mermaid, and F7.1 intra-diagram + mermaid diff already exist and are reused. + - **F3 attribution** (`#6`) — kept as the **data layer** (`spansFor` — who + authored each span); its *in-editor decorations* are removed. + - **F4 propose/accept** (`#12`) — the accept/reject *logic* + (`ProposalController.accept`/`reject` → `applyAgentEdit`, the INV-9 + `WorkspaceEdit` seam) already exists; F10 moves the *controls* into the + preview and removes the in-editor proposal threads. + - **F9 authorship preview** (`#27`) — its two preview modes **collapse** into + the one annotations on/off toggle; its PUA-sentinel author-coloring technique + is **reused** inside the new combined render; its standalone + `renderAuthorship` / segmented control are **superseded** (removed). +- **Constraint:** the preview is a **sealed webview** (strict CSP, local assets, + no network — F7 INV-21). Interactive ✓/✗ controls post messages to the + extension host, which applies/discards proposals via the F4 path; the webview + **never** edits the document directly. +- **Constraint:** deletions aren't in the live buffer — they are reconstructed by + diffing the buffer against the F6 baseline (the F7 engine already does this). +- No LLM, no network, no new credential surface (INV-8 untouched); nothing + persisted changes (`.threads/`, the contract, `SCHEMA_VERSION` all untouched). + +### 1.9 Business Use Cases + +- **BUC-1 (unified review)** Mid-session the writer opens the preview and sees the + rendered chapter with their own additions in green, their deletions struck, and + Claude's pending proposals as blue blocks carrying ✓/✗ — one glance answers + "what changed, who, and do I keep Claude's edits?" +- **BUC-2 (act in place)** The writer clicks ✓ on a Claude proposal in the + preview; the proposal lands in the document, the baseline advances, and the + blue block becomes ordinary (unmarked) text — without leaving the preview or + touching the editor. +- **BUC-3 (clean writing)** The writer works in the editor, which shows **no** + coauthoring decorations at all — a plain markdown surface — and flips + annotations **off** in the preview to read the document clean (built-in-preview + parity) when they want to. + +--- + +## 2. Solution Proposal + +The editor is stripped of **all** coauthoring decorations (F3 attribution tint, +F4 proposal threads), and F6's two-pane diff command/keybinding are hidden +(`when:false`); the F6 baseline store and `onDidChangeBaseline` event remain as +the data layer. + +The preview's per-panel mode collapses from F9's `"changes" | "authorship"` to a +single **`"on" | "off"`** annotations toggle (default **on**): + +- **Off** — plain `markdown-it` render of the current buffer, no marks (parity + with VS Code's built-in markdown preview). +- **On** — a **combined annotated render** produced by one new **pure, + vscode-free** engine, `renderReview(baselineText, currentText, authorSpans, + proposals)`, that overlays **two axes in a single pass**: + 1. **Changes since the F6 baseline** — the F7 block + word diff of + `baselineText` vs `currentText`: additions and deletions, **author-colored** + via F3 `authorSpans` (human → green, agent → blue) using F9's PUA-sentinel + technique; deletions rendered **struck** (reconstructed from the baseline). + 2. **Pending Claude proposals** — each F4 pending proposal rendered **inline at + its resolved anchor** as a **blue block** (the replaced text struck, the + proposed text shown) carrying a `data-proposal-id` and **✓/✗ buttons**. + +The webview becomes **interactive**: clicking ✓/✗ posts `{type: "accept" | +"reject", proposalId}` to the host, which calls `ProposalController.accept` / +`reject` — accept runs the existing `applyAgentEdit` `WorkspaceEdit` seam and +advances the baseline (INV-18), reject discards the proposal; either way the host +re-renders. The webview never mutates the document (INV-20/21 hold). Everything +downstream of `(baseline, current, spans, proposals) → HTML` is a pure function +(INV-33), unit-testable with no vscode and no webview. + +Because proposals are now **preview-only**, a small **status-bar indicator** +("N Claude proposals — open review", clickable to open the preview) signals +pending proposals when no preview is open (§6.5 PUC-6). + +--- + +## 3. Product Personas + +- **PP-1 Inner-loop coauthor** — the human markdown writer/engineer (as F2–F9); + the only persona F10 serves. + +## 4. Product Use Cases + +- **PUC-1 (clean editor)** Opening / editing a markdown document shows a plain + editor with **no** coauthoring decorations (no F3 tint, no F4 threads, no F6 + diff). The writing surface is distraction-free. +- **PUC-2 (toggle annotations)** In the preview, a header switch flips + annotations **on** (combined annotated render) / **off** (clean rendered + markdown). Default on. Mode is remembered per panel. +- **PUC-3 (see who changed what)** With annotations on, the writer's additions + since the baseline render **green**, their deletions render **struck**, and + Claude's pending proposals render as **blue** blocks. One visual language. +- **PUC-4 (accept a proposal)** The writer clicks **✓** on a blue proposal → it + lands in the document via the F4 seam → the baseline advances → the block + re-renders as ordinary (unmarked) text. The editor reflects the new text, still + with no decorations. +- **PUC-5 (reject a proposal)** The writer clicks **✗** on a blue proposal → the + proposal is discarded → the block disappears on re-render → the document is + unchanged. +- **PUC-6 (proposal while no preview open)** Claude proposes while no preview is + open → a **status-bar item** ("N Claude proposals — open review") appears; + clicking it opens the preview where the proposals are reviewable. (No forced + auto-open — the writer stays in control of focus.) +- **PUC-7 (graceful edges)** Non-markdown active editor → the open command warns + and opens nothing (markdown-only). A malformed markdown/mermaid block → an + inline error chip for that block, the rest renders. No baseline yet → render + current doc with **no** change-marks (everything is "since opened") + a header + note; proposals (if any) still render with ✓/✗. + +## 5. UX Layout + +A webview in the editor column **beside** the source (`ViewColumn.Beside`). The +source editor stays fully editable but is now **visually clean** — no coauthoring +decorations. The preview is **read-only rendered output** except for the ✓/✗ +proposal controls, which post intent (never edit the doc directly). With +annotations **on**: + +- **Human additions** since baseline — soft green background (`cw-by-human` over + ``), themeable. +- **LLM-authored text** (already landed, attributed by F3) — soft blue + (`cw-by-claude`). +- **Deletions** — struck-through, muted (``), reconstructed + from the baseline. +- **Pending Claude proposal** — a **blue proposal block**: the text it would + replace shown struck, the proposed text shown, and a compact **✓ / ✗** control + pair in the block's corner (`data-proposal-id` on the block). +- **Atomic blocks** (code / mermaid) — diffed whole (INV-23) with F7.1 + intra-diagram mermaid styling for changed flowchart/sequence diagrams; an + overlapping author span yields a block-level author badge (F9 INV-27). +- A compact **header bar**: the annotations **on/off** switch; in the on state, a + `Since ` label (reusing F6's epoch text) + a `+N −M · P proposals` + summary and the legend (● You · ● Claude · ✓/✗ = accept/reject Claude). + +Colors derive from the active VS Code theme via webview CSS variables (light / +dark / high-contrast); proposal-block and ✓/✗ button styling are bespoke but +theme-variable-driven. + +--- + +## 6. Technical Design + +### 6.1 Invariants + +Parent invariants INV-1..INV-31 carry over **except where F10 supersedes them**. +F10 adds: + +- **INV-32 (single review surface; clean editor)** The rendered preview is the + **only** annotated review surface. The editor carries **zero** coauthoring + decorations: F3 attribution decorations, F4 in-editor proposal threads, and the + F6 two-pane diff are all removed from / hidden in the editor. (F3 `spansFor`, + F4 accept/reject logic, and the F6 baseline store remain as **data layers**.) +- **INV-33 (combined pure render)** The "on" render unifies two axes — + changes-since-baseline (author-colored additions + struck deletions) **and** + interactive pending proposals — in **one pass** of a pure, vscode-free, + DOM-free function `renderReview(baselineText, currentText, authorSpans, + proposals): string`: same inputs → identical HTML (extends INV-22). The "off" + render is the plain `markdown-it` of the current buffer. +- **INV-34 (✓/✗ act only on pending Claude proposals, via the F4 seam)** + Interactive controls appear **only** on pending Claude proposals (blocks + carrying `data-proposal-id`); human (green) changes carry no controls. ✓/✗ post + **intent** to the host; **all** document mutation goes through the existing F4 + `ProposalController.accept`/`reject` → `applyAgentEdit` (`WorkspaceEdit`) seam + (INV-9). The webview never mutates the document, sidecar, or baseline + (INV-20/21 hold). + +**Supersession (explicit):** F10 **reverses F9 INV-26** ("authorship mode is +baseline-independent and never combined with the diff"). F10 *combines* the +authorship axis with the change axis by design; F9's standalone authorship mode +and segmented control are removed. F9 INV-27 (atomic fences get a block-level +author badge) and INV-28 (pure authorship render) are **absorbed** into INV-33 +(the combined render keeps fences atomic and stays pure). + +### 6.2 High-level architecture + +Three reused data layers + one new pure render path + an interactive webview. + +```mermaid +flowchart LR + edit["source editor\n(clean — no decorations)"] -- onDidChangeTextDocument (debounced) --> ctl + base["F6 baseline\n(DiffViewController, INV-18)"] -- onDidChangeBaseline --> ctl["trackChangesPreview\n(vscode layer)"] + attr["F3 AttributionController\nspansFor(doc)"] -- author spans --> ctl + prop["F4 ProposalController\npending proposals"] -- onDidChangeProposals --> ctl + ctl -- "(baseline, current, spans, proposals)" --> model["renderReview\n(pure, vscode-free)\nblock+word diff · author sentinels · proposal blocks"] + model -- annotated HTML --> ctl + ctl -- postMessage{html, mode, summary} --> wv["webview\n(sealed CSP, local assets)\nmermaid.run() · ✓/✗ buttons"] + wv -- "postMessage{accept|reject, proposalId}" --> ctl + ctl -- "accept/reject" --> prop + prop -- "applyAgentEdit (WorkspaceEdit)" --> doc[(document)] + prop -- accept --> base +``` + +- **`trackChangesModel.ts`** (pure, vscode-free — INV-33) gains + **`renderReview(baselineText, currentText, authorSpans, proposals): string`**. + Internals: (1) the existing F7 `diffBlocks` over baseline/current → block ops; + (2) for changed/added **prose**, the existing word-level ``/``, + **then** author-color each `` (and live text) by intersecting `authorSpans` + via the F9 **PUA-sentinel** technique (refactored out of `renderAuthorship` + into a shared `colorByAuthor` helper); (3) **inject proposal blocks**: for each + proposal, resolve its anchor offset in `currentText`, emit a + `
` containing the struck + replaced-text and the proposed text + a `✓ ✗` + placeholder rendered by the webview; (4) atomic code/mermaid unchanged (F7.1 + reused). Off-mode calls a thin `renderPlain(currentText)` (markdown-it only). +- **`trackChangesPreview.ts`** (vscode layer) — `mode: Map` + (default `"on"`, replacing F9's `"changes"|"authorship"`); gains a + **`ProposalController` dependency** and subscribes to a F4 + `onDidChangeProposals` event (additive, mirroring F6's `onDidChangeBaseline`) + alongside the existing edit + baseline triggers; on the new + `{accept|reject, proposalId}` inbound message, calls + `ProposalController.accept`/`reject` then re-renders; owns the **status-bar + item** (PUC-6). +- **Webview asset** (`media/preview.ts` + `.css`) — header **on/off** switch + (replacing the segmented control); **inbound** click handler on `.cw-actions` + buttons → `postMessage({accept|reject, proposalId})` (nonce-gated inline + script, CSP-safe); proposal-block + ✓/✗ button CSS; reuses the existing + `mermaid.run()` swap. Stays sealed (INV-21). +- **F3 `AttributionController`** — remove the `render()` `setDecorations` calls + and dispose the decoration types; **keep** `spansFor`. Retire/hide + `cowriting.toggleAttribution`. +- **F4 `ProposalController`** — remove the in-editor proposal-thread decorations + and the in-editor accept/reject codelens/commands' UI; **keep** + `accept`/`reject`/`propose`. Add `listProposals(document): ProposalView[]` + (anchor offset + replacement + id) and `onDidChangeProposals` for the preview. +- **F6 `DiffViewController`** — unchanged code; `cowriting.toggleDiffView` + + `ctrl+alt+d` set `when:false` in `package.json`. + +### 6.3 Data model & ownership + +**No new persisted artifact** (INV-20). F10 holds only in-memory webview panels +keyed by document URI + the per-panel on/off mode + the status-bar item. The +baseline is owned by F6; proposals by F4 (sidecar); attribution by F3. The only +on-the-wire models are transient: + +```ts +// host → webview (transient HTML payload) +type RenderMsg = { + type: "render"; mode: "on" | "off"; html: string; + epoch?: string; summary?: { added: number; removed: number; proposals: number }; +}; +// webview → host (intent only — INV-34) +type ActionMsg = + | { type: "setMode"; mode: "on" | "off" } + | { type: "accept"; proposalId: string } + | { type: "reject"; proposalId: string }; + +// internal to renderReview (illustrative — not persisted) +type ProposalView = { id: string; anchorStart: number; anchorEnd: number; replacement: string }; +``` + +### 6.4 Interfaces & contracts + +- **`trackChangesModel`** (vscode-free): `renderReview(baselineText: string, + currentText: string, authorSpans: AuthorSpan[], proposals: ProposalView[]): + string` — annotated HTML for the on-state body; deterministic (INV-33). + `renderPlain(currentText: string): string` — off-state body. Exports + `colorByAuthor(html-or-blocks, spans)` (the salvaged F9 sentinel helper) for + unit tests. **Removes** the public `renderAuthorship` (superseded); + `renderTrackChanges` is retained internally / folded into `renderReview`. +- **`TrackChangesPreviewController`** (vscode layer): `show(document)`, + `refresh(document)` (debounced), `setMode(uriString, "on"|"off")` (test seam), + `getLastModel(uriString)` (test seam — the last computed block/proposal model, + so host E2E asserts marks without reading webview DOM). Constructor now takes + `AttributionController` **and** `ProposalController`. +- **`AttributionController`** (additive removal): `render()` no longer applies + editor decorations; `spansFor(document): AuthorSpan[]` unchanged. +- **`ProposalController`** (additive): `listProposals(document): ProposalView[]`; + `readonly onDidChangeProposals: vscode.Event<{ uri: string }>` fired on + propose/accept/reject. `accept`/`reject`/`applyAgentEdit` unchanged. +- **`DiffViewController`** (F6) — unchanged; only its `package.json` + command/keybinding `when` flips to `false`. +- **Commands / keybindings** (`package.json`): `cowriting.showTrackChangesPreview` + ("Cowriting: Open Review Preview", `ctrl+alt+r`, `when: editorLangId == + markdown`) — retitled, kept. `cowriting.toggleDiffView` + `ctrl+alt+d`, + `cowriting.toggleAttribution`, and the in-editor `cowriting.acceptProposal` / + `cowriting.rejectProposal` palette entries → `when:false` (hidden; the seams + stay for tests/programmatic use). Status-bar item registered in `extension.ts`. + +### 6.5 Per–Product-Use-Case design + +- **PUC-1 (clean editor):** delete `AttributionController.render`'s + `setDecorations` + decoration-type creation; remove F4's in-editor proposal + decorations. Editor shows plain text. +- **PUC-2 (toggle):** header switch → `{type:"setMode", mode}` → controller + updates the per-panel map → re-render. Off = `renderPlain`; on = `renderReview`. +- **PUC-3 (who changed what):** `renderReview` overlays the F7 baseline diff + (author-colored via F9 sentinels) + proposal blocks. +- **PUC-4 (accept):** `{type:"accept", id}` → `ProposalController.accept(state, + proposal)` → `applyAgentEdit` `WorkspaceEdit` (INV-9) → baseline advances + (INV-18) → `onDidChangeBaseline` + `onDidChangeProposals` → re-render (block now + ordinary text). +- **PUC-5 (reject):** `{type:"reject", id}` → `ProposalController.reject` → + proposal removed from sidecar → `onDidChangeProposals` → re-render (block gone); + document untouched. +- **PUC-6 (proposal while no preview):** on `onDidChangeProposals`, if no panel + is open for the doc, show/update a status-bar item with the pending count, + `command = cowriting.showTrackChangesPreview`. Hidden when count = 0 or a panel + is open. +- **PUC-7 (edges):** non-markdown → `showWarningMessage`, no panel. Per-block + `try/catch` → error chip (F7). No baseline → render with no change-marks + a + note; proposals still render. + +### 6.6 Non-functional requirements & cross-cutting concerns + +Render cost is O(document) on a debounced edit / proposal change — fine at +inner-loop scale (large-doc debounce/idle cap is the F7 tunable, not v1 +critical). The webview stays **sealed** (INV-21): local assets, strict CSP with a +per-load nonce, no network; mermaid lives only in the webview bundle. The ✓/✗ +buttons are host-rendered HTML with `data-proposal-id`; the nonce'd inline script +only reads those ids and posts intent (no eval, no remote). No telemetry, no LLM, +no credentials, nothing persisted. + +### 6.7 Key decisions & alternatives considered + +| Decision | Chosen | Alternatives rejected | +| --- | --- | --- | +| **Where Claude's reviewable changes come from** | **Pending F4 proposals surfaced in the preview** — keep propose-by-default; ✓ = F4 accept (apply + advance baseline), ✗ = F4 reject. | **Apply-then-review** (LLM edits land in the buffer; ✓/✗ pin/revert per span) — reverses F4 INV-10/11 and needs new per-span baseline machinery. **Both axes overlaid** (landed-LLM authorship + pending proposals + human diff) — richest but most rendering complexity; deferred. *(Operator decision, session 0029.)* | +| **Editor decoration scope** | **Fully clean** — strip F3 attribution **and** F4 proposal threads; proposals are **preview-only**. | **Strip F3 only, keep amber threads** — editor isn't truly "zero annotations"; two proposal surfaces to keep in sync. *(Operator decision, session 0029.)* | +| **F6 two-pane diff** | **Hide** command + `ctrl+alt+d` (`when:false`); keep the controller + baseline store. | **Delete the view code now** — larger, less reversible diff; a later cleanup PR can remove the dead view. *(Operator decision, session 0029.)* | +| **F9's two modes** | **Collapse** into one **on/off** toggle; on = combined render; off = plain markdown. Remove `renderAuthorship` + segmented control. | **Keep three modes** (changes / authorship / combined) — re-introduces the very surface-multiplicity F10 removes. | +| **Combined render approach** | **One pure `renderReview`** overlaying diff + author sentinels + proposal blocks (extends INV-22). | **Compose two HTML passes in the webview** — pushes logic into the sealed sandbox, loses unit-testability (INV-33). | +| **Proposal-while-no-preview** | **Status-bar indicator** (clickable to open). | **Force auto-open the preview** — hijacks focus, fights "write left, review right." *(Deferred decision — cheap to flip to auto-open.)* | +| **Deletion coloring** | **Neutral struck** (`cw-del`), per the issue's "strikethrough = deleted". | **Color the strikethrough by who deleted** — adds a fourth signal for marginal value. | +| **Non-markdown review** | **Dropped as a goal** (markdown-only product). | **Keep F6's any-file reach** — contradicts the locked product vision; rename to a markdown-specific identity is the eventual corollary (deferred). | + +### 6.8 Testing strategy + +- **Unit (vitest, vscode-free):** `renderReview` over fixtures — human addition + (green ``), human deletion (struck `cw-del`), a proposal **insert** (blue + `cw-proposal` block + `data-proposal-id` + ✓/✗ markup), a proposal **replace** + (struck old + proposed new in one block), mixed human+proposal in a paragraph, + an atomic code fence + a changed mermaid fence (F7.1 augmentation intact, no + inner sentinels), **off-mode** = plain markdown (`renderPlain` equals + markdown-it of source), determinism (same inputs → identical HTML). `colorByAuthor` + helper unit tests (salvaged F9 cases). Proposal-anchor resolution edge: an + unresolvable fingerprint → the proposal is rendered as a trailing block, never + dropped silently. +- **Host E2E (`@vscode/test-electron`, no LLM, extends the F7/F9 suite):** open a + markdown fixture → `cowriting.showTrackChangesPreview` → assert panel open + + `getLastModel` (opened baseline → no change-marks). Type → an `added` block, + author = human. `proposeAgentEdit` seam → `getLastModel` shows a proposal op + with id; **simulate `{type:"accept", id}`** → proposal applied, baseline + advanced, block now `unchanged`/ordinary; **`{type:"reject", id}`** → proposal + gone, document text unchanged. Assert the **editor has no decorations** (the F3 + decoration types are not created / applied). Assert `toggleDiffView` is not in + the active command set (or its `when` is false). Non-markdown doc → command + warns, no panel. Status-bar item appears on a proposal with no panel open. + Webview DOM / real button clicks are **not** E2E-asserted (sealed sandbox) — + manual smoke. +- **Live smoke (manual — `docs/MANUAL-SMOKE-F10.md`):** open a markdown doc; + confirm the editor is clean (no tint/threads); edit prose (green ins / struck + del); ask Claude to edit a selection → a blue proposal block with ✓/✗ appears + in the preview; click ✓ (lands, mark clears) and ✗ on another (block vanishes, + doc unchanged); toggle annotations off (clean render) / on; verify the + status-bar indicator with no preview open; verify light/dark theming and that + `git status` shows nothing. + +### 6.9 Failure modes, rollback & flags + +`markdown-it`/`mermaid` throwing on a block → inline error chip (F7), preview +still renders. A proposal whose fingerprint no longer resolves → rendered as a +trailing "unanchored proposal" block with ✓/✗ (never silently dropped — INV-34 +must remain actionable). Webview disposed → controller drops the panel + clears +the status-bar item; re-run the command to reopen. **No feature flag** — the +preview is a pure read-only view (INV-20); not opening it is the off state. +Rollback is reverting the PR with **zero** data migration (nothing persisted; the +F6 baseline, F4 sidecar, F3 attribution data are untouched, so the hidden/removed +UI can be restored). The clean-editor change is mostly deletion of decoration +calls — trivially reversible. + +--- + +## 7. Delivery Plan + +### 7.1 Approach / strategy + +One planning-and-executing session (F10 = `#29`), plan written just-in-time from +this spec — the F2–F9 precedent. Host-E2E tier (a VS Code extension has no +browser/deploy stage); no LLM in CI. The webview's *visual* rendering and real +button clicks are verified by the manual smoke; the automated seams are the pure +render **model** and the host's accept/reject wiring. + +### 7.2 Slicing plan + +- **SLICE-1 — Clean editor.** Strip F3 attribution decorations + (`AttributionController.render`) + F4 in-editor proposal threads; hide + `toggleDiffView`/`ctrl+alt+d` + `toggleAttribution` (`when:false`). Host E2E: + no editor decorations. *(Mostly deletion.)* +- **SLICE-2 — Combined render engine.** `renderReview` + `renderPlain` in + `trackChangesModel.ts`; salvage `colorByAuthor` from `renderAuthorship`; remove + the public `renderAuthorship`; proposal-block emission + anchor resolution; + vitest suite (§6.8). Pure, vscode-free (INV-33). +- **SLICE-3 — Interactive controller + webview.** `ProposalController.listProposals` + + `onDidChangeProposals`; wire `ProposalController` + `AttributionController` + into `TrackChangesPreviewController`; collapse mode to on/off; inbound + `accept`/`reject`/`setMode` handling + re-render; webview on/off switch + ✓/✗ + click→postMessage + proposal/button CSS; status-bar indicator (PUC-6). +- **SLICE-4 — Tests & docs.** Host E2E (open / toggle / propose→accept→reject / + clean-editor / hidden-F6 / status-bar) + `docs/MANUAL-SMOKE-F10.md` + README + F10 section (and a note that F6/F9 user surfaces are superseded). + +E2E are first-class plan tasks (handbook §9/§4); this app's required tier is host +E2E (the F2–F9 precedent). + +### 7.3 Rollout / launch plan + +Non-shippable (no marketplace publish). "Done" = `#29` acceptance: a clean +editor; the preview is the single review surface with an annotations on/off +toggle; on-state paints green human / blue LLM / struck deletions and renders +Claude's pending proposals with working ✓/✗ that land/discard via the F4 seam; +unit + host E2E green; live smoke performed once. The repo rename remains +deferred. + +### 7.4 Risks & mitigations + +| Risk | Mitigation | +| --- | --- | +| Proposals being preview-only hides pending work when no preview is open | Status-bar indicator (PUC-6); README documents "review happens in the preview"; auto-open is a cheap fallback if it proves insufficient | +| Overlaying author sentinels onto the diff's ``/`` perturbs markdown-it inline parsing (F9's known edge) | Per-block `try/catch` → error chip (no hard failure); fences atomic; covered by the common-case tests; intra-emphasis hardening stays deferred | +| Proposal anchor can't be resolved at render time | Render as a trailing "unanchored proposal" block (never dropped); unit-tested | +| Removing F4's in-editor threads loses a familiar affordance | The ✓/✗ move to the preview (the single review surface by design); status-bar signals presence | +| Two reversed decisions (F9 INV-26; F6 any-file `#19`) confuse future readers | This spec records the supersession explicitly (§6.1, §6.7); README updated | +| Webview interactivity widens the attack surface | Nonce-gated inline script reads only `data-proposal-id` and posts intent; CSP unchanged; host validates the id against live proposals before acting (INV-34) | + +--- + +## 8. Traceability matrix + +| Requirement (#29) | Use case | Design | Slice | +| --- | --- | --- | --- | +| Editor carries no coauthoring decorations | PUC-1 | INV-32, §6.2 | SLICE-1 | +| Preview annotations on/off toggle (off = clean render) | PUC-2 | §6.2, §6.4 | SLICE-3 | +| One language: green human / blue LLM / struck deleted | PUC-3 | INV-33, §6.2 (renderReview) | SLICE-2 | +| ✓/✗ on Claude's changes only; ✓ keeps, ✗ reverts | PUC-4/5 | INV-34, §6.5 (F4 seam) | SLICE-2/3 | +| Accept/reject updates doc + preview consistently | PUC-4/5 | §6.5, INV-18 reuse | SLICE-3 | +| Deletions reconstructed from the F6 baseline | PUC-3 | F7 reuse, §6.2 | SLICE-2 | +| Deprecate F6 as a view; keep baseline data layer | — | §6.2, §6.7 | SLICE-1 | +| Markdown-only; no sidecar/contract/SCHEMA change | — | §1.7, INV-20 | all | +| Sealed webview, no network/LLM | — | INV-21, §6.6 | SLICE-3 | +| Unit + host E2E, no LLM in CI | — | §6.8 | SLICE-1..4 | + +## 9. Open Questions & Decisions log + +- **RESOLVED (session 0029, operator):** Claude's reviewable changes = **pending + F4 proposals surfaced in the preview** (propose-by-default kept; ✓ = F4 accept + + baseline advance, ✗ = F4 reject); editor decoration scope = **fully clean** + (strip F3 attribution **and** F4 proposal threads — proposals preview-only); F6 + two-pane diff = **hidden** (`when:false`, code/baseline kept). +- **RESOLVED (this spec, autonomous — cheap to revisit):** proposal-while-no-preview + → **status-bar indicator** (not forced auto-open); F9's dead `renderAuthorship` + / segmented control → **removed** (the PUA-sentinel coloring salvaged into a + shared `colorByAuthor` helper); deletions → **neutral** strikethrough. +- **SUPERSEDED:** F9 INV-26 ("authorship never combined with the diff") — F10 + combines them by design; F9's authorship mode + segmented control are removed. + F6's any-file review reach (`#19`) is dropped as a user goal (markdown-only). +- **OPEN → later:** the repo rename to `vscode-markdown-cowriting-plugin` + (deliberate, deferred); preview→source scroll-sync; intra-emphasis + sentinel-safety hardening; whether to eventually overlay *landed*-LLM authorship + (F9's old axis) in the on-state too (the "both axes" option, deferred); deleting + F6's two-pane view code in a later cleanup PR. + +## 10. Glossary & References + +- **Review preview** — the rendered markdown preview as the single review + surface: clean (off) or annotated (on). **Annotations on/off** — the per-panel + toggle replacing F9's two modes. **Combined render** — `renderReview`'s + single-pass overlay of changes-since-baseline (author-colored + struck) and + pending proposals. **Pending proposal** — an F4 propose-by-default edit, not yet + in the buffer, rendered blue with ✓/✗. **Baseline / epoch / advance** — as F6 + (`coauthoring-diff-view.md`). **`spansFor`** — F3's author-span data layer + (`coauthoring-attribution.md`). **PUA sentinels / `colorByAuthor`** — F9's + author-coloring technique (`2026-06-11-authorship-preview-design.md`), salvaged + here. +- Feature `#29` (F10) · Epic `#1` · builds on F3 `#6`, F4 `#12`, F6 `#17`/`#19`, + F7 `#21`/`#22`, F9 `#27` · parent specs `coauthoring-inner-loop.md`, + `coauthoring-attribution.md`, `coauthoring-propose-accept.md`, + `coauthoring-diff-view.md`, `coauthoring-rendered-preview.md`, + `2026-06-11-authorship-preview-design.md` · capture session 0028 · lineage + `ben.stull/rfc-app#48`.