# Solution Design: Authorship view in the rendered preview (F9) | | | | --- | --- | | **Author(s)** | Ben Stull (with Claude) | | **Status** | `draft` | | **Version** | v0.1.0 | | **Anchor** | Feature **F9** (to be captured, ~#27) — builds on F7 `#21` (rendered preview) + F3 `#6` (live attribution). Surfaced as friction during F8 (`#25`) testing: "the preview doesn't show the Claude-composed annotations." | --- ## 1. Problem & context The F7 rendered track-changes preview (`src/trackChangesPreview.ts` + `src/trackChangesModel.ts`) answers **"what changed since the F6 baseline?"** It diffs `baselineText` vs the live buffer and renders author-*agnostic* `added`/`removed`/`changed` marks. It has never read F3 attribution. Two facts combine into the friction: 1. The F6 baseline **advances on every machine landing** (INV-18): when a Claude proposal is accepted, its text is folded *into* the baseline, so it then reads as **unchanged** in track-changes. 2. The preview has **no authorship axis** — it cannot say "Claude composed this span" vs "you did." So a writer who accepts Claude's edits cannot *see* what Claude contributed in the preview. F3 attribution already records exactly that (Claude=`agent` vs human spans, char-precise), and F8 made attribution available on **any** authorable doc (in-folder / out-of-folder / untitled) — the data exists; the preview just doesn't show it. **Goal:** add an **Authorship** mode to the preview that renders the current document with each span colored by its F3 author — Claude (blue) vs you (green) — inline and char-precise, reading attribution directly (baseline-independent), so Claude's contributions are visible even after the baseline absorbs them. ## 2. Solution overview The preview gains a **second mode**, switched by a **segmented control in the webview header** (`[ Track changes | Authorship ]`); mode is remembered per panel, default **Track changes** (today's behavior, unchanged). - **Track changes mode** — unchanged (`renderTrackChanges(baselineText, current)`). - **Authorship mode** — renders the **current buffer** (no baseline diff) via a new pure engine `renderAuthorship(currentText, authorSpans)`, wrapping each attributed span in `` / ``. A header **legend** (● Claude / ● You) appears in this mode. Text with no attribution record renders plain. The two modes are distinct renderings of different axes; authorship mode does **not** combine with the baseline diff (no `ins`/`del`, no removed text — there is no diff). ## 3. Technical design ### 3.1 The render engine (pure, vscode-free — extends `trackChangesModel.ts`) New export `renderAuthorship(currentText: string, spans: AuthorSpan[]): string`, where `AuthorSpan = { start: number; end: number; author: "claude" | "human" }` (char offsets into `currentText`, non-overlapping). Deterministic (INV-22 extends to it): same inputs → identical HTML; no vscode, no DOM. Algorithm: 1. **Block split with offsets.** Reuse `splitBlocks` but track each block's `[start, end)` char range in `currentText` (add the offsets to the `Block` shape, or a parallel `splitBlocksWithRanges`). Blank-line gaps between blocks are outside any block range (consistent with today's whitespace handling). 2. **Per block:** - **Prose block:** clip `spans` to the block's range, translate to block-local offsets, and inject paired **sentinel markers** at the local boundaries into the block's raw markdown — four Unicode Private-Use Area code points: `U+E000` (Claude-open) / `U+E001` (Claude-close), `U+E002` (human-open) / `U+E003` (human-close). PUA code points never appear in real content and markdown-it passes them through as plain text. Render the block with the existing markdown-it instance, then **post-process** the HTML: string-replace `U+E000`→``, `U+E001`→``, `U+E002`→``, `U+E003`→``. - **Code / mermaid fence (atomic, INV-23 extends → INV-27):** never inject sentinels inside a fence. If any span overlaps the fence, wrap the rendered fence in a block div carrying the author class + a small badge (`Claude` / `You` / `mixed` when both authors overlap); else render plain. 3. Concatenate block HTML (same join as `renderTrackChanges`). Each block keeps the existing per-block `try/catch` → error chip, so a render failure degrades to a visible chip, never a throw. **Known edge case (documented):** a sentinel placed immediately adjacent to active emphasis markers (`**`, `_`) can perturb markdown-it's inline parsing for that span. Spans from F3 fall on real edit boundaries, so this is rare; v1 accepts it (the per-block chip prevents any hard failure) and covers the common cases with tests. Snapping span boundaries to token-safe positions is a deferred refinement. ### 3.2 Data source & wiring - **Spans come from F3.** `AttributionController` already maintains live spans as current-buffer char ranges with author kind (`getSpans(key)` → `RenderedSpan[]`, `authorKind: "human" | "agent"`). Add `spansFor(document): AuthorSpan[]` to `AttributionController` — it computes the document key internally (`this.keyOf(document)` via the F8 router) and maps `agent→"claude"`, `human→"human"`. This resolves the key mismatch (the preview keys panels by `document.uri.toString()`; attribution keys by `keyOf`). - **Preview gains an attribution dependency.** `TrackChangesPreviewController` takes `AttributionController` in its constructor. In `extension.ts`, construct the authoring stack (router, guard, attribution) **before** the F7 controller so the dependency is available (a small, safe reorder — F8 already removed the no-root early return, so all controllers are constructed unconditionally). - **Mode state & toggle.** The controller holds `mode: Map` (default `"changes"`). `refresh(document)` renders the panel's current mode: track-changes (existing path) or authorship (`renderAuthorship(current, attribution.spansFor(document))`), posting `{type:"render", html, mode, epoch?, summary?, legend?}`. The webview header shows the segmented toggle; clicking posts `{type:"setMode", mode}` back; the controller updates the map and re-renders. Edits/baseline-changes re-render in the current mode. - **Webview (`media/`):** add the segmented toggle to `#cw-header` and a `setMode` message; render the posted HTML into `#cw-body` as today; show/hide the epoch+summary (track-changes) vs the legend (authorship) per mode. CSS adds `.cw-by-claude` (blue) / `.cw-by-human` (green) + the badge/legend styles, themed via VS Code CSS variables (consistent with the existing preview CSS). ### 3.3 Invariants (additions) - **INV-26 (authorship mode is baseline-independent):** authorship mode renders the **current buffer** colored by F3 author, reading `AttributionController`, never the F6 baseline. Track-changes mode is unchanged. The two are distinct modes, never combined. - **INV-27 (fences stay atomic in authorship too):** a code/mermaid fence is never marked inline; an overlapping author span yields a **block-level** author badge (extends INV-23). - **INV-28 (pure authorship render):** `renderAuthorship(currentText, spans)` is vscode-free and deterministic (extends INV-22) — spans are passed as data, so it unit-tests with no editor. - INV-19/20/21 (read-only preview, sealed webview, no persistence/network) carry over: authorship mode reads attribution, never mutates the document, sidecar, or baseline; no new LLM/network/credential surface. ## 4. Scope **In scope:** the Authorship mode + header toggle; `renderAuthorship` engine; `AttributionController.spansFor`; the preview↔attribution wiring + `extension.ts` reorder; webview toggle/legend/CSS; unit + host E2E + a manual-smoke addendum. **Out of scope / non-goals:** combining authorship with the diff in one view (rejected in design — two modes); marking removed text (no diff in authorship mode); the "untracked / always-there" third author class (rejected — only Claude & human are colored, unattributed renders plain); intra-emphasis sentinel-safety hardening (deferred); any change to F3 attribution capture, the seam, persistence, or the cross-rung contract. ## 5. Testing strategy - **Unit (vitest, vscode-free):** `renderAuthorship` — - single Claude span / single human span → correct wrapper class; - two authors in one prose paragraph → exact inline boundaries; - span clipped at a block boundary (spans one block only); - a code fence and a mermaid fence overlapping a span → block-level badge, no inner sentinels (atomic); - adjacent same-author spans; a span covering a whole block; - empty `spans` → plain render (equals markdown-it of the source); - determinism (same inputs → identical HTML). - **Host E2E (`@vscode/test-electron`, no LLM, extends the F7 suite):** open the preview on a markdown doc → land a Claude edit via the `proposeAgentEdit` seam + accept → set authorship mode → assert the posted model marks Claude's span as Claude-authored (and a human span as human). Assert track-changes mode still renders as before (regression). - **Manual smoke (`docs/MANUAL-SMOKE-F9.md`):** the webview visuals — blue/green colors, the header toggle, the legend, a mixed-author paragraph, a Claude-authored mermaid fence badge — verified by eye (the sealed webview's rendering isn't covered by the E2E). ## 6. Delivery slices - **SLICE-1** `renderAuthorship` + block-offset split in `trackChangesModel.ts` (the sentinel inject/post-process; atomic fences) + unit tests. - **SLICE-2** `AttributionController.spansFor` + wire it into `TrackChangesPreviewController` (constructor dep, `extension.ts` reorder) + per-panel mode state + the `setMode` message. - **SLICE-3** webview header toggle + legend + `.cw-by-*` CSS (`media/`). - **SLICE-4** host E2E (authorship-mode assertion + track-changes regression) + `docs/MANUAL-SMOKE-F9.md` + README F9 note. **Done =** authorship mode shows Claude's spans (blue) and yours (green) inline on a markdown doc; the header toggle flips modes; code/mermaid fences get a block badge; track-changes mode unchanged; unit + host E2E green; smoke performed once.