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>
36 KiB
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 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 CodeextensionDependencies; no CDN — mermaid is bundled as a webview-local asset (CSP-safe), never in the extension-host bundle (the@cline/sdkprecedent: 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_VERSIONuntouched.
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 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
mermaidblock 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 −Msummary.
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 HTMLis 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 | changedblock ops; for achangedprose pair, compute a word-leveldiffand emit inline<ins>/<del>; code/mermaid blocks stay atomic (INV-23); render each block's markdown viamarkdown-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-itanddiffare libraries; no vscode, no DOM.trackChangesPreview.ts(src/, vscode layer) — the webview controller: registerscowriting.showTrackChangesPreview; owns one panel per document (Map<uriKey, WebviewPanel>); on open / debouncedonDidChangeTextDocument/ F6onDidChange*epoch change, reads the baseline (fromDiffViewController) + the current buffer, callsrenderTrackChanges, andpostMessages 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+ bundledmermaid) — receives{html}messages, swapsinnerHTML, runsmermaid.run()over.mermaidblocks, applies theme CSS variables. Built as a separate esbuild bundle (out/media/preview.js) so mermaid never enters the extension-host bundle. - F6 reuse:
DiffViewControllerexposes 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, likeonDidApplyAgentEdit).
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 exportsdiffBlocks(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): booleanandgetLastModel(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 internalonDidChangeEmitter.- Command / keybinding (
package.json):cowriting.showTrackChangesPreview("Cowriting: Open Track-Changes Preview", palette; default keybindingctrl+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.mjsgains a second entry buildingmedia/preview.ts→out/media/preview.js(IIFE, bundlesmermaid).markdown-it+diffbundle into the hostout/extension.cjsas 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 (CSPdefault-src 'none'; script-src 'nonce-…'; style-src 'nonce-…' …; img-src data:), thenrefresh. - PUC-2 (live edit): subscribe to
onDidChangeTextDocumentfiltered 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 amermaidfence and tags the wrapping blockcw-changed/cw-addedwhen the fence text differs from / is absent in the baseline; the webview runsmermaid.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 inmarkdown-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/renderTrackChangesover 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 blockcw-changed, INV-23), a mermaid fence change (atomic + emits<pre class="mermaid">+cw-changedbadge 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 → runcowriting.showTrackChangesPreview→ assert a panel is open (isOpentrue, viewType matches) andgetLastModel()shows the expected block ops (e.g.openedbaseline → no changes; type → anadded/changedblock; programmatic propose+accept → baseline advanced, accepted block nowunchanged; 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 thatgit statusshows 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-itrender + atomic code/mermaid (INV-22/23) + vitest suite. Addmarkdown-it+diffdeps. - SLICE-2 Webview asset + build:
media/preview.ts(receive HTML, run mermaid, theme CSS), esbuild second entry, bundlemermaid; sealed CSP shell. - SLICE-3
TrackChangesPreviewController+ F6onDidChangeBaselineevent +package.jsoncommand/keybinding +CowritingApihandle + 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 F6onDidChangeBaselineevent. - 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+rkeybinding; 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· lineageben.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, thenclass <ids> cwAddedfor nodes andlinkStyle <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 thecwRemovedclass / a dashedlinkStyle. - Sequence — mermaid has no per-message color directive; its only
per-message styling hook is the
rect rgb(r,g,b) … endbackground 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-messagerecttinted 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 sharedCW_COLORSpalette.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 jsdiffdiffArrays;recttints + 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
fallbackit does exactly what it did before. The webview (media/preview.ts) needs no change — the styling rides inside the mermaid source.media/preview.cssgains 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
changedmermaid 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 +rectemission (mermaidSequenceDiff.test.ts);renderTrackChangesaugments 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 →
getLastModelshows the mermaid op ischanged+atomic and the emitted HTML (via arenderHtmlFortest 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.