add spec ./specs/coauthoring-rendered-preview.md (status: graduated)
This commit is contained in:
@@ -0,0 +1,471 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
- **OPEN → later:** intra-diagram mermaid diffing (**#22**); preview→source
|
||||||
|
scroll-sync; non-markdown rendered views; per-author coloring in the preview;
|
||||||
|
whether F7 should eventually subsume F6's prose path.
|
||||||
|
- **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`.
|
||||||
Reference in New Issue
Block a user