Files
vscode-cowriting-plugin-con…/specs/coauthoring-rendered-preview.md
T
Ben Stull 1c2844d12b spec(f7.1): intra-diagram mermaid diffing design + ship note (#22)
Refine the F7 whole-diagram "changed" badge into a node/edge-level diff inside
flowchart and sequence diagrams against the F6 baseline. Parsed-graph diff
re-emitted with mermaid's own styling directives (classDef/class/linkStyle for
flowcharts; rect-tinted runs for sequence), removed elements ghosted in place,
layout reflow accepted, all else (other types, parse failure) falling back to
the v1 badge. New §11 + INV-29..31. Shipped via PR #28 (session 0027).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:39:13 -07:00

606 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: graduated
---
# Solution Design: Rendered Track-Changes Preview (F7)
| | |
| --- | --- |
| **Author(s)** | Ben Stull (with Claude) |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `draft` |
| **Version** | v0.1.0 |
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#21` (F7) · Deferred task `#22` (intra-diagram mermaid diffing) · Epic `#1` · Builds on F6 `#17` (PR #18) + `#19` (PR #20) · Parent specs: `coauthoring-diff-view.md` (F6, graduated), `coauthoring-inner-loop.md`, `coauthoring-attribution.md`, `coauthoring-propose-accept.md` · Lineage: `ben.stull/rfc-app#48` |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-11 | v0.1.0 | Initial draft — brainstorming session 0020 (from the F6-follow-up ideation in session 0019) | Ben Stull + Claude |
---
## 1. Business Context
### 1.1 Executive Summary
F6 gave the human coauthor a *diff* of their own changes since a coauthoring
baseline — but as a two-pane native `vscode.diff` of **raw text**, with only the
right pane editable. For prose, that's the wrong altitude: a writer wants to see
the **rendered** document and what changed in it, the way "track changes" /
"suggesting mode" works in Word or Google Docs — not a raw-markdown split-diff.
F7 adds a **rendered track-changes preview**: a webview, opened beside the normal
(fully editable) source editor, that renders the markdown document and marks
**what changed since the F6 baseline** — additions highlighted, deletions struck
through — updating live as the human and Claude edit. It has **full mermaid
support**: fenced `mermaid` diagrams render as diagrams, and a diagram that
changed since the baseline carries a "changed" badge (intra-diagram diffing is a
deferred follow-up, #22). F7 **reuses the F6 baseline** as its "before" and adds
no new persistence: it is a pure, read-only view (INV-20).
### 1.2 Background
The inner loop shipped F2F5 (threads · attribution · propose/accept ·
cross-rung), F6 added the baseline + a native diff toggle (#17), and #19
broadened F6 to any file. In manual testing the operator found the raw two-pane
diff unintuitive for prose ("the diff view isn't intuitive, and only the right
document being editable isn't either") and asked for a single, intuitive surface
that shows the rendered doc and its changes. The ideation (session 0019) compared
a single-pane change gutter, inline track-changes marks, and a **rendered
preview with change highlights**; the operator chose the rendered preview and
locked the architecture (custom webview; `markdown-it` + `mermaid` + `diff`
libraries; block-level diff with atomic code/mermaid blocks). Those decisions are
recorded in #21 and are the inputs to this design.
### 1.3 Business Actors / Roles
- **Coauthor (human)** — the writer/engineer (PP-1); F7's sole user.
- **Coauthor (machine)** — Claude via `@cline/sdk`; not a user of F7, but its
landings advance the F6 baseline F7 renders against.
### 1.4 Problem Statement
A markdown writer cannot see, in the *rendered* document, what they (and Claude)
changed since the last coauthoring moment. F6's diff is raw text in a split pane;
the only "rendered" view (VS Code's built-in preview) shows the current document
with no notion of a baseline or what changed.
### 1.5 Pain Points
- Reviewing prose changes as raw-markdown diff hunks is low-altitude and noisy
(markup syntax, reflowed lines) — you read the source, not the document.
- Mermaid diagrams, tables, headings, lists — the things a render *clarifies*
are exactly where a raw diff is least legible.
- The two-pane, right-only-editable diff is an unfamiliar editing posture.
### 1.6 Targeted Business Outcomes
Open one preview beside the document; see the rendered doc with your changes
since the baseline marked inline (additions highlighted, deletions struck);
keep editing the normal source pane (where you and Claude work) and watch the
preview update live. Self-review of prose becomes a glance at a familiar
"track-changes" rendering.
### 1.7 Scope (business)
**In scope:** a command that opens a **rendered track-changes preview** webview
beside the active **markdown** document; rendering the document via `markdown-it`
with **full mermaid** diagram support; computing a **block-level** diff against
the F6 baseline (word-level refinement inside changed prose blocks); marking
added / removed / changed blocks and inline ins/del; a **whole-diagram "changed"
badge** for changed/added mermaid (and code) blocks; live update on edit
(debounced) and on baseline advance/pin; bundling mermaid as a **webview-local**
asset (no CDN); unit + host-E2E coverage of the pure render model; manual smoke.
**Out of scope (deferred, not forgotten):** **intra-diagram mermaid diffing**
(#22 — v1 uses the whole-diagram badge); preview→source **scroll-sync**;
non-markdown documents (the preview is offered only for markdown); export /
print / copy-as-clean; editing *in* the preview; per-author coloring in the
preview (F3 attribution lives in the editor, not here); replacing F6 (F7
**coexists** — see §6.7).
**Non-goals (firm):** a general-purpose markdown previewer to rival the built-in
one (F7 is specifically the *track-changes* render); a WYSIWYG editor; any change
to the sidecar, the cross-rung contract, or `SCHEMA_VERSION`.
### 1.8 Assumptions · Constraints · Dependencies
- **Anchor:** Feature #21; deferred mermaid task #22. Builds on F6 (#17/#19) and
the shipped inner loop.
- **Reuses the F6 baseline** (`DiffViewController` / `BaselineStore`, global
storage keyed by URI hash, INV-18/19) as the "before" text; F7 adds **no**
persistence and never writes the baseline (INV-20).
- **New library dependencies (npm, runtime):** `markdown-it`, `mermaid`,
`diff` (jsdiff). **No** VS Code `extensionDependencies`; **no** CDN — mermaid
is bundled as a webview-local asset (CSP-safe), never in the extension-host
bundle (the `@cline/sdk` precedent: heavy deps stay out of the host bundle).
- No LLM anywhere in F7 or its tests; no network; no new credential surface
(INV-8 untouched).
- Nothing persisted touches `.threads/`; INV-14..17 + `SCHEMA_VERSION` untouched.
### 1.9 Business Use Cases
- **BUC-1 (prose review)** Mid-session the writer opens the preview and sees the
rendered chapter with their additions highlighted and deletions struck since
Claude last landed — then keeps writing, watching it update.
- **BUC-2 (diagram review)** A doc with a mermaid flowchart: after editing the
diagram, the preview renders the new diagram with a "changed since baseline"
badge, so the writer notices the diagram moved without hunting the fence.
---
## 2. Solution Proposal
A new **webview panel** (`cowriting.trackChangesPreview`) opened beside the
active markdown editor by a command. On open and on every (debounced) document
change or baseline epoch change, the **extension host** computes a **block-level
diff** between the F6 baseline text and the current buffer, renders it to
annotated HTML with `markdown-it` (prose blocks carry inline `<ins>`/`<del>` from
a word-level refinement; code and mermaid fences are **atomic** — whole block
added/removed/changed), and **posts the HTML to the webview**. The webview swaps
in the HTML and runs **mermaid** over any `<pre class="mermaid">` blocks (mermaid
needs a DOM, so it runs client-side), applying CSS for added/removed/changed
marks and the diagram "changed" badge. The document, the sidecar, and the F6
baseline are never mutated (INV-20). The whole render pipeline downstream of "get
baseline + current text" is a **pure function** (`trackChangesModel.ts`),
unit-testable with no vscode and no webview (INV-22).
---
## 3. Product Personas
- **PP-1 Inner-loop coauthor** — the human markdown writer/engineer (as F2F6);
the only persona F7 serves.
## 4. Product Use Cases
- **PUC-1 (open / close)** In a markdown editor, run **"Cowriting: Open
Track-Changes Preview"** → a preview opens in the column beside the editor,
rendering the doc with changes-since-baseline marked. Closing the panel (or
re-running the command) dismisses it. The document is untouched.
- **PUC-2 (live edit)** Type in the source editor → the preview updates
(debounced) with the new additions highlighted; nothing in the document,
sidecar, or baseline changes.
- **PUC-3 (epoch follows the machine)** Accept a Claude proposal → the F6
baseline advances (INV-18) → the preview re-renders: the accepted text is **no
longer** marked as a change; the writer's subsequent edits still are.
- **PUC-4 (pin)** Run "Cowriting: Pin Diff Baseline to Now" (the F6 command) →
the preview empties of marks (baseline = now); edits from here are marked.
- **PUC-5 (mermaid)** A changed or added `mermaid` block renders as the **new**
diagram with a **"changed since baseline"** badge; a removed diagram renders
struck/ghosted at the block level. (Intra-diagram diffing → #22.)
- **PUC-6 (graceful edges)** Non-markdown active editor → the command warns and
opens nothing (F7 is markdown-only; F6 covers any file). A malformed
mermaid/markdown block renders an inline error chip in place of that block, not
a broken preview. Storage/baseline absent → the preview shows the doc with
*no* marks (everything is "since opened") and a one-line note.
## 5. UX Layout
A webview in the editor column **beside** the source (the `ViewColumn.Beside`
convention of the built-in preview). The source editor stays the normal, fully
editable editor — the writer and Claude work there; F3 attribution tint / F4
proposals keep working there unchanged. The preview is **read-only rendered
output**: the document as markdown, with:
- **Added** text/blocks — a soft green background (`<ins>`), themeable.
- **Removed** text/blocks — struck-through, muted (`<del>`).
- **Changed atomic block** (code / mermaid) — rendered new, with a small
**"changed"** badge at the block's top-right corner.
- A compact **header bar**: `Track changes since <epoch>` (`opened` / `Claude
landed` / `pinned`, reusing F6's epoch label) and a `+N M` summary.
Colors derive from the active VS Code theme via webview CSS variables
(`--vscode-diffEditor-insertedTextBackground`, `…removedTextBackground`, etc.)
so it matches light/dark/high-contrast. No bespoke chrome beyond the header.
---
## 6. Technical Design
### 6.1 Invariants
Parent invariants INV-1..INV-19 carry over unchanged. F7 adds:
- **INV-20 (pure read-only preview)** F7 never mutates the document, the
sidecar, the attribution state, or the F6 baseline. It only *reads* the
baseline text (via the F6 controller) and the current buffer.
- **INV-21 (sealed webview)** The webview loads only **local** bundled assets
under a strict CSP (no remote scripts/styles, no CDN); mermaid is a bundled
webview asset. No network, no LLM, no credential surface (INV-8 untouched).
- **INV-22 (deterministic, testable render model)** Everything downstream of
`(baselineText, currentText) → annotated HTML` is a **pure, vscode-free**
function (`trackChangesModel.ts`): same inputs → same HTML, unit-testable with
no editor and no webview.
- **INV-23 (atomic non-prose blocks)** Code and mermaid fences are diffed at the
**whole-block** level only — never word-refined, never partially rendered. A
changed/added one renders fully (with a "changed" badge); a removed one
renders struck at block level. (Guards against rendering half a diagram.)
### 6.2 High-level architecture
Two new units plus a webview asset; the F6 controller is reused for the baseline.
- **`trackChangesModel.ts`** (`src/`, **vscode-free, pure** — INV-22) — the
engine: `renderTrackChanges(baselineText, currentText): string` (annotated
HTML). Internals: split both texts into top-level markdown **blocks**; LCS-diff
the block sequences by normalized text → `unchanged | added | removed |
changed` block ops; for a `changed` **prose** pair, compute a **word-level**
`diff` and emit inline `<ins>`/`<del>`; **code/mermaid** blocks stay atomic
(INV-23); render each block's markdown via `markdown-it` (mermaid fences →
`<pre class="mermaid">…</pre>` for client-side rendering), wrapping each block
in a `<div class="cw-blk cw-added|removed|changed">`. Pure: `markdown-it` and
`diff` are libraries; no vscode, no DOM.
- **`trackChangesPreview.ts`** (`src/`, vscode layer) — the webview controller:
registers `cowriting.showTrackChangesPreview`; owns one panel per document
(`Map<uriKey, WebviewPanel>`); on open / debounced `onDidChangeTextDocument` /
F6 `onDidChange*` epoch change, reads the **baseline** (from
`DiffViewController`) + the current buffer, calls `renderTrackChanges`, and
`postMessage`s the HTML; builds the sealed HTML shell (CSP, nonce, local
asset URIs). Markdown-only guard; disposes panels on close.
- **Webview asset** (`media/preview.js` + `media/preview.css` + bundled
`mermaid`) — receives `{html}` messages, swaps `innerHTML`, runs
`mermaid.run()` over `.mermaid` blocks, applies theme CSS variables. Built as a
**separate esbuild bundle** (`out/media/preview.js`) so mermaid never enters
the extension-host bundle.
- **F6 reuse:** `DiffViewController` exposes the baseline (`getBaseline(uriString)`
already returns `{text,…}`); F7 subscribes to baseline epoch changes. A small
additive event (`onDidChangeBaseline`) may be added to the F6 controller so the
preview refreshes on advance/pin without polling (additive, like
`onDidApplyAgentEdit`).
```mermaid
flowchart LR
edit["source editor\n(human + Claude edits)"] -- onDidChangeTextDocument (debounced) --> ctl
base["F6 baseline\n(DiffViewController, INV-18/19)"] -- onDidChangeBaseline --> ctl["trackChangesPreview\n(vscode layer)"]
ctl -- "(baselineText, currentText)" --> model["trackChangesModel\n(pure, vscode-free)\nblock diff + markdown-it"]
model -- annotated HTML --> ctl
ctl -- postMessage{html} --> wv["webview\n(sealed CSP, local assets)\nmermaid.run() on .mermaid"]
```
### 6.3 Data model & ownership
**No persisted artifact of any kind** (INV-20). F7 holds only in-memory webview
panels keyed by document URI; the baseline it reads is owned by F6. The only
on-the-wire model is the **annotated block** the engine produces (host→webview),
which is transient HTML, not stored:
```ts
// internal to trackChangesModel.ts (illustrative — not persisted)
type BlockOp =
| { kind: "unchanged"; html: string }
| { kind: "added"; html: string }
| { kind: "removed"; html: string } // rendered struck at block level
| { kind: "changed"; html: string; // prose: inline <ins>/<del>; atomic: badge
atomic: boolean }; // true for code/mermaid (INV-23)
```
### 6.4 Interfaces & contracts
- **`trackChangesModel`** (vscode-free): `renderTrackChanges(baselineText: string,
currentText: string, opts?: { mermaid?: boolean }): string` — returns the inner
HTML for the preview body. Deterministic (INV-22). Also exports
`diffBlocks(baselineText, currentText): BlockOp[]` (the pre-render model) for
fine-grained unit tests.
- **`TrackChangesPreviewController`** (vscode layer):
`show(document)` — open/reveal the panel for a markdown doc (warn + no-op for
non-markdown); `refresh(document)` — recompute + post (debounced internally);
`isOpen(uriString): boolean` and `getLastModel(uriString): BlockOp[] | undefined`
(test seam — the last computed model, so host E2E can assert marks without
reading webview DOM). Disposable.
- **`DiffViewController`** (F6, additive): `readonly onDidChangeBaseline:
vscode.Event<{ uri: string }>` fired on every capture (open/advance/pin) so the
preview refreshes; mirrors the existing internal `onDidChangeEmitter`.
- **Command / keybinding** (`package.json`): `cowriting.showTrackChangesPreview`
("Cowriting: Open Track-Changes Preview", palette; default keybinding
`ctrl+alt+r`, `when: editorLangId == markdown`). Registered as a warning stub in
the no-folder path is **not** required — like F6 (#19) it is workspace-
independent (the baseline is global storage), so it is registered for real.
- **Build:** `esbuild.mjs` gains a second entry building `media/preview.ts` →
`out/media/preview.js` (IIFE, bundles `mermaid`). `markdown-it` + `diff` bundle
into the host `out/extension.cjs` as today.
### 6.5 PerProduct-Use-Case design
- **PUC-1 (open/close):** command → markdown guard → create or reveal a
`WebviewPanel` (`viewType "cowriting.trackChangesPreview"`, `ViewColumn.Beside`,
`retainContextWhenHidden: false`, `localResourceRoots = [out/media]`). Build the
sealed shell (CSP `default-src 'none'; script-src 'nonce-…'; style-src
'nonce-…' …; img-src data:`), then `refresh`.
- **PUC-2 (live edit):** subscribe to `onDidChangeTextDocument` filtered to the
panel's doc; **debounce** ~150 ms; recompute + post. Pure model keeps this
cheap; O(document) per render at inner-loop scale.
- **PUC-3 / PUC-4 (epoch follows machine / pin):** subscribe to F6
`onDidChangeBaseline`; on fire for the panel's doc, `refresh`. Accepted text is
in the new baseline, so it renders unmarked.
- **PUC-5 (mermaid):** the engine emits `<pre class="mermaid">SRC</pre>` for a
`mermaid` fence and tags the wrapping block `cw-changed`/`cw-added` when the
fence text differs from / is absent in the baseline; the webview runs
`mermaid.run()` and CSS draws the badge. Atomic (INV-23) — never word-diffed.
- **PUC-6 (graceful edges):** non-markdown → `showWarningMessage`, no panel.
A block that throws in `markdown-it`/`mermaid` → the engine/webview substitutes
an inline error chip for that block; the rest renders. No baseline yet → render
current doc with zero marks + a header note.
### 6.6 Non-functional requirements & cross-cutting concerns
Render cost is O(document) on a debounced edit — fine at inner-loop scale; large
docs (>~a few hundred KB) can cap the live debounce or render on idle (a tunable,
not v1-critical). The webview is **sealed** (INV-21): local assets only, strict
CSP with a per-load nonce, no network. mermaid (~MB) lives only in the webview
bundle, never the host bundle (the `@cline/sdk` size discipline). Theme-aware via
CSS variables. No telemetry, no LLM, no credentials.
### 6.7 Key decisions & alternatives considered
| Decision | Chosen | Alternatives rejected |
| --- | --- | --- |
| **Render surface** | **Custom webview** we own | **Augment the built-in markdown preview** (markdown-it plugin + CSS) — the built-in preview is hardwired to render the *active document's text*, not a diff artifact; can't cleanly feed it a baseline-vs-current delta. **Decoration-only in the editor** — that's the F6/single-pane path, not a *rendered* view |
| **Libraries vs roll-our-own** | **Depend on `markdown-it` + `mermaid` + `diff`** (npm libs), **roll the track-changes integration** (no off-the-shelf "rendered markdown diff w/ mermaid") | **roll a markdown/mermaid renderer** (months of work, worse); **depend on a 3rd-party VS Code *extension*** (e.g. a mermaid-preview extension) — fragile `extensionDependencies`, install-coupled, quality risk |
| **Where each layer runs** | markdown-it + diff in the **host** (post HTML); **mermaid in the webview** (DOM-bound) | mermaid in the host (no DOM; headless mermaid is heavy/brittle); everything in the webview (ship the diff engine into the sandbox — harder to unit-test, INV-22 lost) |
| **Diff granularity** | **Block-level + word refinement in prose; code/mermaid atomic** (INV-23) | **pure inline source ins/del** — breaks on structural edits, renders half a mermaid fence; **DOM/SVG diffing** — complex, mermaid SVGs diff poorly |
| **Mermaid changes (v1)** | **Whole-diagram "changed" badge**; intra-diagram diff **deferred (#22)** | **intra-diagram node/edge diff now** — large unscoped design (source vs parsed-graph vs SVG; layout reflow) — explicitly deferred by the operator |
| **Relationship to F6** | **Coexist** — F7 is the markdown track-changes view; F6's native diff/gutter still serves **any file incl. code** | **replace F6** — F6 works on non-markdown where a rendered preview is meaningless; removing it loses coverage |
| **Asset delivery** | **Bundle mermaid as a webview-local asset** (esbuild second entry), strict CSP | **CDN/remote mermaid** — violates INV-21 (offline/CSP); **host-bundle mermaid** — bloats `extension.cjs` for code that only runs in the webview |
| **Open keybinding** | `ctrl+alt+r` (`when: editorLangId == markdown`) — "r" for *rendered*; unbound in stock VS Code | `ctrl+k v` (taken by the built-in preview); `cmd+…` (macOS-specific, the F6 §6.7 reasoning) |
### 6.8 Testing strategy
- **Unit (vitest, vscode-free):** `trackChangesModel` — `diffBlocks` /
`renderTrackChanges` over fixtures: pure-addition, pure-deletion, prose
modification (asserts inline `<ins>`/`<del>`), block insert/remove/reorder,
a **code** fence change (atomic — no inline ins/del, whole block `cw-changed`,
INV-23), a **mermaid** fence change (atomic + emits `<pre class="mermaid">` +
`cw-changed` badge class), unchanged doc (no marks), malformed block (error
chip, rest renders), determinism (same inputs → identical HTML, INV-22).
- **Host E2E (`@vscode/test-electron`, the F2F6 pattern, no LLM):** open a
markdown fixture → run `cowriting.showTrackChangesPreview` → assert a panel is
open (`isOpen` true, viewType matches) and `getLastModel()` shows the expected
block ops (e.g. `opened` baseline → no changes; type → an `added`/`changed`
block; programmatic propose+accept → baseline advanced, accepted block now
`unchanged`; pin → no marks). Webview **DOM/mermaid rendering** is *not* asserted
in E2E (sealed sandbox) — covered by manual smoke. Non-markdown doc → command
warns, no panel.
- **Live smoke (manual — `docs/MANUAL-SMOKE-F7.md`):** open a markdown doc with
prose + a mermaid diagram + a code block → edit prose (see ins/del) → edit the
mermaid (see the diagram re-render with the "changed" badge) → ask Claude to
edit a selection + accept (the accepted text drops its marks) → pin (marks
clear). 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 for that block,
preview still renders (PUC-6). Baseline missing → render with no marks + a note.
Webview disposed by the user → controller drops the panel; 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). Large-doc perf → debounce/idle cap (tunable),
never a correctness issue.
---
## 7. Delivery Plan
### 7.1 Approach / strategy
One planning-and-executing session (F7 = #21), plan written just-in-time from
this spec — the F2F6 precedent. Host-E2E tier (a VS Code extension has no
browser/deploy stage); no LLM in CI. The webview's *visual* rendering (mermaid,
theming) is verified by the manual smoke, not automated — the automated seam is
the pure render **model**.
### 7.2 Slicing plan
- **SLICE-1** `trackChangesModel` (vscode-free): block diff + word refinement +
`markdown-it` render + atomic code/mermaid (INV-22/23) + vitest suite. Add
`markdown-it` + `diff` deps.
- **SLICE-2** Webview asset + build: `media/preview.ts` (receive HTML, run
mermaid, theme CSS), esbuild second entry, bundle `mermaid`; sealed CSP shell.
- **SLICE-3** `TrackChangesPreviewController` + F6 `onDidChangeBaseline` event +
`package.json` command/keybinding + `CowritingApi` handle + test seam
(`isOpen`/`getLastModel`); markdown guard; debounced live update.
- **SLICE-4** Host E2E (open/live/epoch/pin/non-markdown per §6.8) +
`docs/MANUAL-SMOKE-F7.md` + README F7 section.
E2E are first-class plan tasks (handbook §9/§4); this app's required tier is host
E2E (the F2F6 precedent).
### 7.3 Rollout / launch plan
Non-shippable (no marketplace publish). "Done" = #21 acceptance: a rendered
track-changes preview opens beside a markdown doc, marks changes-since-baseline
(prose ins/del + atomic code/mermaid "changed" badge), updates live and on epoch
change; unit + host E2E green; live smoke performed once. #22 (intra-diagram
mermaid diff) remains open.
### 7.4 Risks & mitigations
| Risk | Mitigation |
| --- | --- |
| Rendering a *diff* of markdown breaks block structure (half a fence, broken list) | Block-level diff with **atomic** code/mermaid (INV-23); render per-block; malformed → error chip |
| mermaid bundle size / load | Webview-only bundle, never the host bundle; lazy `mermaid.run()` after HTML swap |
| Webview security | Sealed CSP, nonce, local-only assets (INV-21); no network/LLM |
| Live re-render cost on large docs | Debounce + optional idle cap; pure O(document) model |
| Scope creep into intra-diagram mermaid diff | Explicitly deferred to #22; v1 is the whole-diagram badge |
| Two change-views (F6 + F7) confuse | Distinct commands + titles; F7 markdown-only, F6 any-file; README explains when to use which |
---
## 8. Traceability matrix
| Requirement (#21) | Use case | Design | Slice |
| --- | --- | --- | --- |
| Rendered preview beside the editable source | PUC-1 | §6.2, §5 | SLICE-3 |
| Marks what changed since the F6 baseline | PUC-2/3 | INV-18 reuse, §6.2 | SLICE-1 |
| Full mermaid support | PUC-5 | §6.2, §6.4 | SLICE-1/2 |
| Mermaid change = whole-diagram badge (intra-diagram → #22) | PUC-5 | INV-23 | SLICE-1 |
| Prose additions/deletions inline | PUC-2 | §6.2 word refinement | SLICE-1 |
| Live update on edit + epoch change | PUC-2/3/4 | §6.5 | SLICE-3 |
| Pure read-only; no sidecar/contract impact | — | INV-20, §6.3 | all |
| Sealed webview, no network/LLM, local mermaid | — | INV-21 | SLICE-2 |
| Unit + host E2E, no LLM in CI | — | §6.8 | SLICE-1/4 |
## 9. Open Questions & Decisions log
- **RESOLVED (session 0019 ideation, recorded in #21):** render surface =
custom webview; libraries = `markdown-it` + `mermaid` + `diff` (no
extension-deps, no CDN); layer split = host renders/diffs, webview runs
mermaid; diff granularity = block-level + prose word-refinement, code/mermaid
atomic; mermaid v1 = whole-diagram "changed" badge; F7 **coexists** with F6.
- **RESOLVED (this spec):** open command `cowriting.showTrackChangesPreview`
(`ctrl+alt+r`, markdown-only); test seam = pure model + `getLastModel`
(webview DOM not E2E-asserted); additive F6 `onDidChangeBaseline` event.
- **RESOLVED (session 0027, §11):** intra-diagram mermaid diffing (**#22**) —
parsed-graph diff, flowchart + sequence this increment, removed elements
ghosted in place, layout reflow accepted; all else falls back to the v1 badge.
- **OPEN → later:** preview→source scroll-sync; non-markdown rendered views;
per-author coloring in the preview; whether F7 should eventually subsume F6's
prose path; further mermaid diagram types for #22 (class/state/ER/…).
- **Deferred decisions (autonomous calls for operator review):** **F7 coexists
with F6** rather than replacing it (markdown-only vs any-file); the `ctrl+alt+r`
keybinding; webview DOM/mermaid rendering verified by manual smoke rather than
automated E2E (sealed-sandbox constraint). All cheap to revisit.
## 10. Glossary & References
- **Track-changes preview** — a rendered (not raw) view of the markdown document
marking what changed since the F6 baseline. **Baseline / epoch / advance / pin**
— as F6 (`coauthoring-diff-view.md`). **Atomic block** — a code or mermaid
fence diffed whole, never word-refined (INV-23). **Changed badge** — the
whole-diagram mark on a changed/added mermaid (or code) block; intra-diagram
diffing is #22.
- Feature #21 (F7) · deferred #22 · Epic #1 · F6 #17 (PR #18) + #19 (PR #20) ·
parent specs `coauthoring-diff-view.md`, `coauthoring-inner-loop.md`,
`coauthoring-attribution.md`, `coauthoring-propose-accept.md` · lineage
`ben.stull/rfc-app#48`.
---
## 11. F7.1 — Intra-diagram mermaid diffing (#22)
> **Status:** shipped (session 0027, 2026-06-11; PR #28). Addendum to F7 (#21):
> the v1 whole-diagram "changed" badge (PUC-5, INV-23) is refined into a
> **node/edge-level diff inside the diagram** for the two most common diagram
> types. Anchor: task `benstull/vscode-cowriting-plugin#22` (`type/task`). Builds
> on the §6 render engine; reuses the F6 baseline as "before". No new persistence
> (INV-20 holds), no webview-security change (INV-21 holds), host stays pure &
> deterministic (INV-22 holds).
### 11.1 What changes
When a **`changed` mermaid block** of a **supported diagram type** is diffed
against the F6 baseline, instead of rendering the new diagram whole with a single
"changed" badge, F7.1 renders the new diagram **with its individual elements
colored by what changed**:
- **flowchart** (`graph` / `flowchart`) — added / removed / changed **nodes and
edges**;
- **sequence** (`sequenceDiagram`) — added / removed / changed **participants and
messages**.
Every other case is **unchanged from v1**: a wholly `added` / `removed` mermaid
block, a `changed` block of any **other** type (class, state, ER, gantt, …), and
**any parse failure** all keep the v1 whole-block badge. This is a pure
refinement of one branch of `renderOp` — nothing else in F7 moves.
### 11.2 The three forks, resolved
| Fork | Decision | Why |
| --- | --- | --- |
| **Diff level** — source-text vs parsed-graph vs SVG | **Parsed-graph.** Parse the diagram source into a typed element model (nodes/edges or participants/messages), diff that, then **re-emit the new diagram source augmented with mermaid's own styling directives** so mermaid renders a colored diagram. | Source-text diff shows a *text* diff in a *rendered* view (defeats F7). SVG diff is layout-brittle and has no stable semantic mapping (§6.7 already rejected it). Parsed-graph keeps the host **pure** (parse + diff + string-augment — fully unit-testable, INV-22) and the **webview unchanged** (it still just runs `mermaid.run()` over `<pre class="mermaid">`). |
| **Visual marking of removed elements** | **Ghost in place.** Removed nodes/edges/messages are **re-injected** into the emitted diagram, styled faded/dashed, so the deletion is visible in its original context. | The point of an intra-diagram diff is to *see* what left, where it was. A caption-only list loses position. Accepted cost: ghosts add to layout reflow. |
| **Diagram types this increment** | **Flowchart + sequence.** All other types fall back to the v1 badge. | The two most common types. Each later type is an additive follow-up on #22 (same dispatch seam). |
**Layout reflow (the fourth open question):** **accepted, not fought.** The
augmented new diagram is laid out fresh by mermaid; we do **not** attempt to pin
node positions to the baseline layout (mermaid exposes no stable layout pinning;
position-matching is a large unscoped effort). Ghosting keeps removed elements
present so the diff still reads; we do not promise the before/after diagrams are
spatially aligned.
### 11.3 Styling-hook asymmetry (a real mermaid constraint)
Flowcharts and sequence diagrams give very different styling surfaces, so the two
emitters differ:
- **Flowchart** — crisp per-element styling. Emit `classDef cwAdded/cwChanged/cwRemoved …`
once, then `class <ids> cwAdded` for nodes and `linkStyle <indices> stroke:…` for
edges (edges are addressed by their **declaration-order index**, which the parser
tracks). Ghost removed nodes/edges are appended to the source with the `cwRemoved`
class / a dashed `linkStyle`.
- **Sequence** — mermaid has **no per-message color directive**; its only
per-message styling hook is the **`rect rgb(r,g,b) … end`** background block. So
the sequence emitter rebuilds the message stream (ghosted-removed messages
re-inserted at their baseline position) and wraps each added / changed / removed
message in a one-message `rect` tinted green / amber / grey. Participants are
re-emitted (removed ones re-declared so they still appear).
Colors are **fixed, theme-neutral** values baked into the emitted source (mermaid
source can't read VS Code CSS variables): added ≈ green (`#2ea043`), changed ≈
amber (`#d29922`), removed ≈ muted grey + dashed (`#808080`), each chosen to read
on both light and dark mermaid themes. A small **legend** (`+ added · ~ changed ·
removed`) is shown beneath a diffed diagram (host-emitted markup, not part of
the mermaid source).
### 11.4 Architecture & seam
A new **pure, vscode-free, DOM-free** host module tree (INV-22), dispatched from
the existing `changed`+atomic+mermaid branch of `renderOp`:
- **`src/mermaidDiff.ts`** — `diffMermaid(beforeSrc, currentSrc): MermaidDiffResult` —
detects the diagram type (`detectDiagramType`); routes to the flowchart or
sequence differ; returns `{ kind: "augmented"; source }` on success or
`{ kind: "fallback" }` for unsupported types / parse failure. Wraps the differ
in try/catch so **any** parser surprise degrades to the v1 badge (never throws —
the §6.9 error-chip philosophy). Owns the shared `CW_COLORS` palette.
- **`src/mermaidFlowchartDiff.ts`** — `parseFlowchart` (nodes by id, edges by
declaration order) + `diffFlowchart` (emits the augmented source).
- **`src/mermaidSequenceDiff.ts`** — `parseSequence` (participants + statements) +
`diffSequence` (LCS over statements via jsdiff `diffArrays`; `rect` tints + ghost
re-insertions).
`renderOp`'s changed-mermaid branch (`src/trackChangesModel.ts`) extracts the
fence body, calls `diffMermaid`; on `augmented` it emits `<pre class="mermaid">AUGMENTED</pre>`
+ the legend and drops the single-badge; on `fallback` it does exactly what it did
before. **The webview (`media/preview.ts`) needs no change** — the styling rides
inside the mermaid source. `media/preview.css` gains the legend swatch styles only.
### 11.5 Invariants (continuing the project sequence; F8 took 2425, F9 2628)
- **INV-29 (supported-type intra-diff)** A `changed` mermaid block whose type is
**flowchart or sequence** is diffed at element granularity (node/edge resp.
participant/message) against the F6 baseline and re-emitted as a single mermaid
diagram whose elements are styled by change kind. It remains **one rendered
diagram**, never a split/word-diff (INV-23's atomicity is *refined* here, not
abandoned — the block is still rendered whole, just self-colored).
- **INV-30 (graceful fallback is total)** Any unsupported diagram type, any
wholly added/removed mermaid block, and **any** parse/emit failure fall back to
the exact v1 whole-block badge. Intra-diagram diffing **never** produces an
error chip or a broken diagram where v1 would have rendered.
- **INV-31 (ghost completeness)** Every element present in the baseline but absent
from the current diagram appears in the rendered diff as a faded/dashed ghost in
its baseline position; no removed element silently vanishes.
- **INV-22/-20/-21 preserved** The differ is pure & deterministic (same inputs →
identical augmented source); F7.1 persists nothing and reads only the existing
F6 baseline; the webview stays sealed and unchanged (styling travels in-source).
### 11.6 Testing (as shipped)
- **Unit (vitest, host, no DOM/LLM):** dispatcher + type detection
(`mermaidDiff.test.ts`); flowchart parser + node/edge diff + emission
(`mermaidFlowchartDiff.test.ts`); sequence parser + participant/message diff +
`rect` emission (`mermaidSequenceDiff.test.ts`); `renderTrackChanges` augments a
changed flowchart/sequence and falls back for unsupported types
(`trackChangesModel.test.ts`). Determinism asserted on each differ.
- **Host E2E:** a changed flowchart in the fixture doc → `getLastModel` shows the
mermaid op is `changed`+atomic and the emitted HTML (via a `renderHtmlFor` test
seam) carries the augmenting directives for the added node. Webview DOM rendering
stays manual-smoke (`docs/MANUAL-SMOKE-F7.1.md`).
- **Counts at ship:** 189 unit + 38/5 host E2E green; typecheck clean.
### 11.7 Out of scope (still deferred)
Class / state / ER / gantt / other diagram types (additive #22 follow-ups, same
seam); pinning layout so before/after align spatially; intra-label word-diffing
inside a single changed node's text; animating the transition. These are
explicitly **not** in this increment.