1c2844d12b
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>
606 lines
36 KiB
Markdown
606 lines
36 KiB
Markdown
---
|
||
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 `<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 `mermaid` block renders as the **new**
|
||
diagram with a **"changed since baseline"** badge; a removed diagram renders
|
||
struck/ghosted at the block level. (Intra-diagram diffing → #22.)
|
||
- **PUC-6 (graceful edges)** Non-markdown active editor → the command warns and
|
||
opens nothing (F7 is markdown-only; F6 covers any file). A malformed
|
||
mermaid/markdown block renders an inline error chip in place of that block, not
|
||
a broken preview. Storage/baseline absent → the preview shows the doc with
|
||
*no* marks (everything is "since opened") and a one-line note.
|
||
|
||
## 5. UX Layout
|
||
|
||
A webview in the editor column **beside** the source (the `ViewColumn.Beside`
|
||
convention of the built-in preview). The source editor stays the normal, fully
|
||
editable editor — the writer and Claude work there; F3 attribution tint / F4
|
||
proposals keep working there unchanged. The preview is **read-only rendered
|
||
output**: the document as markdown, with:
|
||
|
||
- **Added** text/blocks — a soft green background (`<ins>`), themeable.
|
||
- **Removed** text/blocks — struck-through, muted (`<del>`).
|
||
- **Changed atomic block** (code / mermaid) — rendered new, with a small
|
||
**"changed"** badge at the block's top-right corner.
|
||
- A compact **header bar**: `Track changes since <epoch>` (`opened` / `Claude
|
||
landed` / `pinned`, reusing F6's epoch label) and a `+N −M` summary.
|
||
|
||
Colors derive from the active VS Code theme via webview CSS variables
|
||
(`--vscode-diffEditor-insertedTextBackground`, `…removedTextBackground`, etc.)
|
||
so it matches light/dark/high-contrast. No bespoke chrome beyond the header.
|
||
|
||
---
|
||
|
||
## 6. Technical Design
|
||
|
||
### 6.1 Invariants
|
||
|
||
Parent invariants INV-1..INV-19 carry over unchanged. F7 adds:
|
||
|
||
- **INV-20 (pure read-only preview)** F7 never mutates the document, the
|
||
sidecar, the attribution state, or the F6 baseline. It only *reads* the
|
||
baseline text (via the F6 controller) and the current buffer.
|
||
- **INV-21 (sealed webview)** The webview loads only **local** bundled assets
|
||
under a strict CSP (no remote scripts/styles, no CDN); mermaid is a bundled
|
||
webview asset. No network, no LLM, no credential surface (INV-8 untouched).
|
||
- **INV-22 (deterministic, testable render model)** Everything downstream of
|
||
`(baselineText, currentText) → annotated HTML` is a **pure, vscode-free**
|
||
function (`trackChangesModel.ts`): same inputs → same HTML, unit-testable with
|
||
no editor and no webview.
|
||
- **INV-23 (atomic non-prose blocks)** Code and mermaid fences are diffed at the
|
||
**whole-block** level only — never word-refined, never partially rendered. A
|
||
changed/added one renders fully (with a "changed" badge); a removed one
|
||
renders struck at block level. (Guards against rendering half a diagram.)
|
||
|
||
### 6.2 High-level architecture
|
||
|
||
Two new units plus a webview asset; the F6 controller is reused for the baseline.
|
||
|
||
- **`trackChangesModel.ts`** (`src/`, **vscode-free, pure** — INV-22) — the
|
||
engine: `renderTrackChanges(baselineText, currentText): string` (annotated
|
||
HTML). Internals: split both texts into top-level markdown **blocks**; LCS-diff
|
||
the block sequences by normalized text → `unchanged | added | removed |
|
||
changed` block ops; for a `changed` **prose** pair, compute a **word-level**
|
||
`diff` and emit inline `<ins>`/`<del>`; **code/mermaid** blocks stay atomic
|
||
(INV-23); render each block's markdown via `markdown-it` (mermaid fences →
|
||
`<pre class="mermaid">…</pre>` for client-side rendering), wrapping each block
|
||
in a `<div class="cw-blk cw-added|removed|changed">`. Pure: `markdown-it` and
|
||
`diff` are libraries; no vscode, no DOM.
|
||
- **`trackChangesPreview.ts`** (`src/`, vscode layer) — the webview controller:
|
||
registers `cowriting.showTrackChangesPreview`; owns one panel per document
|
||
(`Map<uriKey, WebviewPanel>`); on open / debounced `onDidChangeTextDocument` /
|
||
F6 `onDidChange*` epoch change, reads the **baseline** (from
|
||
`DiffViewController`) + the current buffer, calls `renderTrackChanges`, and
|
||
`postMessage`s the HTML; builds the sealed HTML shell (CSP, nonce, local
|
||
asset URIs). Markdown-only guard; disposes panels on close.
|
||
- **Webview asset** (`media/preview.js` + `media/preview.css` + bundled
|
||
`mermaid`) — receives `{html}` messages, swaps `innerHTML`, runs
|
||
`mermaid.run()` over `.mermaid` blocks, applies theme CSS variables. Built as a
|
||
**separate esbuild bundle** (`out/media/preview.js`) so mermaid never enters
|
||
the extension-host bundle.
|
||
- **F6 reuse:** `DiffViewController` exposes the baseline (`getBaseline(uriString)`
|
||
already returns `{text,…}`); F7 subscribes to baseline epoch changes. A small
|
||
additive event (`onDidChangeBaseline`) may be added to the F6 controller so the
|
||
preview refreshes on advance/pin without polling (additive, like
|
||
`onDidApplyAgentEdit`).
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
edit["source editor\n(human + Claude edits)"] -- onDidChangeTextDocument (debounced) --> ctl
|
||
base["F6 baseline\n(DiffViewController, INV-18/19)"] -- onDidChangeBaseline --> ctl["trackChangesPreview\n(vscode layer)"]
|
||
ctl -- "(baselineText, currentText)" --> model["trackChangesModel\n(pure, vscode-free)\nblock diff + markdown-it"]
|
||
model -- annotated HTML --> ctl
|
||
ctl -- postMessage{html} --> wv["webview\n(sealed CSP, local assets)\nmermaid.run() on .mermaid"]
|
||
```
|
||
|
||
### 6.3 Data model & ownership
|
||
|
||
**No persisted artifact of any kind** (INV-20). F7 holds only in-memory webview
|
||
panels keyed by document URI; the baseline it reads is owned by F6. The only
|
||
on-the-wire model is the **annotated block** the engine produces (host→webview),
|
||
which is transient HTML, not stored:
|
||
|
||
```ts
|
||
// internal to trackChangesModel.ts (illustrative — not persisted)
|
||
type BlockOp =
|
||
| { kind: "unchanged"; html: string }
|
||
| { kind: "added"; html: string }
|
||
| { kind: "removed"; html: string } // rendered struck at block level
|
||
| { kind: "changed"; html: string; // prose: inline <ins>/<del>; atomic: badge
|
||
atomic: boolean }; // true for code/mermaid (INV-23)
|
||
```
|
||
|
||
### 6.4 Interfaces & contracts
|
||
|
||
- **`trackChangesModel`** (vscode-free): `renderTrackChanges(baselineText: string,
|
||
currentText: string, opts?: { mermaid?: boolean }): string` — returns the inner
|
||
HTML for the preview body. Deterministic (INV-22). Also exports
|
||
`diffBlocks(baselineText, currentText): BlockOp[]` (the pre-render model) for
|
||
fine-grained unit tests.
|
||
- **`TrackChangesPreviewController`** (vscode layer):
|
||
`show(document)` — open/reveal the panel for a markdown doc (warn + no-op for
|
||
non-markdown); `refresh(document)` — recompute + post (debounced internally);
|
||
`isOpen(uriString): boolean` and `getLastModel(uriString): BlockOp[] | undefined`
|
||
(test seam — the last computed model, so host E2E can assert marks without
|
||
reading webview DOM). Disposable.
|
||
- **`DiffViewController`** (F6, additive): `readonly onDidChangeBaseline:
|
||
vscode.Event<{ uri: string }>` fired on every capture (open/advance/pin) so the
|
||
preview refreshes; mirrors the existing internal `onDidChangeEmitter`.
|
||
- **Command / keybinding** (`package.json`): `cowriting.showTrackChangesPreview`
|
||
("Cowriting: Open Track-Changes Preview", palette; default keybinding
|
||
`ctrl+alt+r`, `when: editorLangId == markdown`). Registered as a warning stub in
|
||
the no-folder path is **not** required — like F6 (#19) it is workspace-
|
||
independent (the baseline is global storage), so it is registered for real.
|
||
- **Build:** `esbuild.mjs` gains a second entry building `media/preview.ts` →
|
||
`out/media/preview.js` (IIFE, bundles `mermaid`). `markdown-it` + `diff` bundle
|
||
into the host `out/extension.cjs` as today.
|
||
|
||
### 6.5 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 `<pre class="mermaid">SRC</pre>` for a
|
||
`mermaid` fence and tags the wrapping block `cw-changed`/`cw-added` when the
|
||
fence text differs from / is absent in the baseline; the webview runs
|
||
`mermaid.run()` and CSS draws the badge. Atomic (INV-23) — never word-diffed.
|
||
- **PUC-6 (graceful edges):** non-markdown → `showWarningMessage`, no panel.
|
||
A block that throws in `markdown-it`/`mermaid` → the engine/webview substitutes
|
||
an inline error chip for that block; the rest renders. No baseline yet → render
|
||
current doc with zero marks + a header note.
|
||
|
||
### 6.6 Non-functional requirements & cross-cutting concerns
|
||
|
||
Render cost is O(document) on a debounced edit — fine at inner-loop scale; large
|
||
docs (>~a few hundred KB) can cap the live debounce or render on idle (a tunable,
|
||
not v1-critical). The webview is **sealed** (INV-21): local assets only, strict
|
||
CSP with a per-load nonce, no network. mermaid (~MB) lives only in the webview
|
||
bundle, never the host bundle (the `@cline/sdk` size discipline). Theme-aware via
|
||
CSS variables. No telemetry, no LLM, no credentials.
|
||
|
||
### 6.7 Key decisions & alternatives considered
|
||
|
||
| Decision | Chosen | Alternatives rejected |
|
||
| --- | --- | --- |
|
||
| **Render surface** | **Custom webview** we own | **Augment the built-in markdown preview** (markdown-it plugin + CSS) — the built-in preview is hardwired to render the *active document's text*, not a diff artifact; can't cleanly feed it a baseline-vs-current delta. **Decoration-only in the editor** — that's the F6/single-pane path, not a *rendered* view |
|
||
| **Libraries vs roll-our-own** | **Depend on `markdown-it` + `mermaid` + `diff`** (npm libs), **roll the track-changes integration** (no off-the-shelf "rendered markdown diff w/ mermaid") | **roll a markdown/mermaid renderer** (months of work, worse); **depend on a 3rd-party VS Code *extension*** (e.g. a mermaid-preview extension) — fragile `extensionDependencies`, install-coupled, quality risk |
|
||
| **Where each layer runs** | markdown-it + diff in the **host** (post HTML); **mermaid in the webview** (DOM-bound) | mermaid in the host (no DOM; headless mermaid is heavy/brittle); everything in the webview (ship the diff engine into the sandbox — harder to unit-test, INV-22 lost) |
|
||
| **Diff granularity** | **Block-level + word refinement in prose; code/mermaid atomic** (INV-23) | **pure inline source ins/del** — breaks on structural edits, renders half a mermaid fence; **DOM/SVG diffing** — complex, mermaid SVGs diff poorly |
|
||
| **Mermaid changes (v1)** | **Whole-diagram "changed" badge**; intra-diagram diff **deferred (#22)** | **intra-diagram node/edge diff now** — large unscoped design (source vs parsed-graph vs SVG; layout reflow) — explicitly deferred by the operator |
|
||
| **Relationship to F6** | **Coexist** — F7 is the markdown track-changes view; F6's native diff/gutter still serves **any file incl. code** | **replace F6** — F6 works on non-markdown where a rendered preview is meaningless; removing it loses coverage |
|
||
| **Asset delivery** | **Bundle mermaid as a webview-local asset** (esbuild second entry), strict CSP | **CDN/remote mermaid** — violates INV-21 (offline/CSP); **host-bundle mermaid** — bloats `extension.cjs` for code that only runs in the webview |
|
||
| **Open keybinding** | `ctrl+alt+r` (`when: editorLangId == markdown`) — "r" for *rendered*; unbound in stock VS Code | `ctrl+k v` (taken by the built-in preview); `cmd+…` (macOS-specific, the F6 §6.7 reasoning) |
|
||
|
||
### 6.8 Testing strategy
|
||
|
||
- **Unit (vitest, vscode-free):** `trackChangesModel` — `diffBlocks` /
|
||
`renderTrackChanges` over fixtures: pure-addition, pure-deletion, prose
|
||
modification (asserts inline `<ins>`/`<del>`), block insert/remove/reorder,
|
||
a **code** fence change (atomic — no inline ins/del, whole block `cw-changed`,
|
||
INV-23), a **mermaid** fence change (atomic + emits `<pre class="mermaid">` +
|
||
`cw-changed` badge class), unchanged doc (no marks), malformed block (error
|
||
chip, rest renders), determinism (same inputs → identical HTML, INV-22).
|
||
- **Host E2E (`@vscode/test-electron`, the 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 `<pre class="mermaid">`). |
|
||
| **Visual marking of removed elements** | **Ghost in place.** Removed nodes/edges/messages are **re-injected** into the emitted diagram, styled faded/dashed, so the deletion is visible in its original context. | The point of an intra-diagram diff is to *see* what left, where it was. A caption-only list loses position. Accepted cost: ghosts add to layout reflow. |
|
||
| **Diagram types this increment** | **Flowchart + sequence.** All other types fall back to the v1 badge. | The two most common types. Each later type is an additive follow-up on #22 (same dispatch seam). |
|
||
|
||
**Layout reflow (the fourth open question):** **accepted, not fought.** The
|
||
augmented new diagram is laid out fresh by mermaid; we do **not** attempt to pin
|
||
node positions to the baseline layout (mermaid exposes no stable layout pinning;
|
||
position-matching is a large unscoped effort). Ghosting keeps removed elements
|
||
present so the diff still reads; we do not promise the before/after diagrams are
|
||
spatially aligned.
|
||
|
||
### 11.3 Styling-hook asymmetry (a real mermaid constraint)
|
||
|
||
Flowcharts and sequence diagrams give very different styling surfaces, so the two
|
||
emitters differ:
|
||
|
||
- **Flowchart** — crisp per-element styling. Emit `classDef cwAdded/cwChanged/cwRemoved …`
|
||
once, then `class <ids> cwAdded` for nodes and `linkStyle <indices> stroke:…` for
|
||
edges (edges are addressed by their **declaration-order index**, which the parser
|
||
tracks). Ghost removed nodes/edges are appended to the source with the `cwRemoved`
|
||
class / a dashed `linkStyle`.
|
||
- **Sequence** — mermaid has **no per-message color directive**; its only
|
||
per-message styling hook is the **`rect rgb(r,g,b) … end`** background block. So
|
||
the sequence emitter rebuilds the message stream (ghosted-removed messages
|
||
re-inserted at their baseline position) and wraps each added / changed / removed
|
||
message in a one-message `rect` tinted green / amber / grey. Participants are
|
||
re-emitted (removed ones re-declared so they still appear).
|
||
|
||
Colors are **fixed, theme-neutral** values baked into the emitted source (mermaid
|
||
source can't read VS Code CSS variables): added ≈ green (`#2ea043`), changed ≈
|
||
amber (`#d29922`), removed ≈ muted grey + dashed (`#808080`), each chosen to read
|
||
on both light and dark mermaid themes. A small **legend** (`+ added · ~ changed ·
|
||
− removed`) is shown beneath a diffed diagram (host-emitted markup, not part of
|
||
the mermaid source).
|
||
|
||
### 11.4 Architecture & seam
|
||
|
||
A new **pure, vscode-free, DOM-free** host module tree (INV-22), dispatched from
|
||
the existing `changed`+atomic+mermaid branch of `renderOp`:
|
||
|
||
- **`src/mermaidDiff.ts`** — `diffMermaid(beforeSrc, currentSrc): MermaidDiffResult` —
|
||
detects the diagram type (`detectDiagramType`); routes to the flowchart or
|
||
sequence differ; returns `{ kind: "augmented"; source }` on success or
|
||
`{ kind: "fallback" }` for unsupported types / parse failure. Wraps the differ
|
||
in try/catch so **any** parser surprise degrades to the v1 badge (never throws —
|
||
the §6.9 error-chip philosophy). Owns the shared `CW_COLORS` palette.
|
||
- **`src/mermaidFlowchartDiff.ts`** — `parseFlowchart` (nodes by id, edges by
|
||
declaration order) + `diffFlowchart` (emits the augmented source).
|
||
- **`src/mermaidSequenceDiff.ts`** — `parseSequence` (participants + statements) +
|
||
`diffSequence` (LCS over statements via jsdiff `diffArrays`; `rect` tints + ghost
|
||
re-insertions).
|
||
|
||
`renderOp`'s changed-mermaid branch (`src/trackChangesModel.ts`) extracts the
|
||
fence body, calls `diffMermaid`; on `augmented` it emits `<pre class="mermaid">AUGMENTED</pre>`
|
||
+ the legend and drops the single-badge; on `fallback` it does exactly what it did
|
||
before. **The webview (`media/preview.ts`) needs no change** — the styling rides
|
||
inside the mermaid source. `media/preview.css` gains the legend swatch styles only.
|
||
|
||
### 11.5 Invariants (continuing the project sequence; F8 took 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.
|