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

36 KiB
Raw Blame History

status
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 postMessages 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).
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:

// 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.tsout/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): trackChangesModeldiffBlocks / 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.tsdiffMermaid(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.tsparseFlowchart (nodes by id, edges by declaration order) + diffFlowchart (emits the augmented source).
  • src/mermaidSequenceDiff.tsparseSequence (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.