--- 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`.