Contribute rewrite Phase 3: tracked-change overlay in MarkdownPreview

Reintroduces the §8.10 inline tracked-change layer Phase 1 dropped when
Tiptap left, this time anchored to the rendered preview rather than a
writable editor. Each accepted change on the branch decorates the
preview DOM with a `<span class="tracked-insert">` at the proposed text
and a `<span class="tracked-delete">` strikethrough for the original;
hover surfaces the same ChangeTooltip DiffView uses (badge + prompt +
quote + reason), now extracted to its own file so both surfaces share
the affordance.

Design calls per the Phase 3 prompt's open list:
  • Contribute pane: default-on. The pane exists for editorial review
    of your own work; the overlay reinforces that.
  • Discuss pane: opt-in via a new "Show tracked changes" toolbar
    toggle on non-main branches. Clean prose stays the default.
  • DiffView's "Review changes" toolbar is untouched — DiffView dies in
    Phase 7 and overloading its toggle now creates surface to unwind.

Pre-fancy stance on the known overlay subtleties:
  • Drift — if `proposed` no longer appears verbatim (later manual
    edit touched it) we skip the decoration rather than mis-anchor.
  • Overlap — decorate in `acted_at` ascending order; later spans win
    on whatever text is still un-wrapped.
  • Mermaid — text nodes inside `.mermaid-block` are skipped; a tracked
    span that intersects diagram source drops silently. Flagged as a
    known limitation rather than worked around.
  • Code — `<pre>`/`<code>` parents skipped too; matching inside
    verbatim code produced noisy false positives in fixtures.

Backend tests: 125 passed. Helper verified via Vite preview sandbox
eval across eight cases (single-paragraph match, distinct-original
strike, no-match skip, mermaid skip, two-change overlap ordering,
declined/pending ignored, whitespace-normalized cross-newline match,
code-block skip), plus computed-style proof for the new
`.markdown-preview .tracked-insert` / `.tracked-delete` rules. The
end-to-end CM6 → accept → overlay flow against a live branch wasn't
exercised (backend not running this session); the simpler unit-level
verification looked clean, but a future session with the backend up
should drive that golden path before Phase 4 piles on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-25 10:44:09 -07:00
parent ee6e3491e7
commit 2a7c099a33
7 changed files with 407 additions and 95 deletions
+26 -22
View File
@@ -842,29 +842,33 @@ click-to-card binding from §8.8. The margin marker is scannable
precise ("what changed here?"). The two answer different questions
and both are kept.
The inline markup is session-local. Each accepted change is already
a clean commit on Gitea per §8.6, so the branch's canonical state at
any reload is the integrated text without markup. Regenerating
markup on load by diffing against earlier commits is technically
possible but adds a mechanism — a per-user seen-cursor for accepted
changes, an explicit dismiss UI — to solve a problem that DiffView
already solves durably and at higher fidelity. The editor is for
writing; layering permanent diff overlay on top of writable text
degrades the writing surface, so the markup clears on reload and
DiffView is the durable artifact for inspecting accepted changes in
context.
The inline markup lives in the rendered preview surface, not on the
writable editor. With the Contribute-mode split (§8.3) the raw markdown
source is a CodeMirror buffer that can't host HTML decorations, and
even when a writable surface could host them — as Tiptap did in earlier
revisions — layering permanent diff overlay on top of writable text
degraded the writing surface. The preview pane is the natural home: on
the right side of the Contribute split it shows the editor's own work
in-context (default-on, since the pane exists for that editorial
review), and in Discuss mode the single preview pane gates the overlay
behind a toolbar toggle so the default reading experience stays clean
prose. The overlay regenerates on every render from the branch's
`changes` table — no per-user seen-cursor, no dismiss UI; every
accepted change on the branch is included, drift is tolerated by
skipping spans whose `proposed` text no longer matches verbatim, and
mermaid intersections are skipped as a known limitation. Hovering any
marked span surfaces a tooltip with the change's type badge (`ai` or
`manual`), the model identifier where applicable, the
`was_edited_before_accept` flag where set, the user prompt and
selection-quote that drove the change, and the AI's `reason`.
DiffView is the read-only render surface invoked via a toolbar
toggle (§8.15). It reads from the
`changes` table for the branch, reconstructs the markup for every
accepted change in branch history, and renders the result in-place
where the editor was. Hovering any marked span surfaces a tooltip
with the change's type badge (`ai` or `manual`), the model
identifier where applicable, the `was_edited_before_accept` flag
where set, the user prompt and selection-quote that drove the
change, and the AI's `reason`. The toggle is reversible; returning
to the editor restores the live writing surface and reattaches the
session-local baseline.
DiffView is the legacy read-only render surface invoked via the
Contribute-mode toolbar toggle (§8.15) — a full-editor swap-in that
reads the same `changes` table. It is retained as an interim path while
the preview-pane overlay matures and is slated for retirement once
gutter markers (the §8.10 paragraph-margin layer) land in the preview
itself; at that point both visual layers live in the same surface and
the toolbar toggle collapses.
### 8.11 Manual edits and collisions with AI proposals