8fa83f68c8
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>
184 lines
10 KiB
Markdown
184 lines
10 KiB
Markdown
# 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.
|