37 KiB
status
| 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-9WorkspaceEditseam) 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 standalonerenderAuthorship/ segmented control are superseded (removed).
- F6 baseline (
- 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_VERSIONall 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-itrender 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:- Changes since the F6 baseline — the F7 block + word diff of
baselineTextvscurrentText: additions and deletions, author-colored via F3authorSpans(human → green, agent → blue) using F9's PUA-sentinel technique; deletions rendered struck (reconstructed from the baseline). - 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-idand ✓/✗ buttons.
- Changes since the F6 baseline — the F7 block + word diff of
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-humanover<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-idon 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 proposalssummary 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 plainmarkdown-itof 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 F4ProposalController.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.
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) gainsrenderReview(baselineText, currentText, authorSpans, proposals): string. Internals: (1) the existing F7diffBlocksover baseline/current → block ops; (2) for changed/added prose, the existing word-level<ins>/<del>, then author-color each<ins>(and live text) by intersectingauthorSpansvia the F9 PUA-sentinel technique (refactored out ofrenderAuthorshipinto a sharedcolorByAuthorhelper); (3) inject proposal blocks: for each proposal, resolve its anchor offset incurrentText, 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 thinrenderPlain(currentText)(markdown-it only).trackChangesPreview.ts(vscode layer) —mode: Map<key, "on"|"off">(default"on", replacing F9's"changes"|"authorship"); gains aProposalControllerdependency and subscribes to a F4onDidChangeProposalsevent (additive, mirroring F6'sonDidChangeBaseline) alongside the existing edit + baseline triggers; on the new{accept|reject, proposalId}inbound message, callsProposalController.accept/rejectthen 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-actionsbuttons →postMessage({accept|reject, proposalId})(nonce-gated inline script, CSP-safe); proposal-block + ✓/✗ button CSS; reuses the existingmermaid.run()swap. Stays sealed (INV-21). - F3
AttributionController— remove therender()setDecorationscalls and dispose the decoration types; keepspansFor. Retire/hidecowriting.toggleAttribution. - F4
ProposalController— remove the in-editor proposal-thread decorations and the in-editor accept/reject codelens/commands' UI; keepaccept/reject/propose. AddlistProposals(document): ProposalView[](anchor offset + replacement + id) andonDidChangeProposalsfor the preview. - F6
DiffViewController— unchanged code;cowriting.toggleDiffView+ctrl+alt+dsetwhen:falseinpackage.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:
// 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. ExportscolorByAuthor(html-or-blocks, spans)(the salvaged F9 sentinel helper) for unit tests. Removes the publicrenderAuthorship(superseded);renderTrackChangesis retained internally / folded intorenderReview.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 takesAttributionControllerandProposalController.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/applyAgentEditunchanged.DiffViewController(F6) — unchanged; only itspackage.jsoncommand/keybindingwhenflips tofalse.- 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-editorcowriting.acceptProposal/cowriting.rejectProposalpalette entries →when:false(hidden; the seams stay for tests/programmatic use). Status-bar item registered inextension.ts.
6.5 Per–Product-Use-Case design
- PUC-1 (clean editor): delete
AttributionController.render'ssetDecorations+ 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):
renderReviewoverlays the F7 baseline diff (author-colored via F9 sentinels) + proposal blocks. - PUC-4 (accept):
{type:"accept", id}→ProposalController.accept(state, proposal)→applyAgentEditWorkspaceEdit(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-blocktry/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):
renderReviewover fixtures — human addition (green<ins>), human deletion (struckcw-del), a proposal insert (bluecw-proposalblock +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 (renderPlainequals markdown-it of source), determinism (same inputs → identical HTML).colorByAuthorhelper 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 → anaddedblock, author = human.proposeAgentEditseam →getLastModelshows a proposal op with id; simulate{type:"accept", id}→ proposal applied, baseline advanced, block nowunchanged/ordinary;{type:"reject", id}→ proposal gone, document text unchanged. Assert the editor has no decorations (the F3 decoration types are not created / applied). AsserttoggleDiffViewis not in the active command set (or itswhenis 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 thatgit statusshows 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; hidetoggleDiffView/ctrl+alt+d+toggleAttribution(when:false). Host E2E: no editor decorations. (Mostly deletion.) - SLICE-2 — Combined render engine.
renderReview+renderPlainintrackChangesModel.ts; salvagecolorByAuthorfromrenderAuthorship; remove the publicrenderAuthorship; proposal-block emission + anchor resolution; vitest suite (§6.8). Pure, vscode-free (INV-33). - SLICE-3 — Interactive controller + webview.
ProposalController.listProposalsonDidChangeProposals; wireProposalController+AttributionControllerintoTrackChangesPreviewController; collapse mode to on/off; inboundaccept/reject/setModehandling + 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 sharedcolorByAuthorhelper); 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 specscoauthoring-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 · lineageben.stull/rfc-app#48.