--- 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 F2–F5 (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 ``/`` 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 `
` 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 F2–F6);
  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 (``), themeable.
- **Removed** text/blocks — struck-through, muted (``).
- **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 ` (`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 ``/``; **code/mermaid** blocks stay atomic
  (INV-23); render each block's markdown via `markdown-it` (mermaid fences →
  `
` for client-side rendering), wrapping each block in a `
`. 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`); 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 /; 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 Per–Product-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 `
SRC
` 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 ``/``), 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 `
` +
  `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 F2–F6 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 F2–F6 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 F2–F6 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 `
`). |
| **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  cwAdded` for nodes and `linkStyle  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 `
AUGMENTED
` + 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 24–25, F9 26–28) - **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.