F9: authorship view in the rendered preview #27
@@ -0,0 +1,183 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user