add spec ./specs/coauthoring-interactive-review.md (status: graduated)
This commit is contained in:
@@ -0,0 +1,609 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
---
|
||||||
|
# Solution Design: Interactive Track-Changes Review in the Markdown Preview (F10)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Author(s)** | Ben Stull (with Claude) |
|
||||||
|
| **Reviewers / approvers** | Ben Stull |
|
||||||
|
| **Status** | `draft` |
|
||||||
|
| **Version** | v0.1.0 |
|
||||||
|
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#29` (F10, `type/feature`, `priority/P1`) · Epic `#1` (closed) · Capture session `vscode-cowriting-plugin-0028` (2026-06-11) · Brainstorming session `vscode-cowriting-plugin-0029` · Builds on (all shipped): F3 `#6` (live attribution), F4 `#12` (propose/accept), F6 `#17`/`#19` (baseline + diff view), F7 `#21`/`#22` (rendered preview + intra-diagram mermaid), F9 `#27` (authorship preview) · Parent specs (graduated): `coauthoring-inner-loop.md`, `coauthoring-attribution.md`, `coauthoring-propose-accept.md`, `coauthoring-diff-view.md`, `coauthoring-rendered-preview.md`, `2026-06-11-authorship-preview-design.md` · Lineage: `ben.stull/rfc-app#48` |
|
||||||
|
|
||||||
|
**Change log**
|
||||||
|
|
||||||
|
| Date | Version | Change | By |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-06-11 | v0.1.0 | Initial draft — brainstorming session 0029 (from the capture in session 0028, which followed the session-0027 ideation where the operator found the F6 two-pane diff + F3 attribution overlap confusing). | Ben Stull + Claude |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Business Context
|
||||||
|
|
||||||
|
### 1.1 Executive Summary
|
||||||
|
|
||||||
|
The plugin shipped its review signals as **several overlapping surfaces that
|
||||||
|
collide on the same text**: F6's two-pane red/green raw diff (`ctrl+alt+d`), F3's
|
||||||
|
in-editor attribution decorations (green human border / blue Claude fill), F4's
|
||||||
|
in-editor amber proposal threads, and F7/F9's rendered preview (two modes). In
|
||||||
|
manual testing the operator opened the F6 diff and saw F3's blue attribution
|
||||||
|
bleeding into it at the same time — *"I don't understand this."*
|
||||||
|
|
||||||
|
F10 resolves this into **one mental model: write on the left, review on the
|
||||||
|
right.** The editor becomes a **clean, zero-annotation** markdown pane. The
|
||||||
|
**rendered preview becomes the single review surface**, with an **annotations
|
||||||
|
on/off** toggle. With annotations **on**, the preview paints every change since
|
||||||
|
the F6 baseline in one visual language — **green = human-authored, blue =
|
||||||
|
LLM-authored, strikethrough = deleted** — and lets the human **accept (✓) or
|
||||||
|
reject (✗) Claude's pending proposals, and only those, right inside the
|
||||||
|
preview.** F10 is **mostly assembly** of shipped features (F3/F4/F6/F7/F9) plus
|
||||||
|
three new pieces: strip the editor decorations, collapse F9's two preview modes
|
||||||
|
into the one toggle, and make the preview interactive.
|
||||||
|
|
||||||
|
### 1.2 Background
|
||||||
|
|
||||||
|
The inner loop shipped F2–F5 (threads · attribution · propose/accept ·
|
||||||
|
cross-rung). F6 added the baseline + a native two-pane diff toggle (`#17`),
|
||||||
|
broadened to any file (`#19`). F7 added the rendered track-changes preview
|
||||||
|
(`#21`) with full mermaid support, refined by F7.1 intra-diagram mermaid diffing
|
||||||
|
(`#22`). F9 added an **Authorship** mode to that preview (`#27`), switched by a
|
||||||
|
segmented `[ Track changes | Authorship ]` header control — and F9 **explicitly
|
||||||
|
chose two separate modes, never combined** (F9 INV-26).
|
||||||
|
|
||||||
|
The friction the operator hit is the *multiplicity* of surfaces, not any one of
|
||||||
|
them: the two-pane raw diff is low-altitude for prose; the in-editor decorations
|
||||||
|
clutter the writing surface (the human authored most of the document, so
|
||||||
|
attribution paints everywhere); and there is no single place to answer "what
|
||||||
|
changed, who changed it, and do I keep the LLM's edits?" Capture session 0028
|
||||||
|
filed Feature `#29` locking the product vision (markdown-only; clean editor;
|
||||||
|
preview as the single review surface). This spec is the Solution Design for it.
|
||||||
|
|
||||||
|
### 1.3 Business Actors / Roles
|
||||||
|
|
||||||
|
- **Coauthor (human)** — the writer/engineer (PP-1); F10's sole user.
|
||||||
|
- **Coauthor (machine)** — Claude via `@cline/sdk`; not a user of F10, but its
|
||||||
|
proposals are what the human reviews in the preview, and its landings advance
|
||||||
|
the F6 baseline.
|
||||||
|
|
||||||
|
### 1.4 Problem Statement
|
||||||
|
|
||||||
|
A markdown writer reviewing a coauthoring session has **no single, intuitive
|
||||||
|
surface** that answers "what changed since the last coauthoring moment, who
|
||||||
|
changed it, and do I keep Claude's edits?" The signal is fragmented across a raw
|
||||||
|
two-pane diff, in-editor decorations, and a two-mode preview that shows changes
|
||||||
|
**or** authorship but never both, and never lets the human act on Claude's
|
||||||
|
proposals from inside the rendered view.
|
||||||
|
|
||||||
|
### 1.5 Pain Points
|
||||||
|
|
||||||
|
- **Surface collision** — F6's raw diff and F3's attribution paint the same text
|
||||||
|
simultaneously and read as noise ("I don't understand this").
|
||||||
|
- **Cluttered editor** — F3 attribution + F4 proposal threads decorate the
|
||||||
|
writing pane; since the human authored most of the document, attribution paints
|
||||||
|
nearly everywhere.
|
||||||
|
- **Two-pane raw diff is low-altitude for prose** — you read markdown source, not
|
||||||
|
the rendered document.
|
||||||
|
- **Changes XOR authorship** — F9's preview shows *what changed* (track-changes)
|
||||||
|
or *who authored* (authorship) but never both in one glance, and neither lets
|
||||||
|
you accept/reject from the rendered view.
|
||||||
|
|
||||||
|
### 1.6 Targeted Business Outcomes
|
||||||
|
|
||||||
|
One clean model: **write on the left, review on the right.** The editor is
|
||||||
|
distraction-free (zero annotations). The preview, with annotations on, shows
|
||||||
|
every change since the baseline in one visual language **and** lets the human
|
||||||
|
keep or revert Claude's proposals without leaving that pane — so "what did the
|
||||||
|
LLM change, and do I want it?" is answered **in place**, in the rendered
|
||||||
|
document, not in a raw diff or a cluttered editor.
|
||||||
|
|
||||||
|
### 1.7 Scope (business)
|
||||||
|
|
||||||
|
**In scope:** a clean (zero-annotation) markdown editor; the rendered preview as
|
||||||
|
the **single** review surface; an **annotations on/off** toggle (off = clean
|
||||||
|
rendered markdown, built-in-preview parity; on = the combined annotated view);
|
||||||
|
the unified per-author + strikethrough annotation language (green human / blue
|
||||||
|
LLM / struck deletions, reconstructed from the F6 baseline); **interactive
|
||||||
|
accept/reject of Claude's pending proposals from inside the preview** (✓/✗);
|
||||||
|
deprecating F6's two-pane view (command + keybinding hidden) and stripping the
|
||||||
|
editor's F3/F4 decorations; unit + host-E2E coverage; manual webview smoke.
|
||||||
|
|
||||||
|
**Out of scope (deferred, not forgotten):** **non-markdown review** (F6's
|
||||||
|
any-file reach `#19` is dropped *as a user goal* — see §6.7); the **repo rename**
|
||||||
|
to `vscode-markdown-cowriting-plugin` (the markdown-only identity is affirmed,
|
||||||
|
but the rename — Gitea repo, `app.json`, content repo, session-history paths — is
|
||||||
|
a separate deliberate gesture); preview→source **scroll-sync**; export / print /
|
||||||
|
copy-as-clean; **editing in the preview** beyond the ✓/✗ proposal controls;
|
||||||
|
intra-emphasis sentinel hardening (the F9 deferred refinement).
|
||||||
|
|
||||||
|
**Non-goals (firm):** any change to the **sidecar** persistence, the **F4
|
||||||
|
propose/accept model** (proposals stay pending-by-default — F10 surfaces and acts
|
||||||
|
on them, it does **not** change how Claude's edits enter the system), the
|
||||||
|
**cross-rung contract**, or `SCHEMA_VERSION`; a general-purpose markdown
|
||||||
|
previewer; a WYSIWYG editor; combining anything with the LLM/network/credential
|
||||||
|
surface (INV-8 untouched).
|
||||||
|
|
||||||
|
### 1.8 Assumptions · Constraints · Dependencies
|
||||||
|
|
||||||
|
- **Anchor:** Feature `#29` (F10). **Mostly assembly** of shipped features:
|
||||||
|
- **F6 baseline** (`#17`/`#19`) — kept as the **data layer** ("what changed
|
||||||
|
since when"; auto-advances at every machine landing, INV-18); F6's two-pane
|
||||||
|
*view* is deprecated as a user surface (command/keybinding hidden, code kept).
|
||||||
|
- **F7 render/diff engine** (`#21`/`#22`) — the rendered preview, block/word
|
||||||
|
diff, strikethrough deletions, atomic code/mermaid, and F7.1 intra-diagram
|
||||||
|
mermaid diff already exist and are reused.
|
||||||
|
- **F3 attribution** (`#6`) — kept as the **data layer** (`spansFor` — who
|
||||||
|
authored each span); its *in-editor decorations* are removed.
|
||||||
|
- **F4 propose/accept** (`#12`) — the accept/reject *logic*
|
||||||
|
(`ProposalController.accept`/`reject` → `applyAgentEdit`, the INV-9
|
||||||
|
`WorkspaceEdit` seam) already exists; F10 moves the *controls* into the
|
||||||
|
preview and removes the in-editor proposal threads.
|
||||||
|
- **F9 authorship preview** (`#27`) — its two preview modes **collapse** into
|
||||||
|
the one annotations on/off toggle; its PUA-sentinel author-coloring technique
|
||||||
|
is **reused** inside the new combined render; its standalone
|
||||||
|
`renderAuthorship` / segmented control are **superseded** (removed).
|
||||||
|
- **Constraint:** the preview is a **sealed webview** (strict CSP, local assets,
|
||||||
|
no network — F7 INV-21). Interactive ✓/✗ controls post messages to the
|
||||||
|
extension host, which applies/discards proposals via the F4 path; the webview
|
||||||
|
**never** edits the document directly.
|
||||||
|
- **Constraint:** deletions aren't in the live buffer — they are reconstructed by
|
||||||
|
diffing the buffer against the F6 baseline (the F7 engine already does this).
|
||||||
|
- No LLM, no network, no new credential surface (INV-8 untouched); nothing
|
||||||
|
persisted changes (`.threads/`, the contract, `SCHEMA_VERSION` all untouched).
|
||||||
|
|
||||||
|
### 1.9 Business Use Cases
|
||||||
|
|
||||||
|
- **BUC-1 (unified review)** Mid-session the writer opens the preview and sees the
|
||||||
|
rendered chapter with their own additions in green, their deletions struck, and
|
||||||
|
Claude's pending proposals as blue blocks carrying ✓/✗ — one glance answers
|
||||||
|
"what changed, who, and do I keep Claude's edits?"
|
||||||
|
- **BUC-2 (act in place)** The writer clicks ✓ on a Claude proposal in the
|
||||||
|
preview; the proposal lands in the document, the baseline advances, and the
|
||||||
|
blue block becomes ordinary (unmarked) text — without leaving the preview or
|
||||||
|
touching the editor.
|
||||||
|
- **BUC-3 (clean writing)** The writer works in the editor, which shows **no**
|
||||||
|
coauthoring decorations at all — a plain markdown surface — and flips
|
||||||
|
annotations **off** in the preview to read the document clean (built-in-preview
|
||||||
|
parity) when they want to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Solution Proposal
|
||||||
|
|
||||||
|
The editor is stripped of **all** coauthoring decorations (F3 attribution tint,
|
||||||
|
F4 proposal threads), and F6's two-pane diff command/keybinding are hidden
|
||||||
|
(`when:false`); the F6 baseline store and `onDidChangeBaseline` event remain as
|
||||||
|
the data layer.
|
||||||
|
|
||||||
|
The preview's per-panel mode collapses from F9's `"changes" | "authorship"` to a
|
||||||
|
single **`"on" | "off"`** annotations toggle (default **on**):
|
||||||
|
|
||||||
|
- **Off** — plain `markdown-it` render of the current buffer, no marks (parity
|
||||||
|
with VS Code's built-in markdown preview).
|
||||||
|
- **On** — a **combined annotated render** produced by one new **pure,
|
||||||
|
vscode-free** engine, `renderReview(baselineText, currentText, authorSpans,
|
||||||
|
proposals)`, that overlays **two axes in a single pass**:
|
||||||
|
1. **Changes since the F6 baseline** — the F7 block + word diff of
|
||||||
|
`baselineText` vs `currentText`: additions and deletions, **author-colored**
|
||||||
|
via F3 `authorSpans` (human → green, agent → blue) using F9's PUA-sentinel
|
||||||
|
technique; deletions rendered **struck** (reconstructed from the baseline).
|
||||||
|
2. **Pending Claude proposals** — each F4 pending proposal rendered **inline at
|
||||||
|
its resolved anchor** as a **blue block** (the replaced text struck, the
|
||||||
|
proposed text shown) carrying a `data-proposal-id` and **✓/✗ buttons**.
|
||||||
|
|
||||||
|
The webview becomes **interactive**: clicking ✓/✗ posts `{type: "accept" |
|
||||||
|
"reject", proposalId}` to the host, which calls `ProposalController.accept` /
|
||||||
|
`reject` — accept runs the existing `applyAgentEdit` `WorkspaceEdit` seam and
|
||||||
|
advances the baseline (INV-18), reject discards the proposal; either way the host
|
||||||
|
re-renders. The webview never mutates the document (INV-20/21 hold). Everything
|
||||||
|
downstream of `(baseline, current, spans, proposals) → HTML` is a pure function
|
||||||
|
(INV-33), unit-testable with no vscode and no webview.
|
||||||
|
|
||||||
|
Because proposals are now **preview-only**, a small **status-bar indicator**
|
||||||
|
("N Claude proposals — open review", clickable to open the preview) signals
|
||||||
|
pending proposals when no preview is open (§6.5 PUC-6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Product Personas
|
||||||
|
|
||||||
|
- **PP-1 Inner-loop coauthor** — the human markdown writer/engineer (as F2–F9);
|
||||||
|
the only persona F10 serves.
|
||||||
|
|
||||||
|
## 4. Product Use Cases
|
||||||
|
|
||||||
|
- **PUC-1 (clean editor)** Opening / editing a markdown document shows a plain
|
||||||
|
editor with **no** coauthoring decorations (no F3 tint, no F4 threads, no F6
|
||||||
|
diff). The writing surface is distraction-free.
|
||||||
|
- **PUC-2 (toggle annotations)** In the preview, a header switch flips
|
||||||
|
annotations **on** (combined annotated render) / **off** (clean rendered
|
||||||
|
markdown). Default on. Mode is remembered per panel.
|
||||||
|
- **PUC-3 (see who changed what)** With annotations on, the writer's additions
|
||||||
|
since the baseline render **green**, their deletions render **struck**, and
|
||||||
|
Claude's pending proposals render as **blue** blocks. One visual language.
|
||||||
|
- **PUC-4 (accept a proposal)** The writer clicks **✓** on a blue proposal → it
|
||||||
|
lands in the document via the F4 seam → the baseline advances → the block
|
||||||
|
re-renders as ordinary (unmarked) text. The editor reflects the new text, still
|
||||||
|
with no decorations.
|
||||||
|
- **PUC-5 (reject a proposal)** The writer clicks **✗** on a blue proposal → the
|
||||||
|
proposal is discarded → the block disappears on re-render → the document is
|
||||||
|
unchanged.
|
||||||
|
- **PUC-6 (proposal while no preview open)** Claude proposes while no preview is
|
||||||
|
open → a **status-bar item** ("N Claude proposals — open review") appears;
|
||||||
|
clicking it opens the preview where the proposals are reviewable. (No forced
|
||||||
|
auto-open — the writer stays in control of focus.)
|
||||||
|
- **PUC-7 (graceful edges)** Non-markdown active editor → the open command warns
|
||||||
|
and opens nothing (markdown-only). A malformed markdown/mermaid block → an
|
||||||
|
inline error chip for that block, the rest renders. No baseline yet → render
|
||||||
|
current doc with **no** change-marks (everything is "since opened") + a header
|
||||||
|
note; proposals (if any) still render with ✓/✗.
|
||||||
|
|
||||||
|
## 5. UX Layout
|
||||||
|
|
||||||
|
A webview in the editor column **beside** the source (`ViewColumn.Beside`). The
|
||||||
|
source editor stays fully editable but is now **visually clean** — no coauthoring
|
||||||
|
decorations. The preview is **read-only rendered output** except for the ✓/✗
|
||||||
|
proposal controls, which post intent (never edit the doc directly). With
|
||||||
|
annotations **on**:
|
||||||
|
|
||||||
|
- **Human additions** since baseline — soft green background (`cw-by-human` over
|
||||||
|
`<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 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 `<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 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 `<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`.
|
||||||
Reference in New Issue
Block a user