Files
vscode-cowriting-plugin-con…/specs/coauthoring-interactive-review.md
T

610 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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`.