add spec ./specs/coauthoring-interactive-review.md (status: graduated)

This commit is contained in:
Ben Stull
2026-06-11 21:52:37 -07:00
parent 6b14de9d8e
commit 118bc293ad
+609
View File
@@ -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 F2F5 (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 F2F9);
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
`<ins>`), themeable.
- **LLM-authored text** (already landed, attributed by F3) — soft blue
(`cw-by-claude`).
- **Deletions** — struck-through, muted (`<del class="cw-del">`), 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 <epoch>` 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 `<ins>`/`<del>`,
**then** author-color each `<ins>` (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
`<div class="cw-proposal" data-proposal-id="…">` containing the struck
replaced-text and the proposed text + a `<span class="cw-actions">✓ ✗</span>`
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<key, "on"|"off">`
(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 PerProduct-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 `<ins>`), 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 F2F9 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 F2F9 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 `<ins>`/`<del>` 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`.