Files
vscode-cowriting-plugin/docs/superpowers/specs/2026-06-11-authorship-preview-design.md
T
Ben Stull 8fa83f68c8 design(f9): authorship view in the rendered preview
F7 gains an Authorship mode (segmented header toggle) that renders the current doc
with each span colored by its F3 author (Claude blue / human green), inline and
char-precise via PUA sentinel injection; code/mermaid fences get a block-level
author badge. Baseline-independent (INV-26), reads AttributionController.spansFor.
INV-26..28. Surfaced as friction during F8 testing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:04:18 -07:00

10 KiB

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 <span class="cw-by-claude"> / <span class="cw-by-human">. 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<span class="cw-by-claude">, U+E001</span>, U+E002<span class="cw-by-human">, U+E003</span>.
    • 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<key, "changes"|"authorship"> (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.