Contribute rewrite Phase 5: inline word-diff in manual pending card

The §8.11 manual-edit card was a stub — "{N} paragraphs edited
directly" plus a countdown and a Save-now button. The spec says the
card should grow one inline word-diff per touched paragraph as the
contributor types, in the same green/red register the Phase 3 accepted
overlay uses. Phase 5 lands that.

In RFCView.jsx, the 800ms-debounced handleEditorUpdate now computes a
per-paragraph token diff alongside the existing paragraph count and
stores both on manualPending. Baseline is the same Phase 4 gutter
source — originalSourceLinesRef.current, the last server-confirmed
body split on blank lines — so the card resets to empty the same
moment the gutter clears (accept/decline/manualFlush/branch-switch).

In ChangePanel.jsx, diffWords is exported (it was already the AI
card's inline-diff engine — token-level LCS over a whitespace-
preserving /(\\s+)/ split, ~30 lines, no runtime dep). The manual
card consumes the same tokens. Wholly-inserted paragraphs render as
all-insert blocks; wholly-deleted paragraphs as all-delete blocks.

Visual register is intentionally shared with Phase 3's preview
overlay: same #dcfce7/#166534 inserts, same #fee2e2/#991b1b
strike-through deletes. Selectors are scoped under .change-manual-diff
rather than reusing .markdown-preview .tracked-* since the card lives
outside the preview surface.

Pre-fancy stance, matching Phase 4's gutter: the diff is index-aligned
against the baseline, so adding a paragraph in the middle lights up
the rest of the doc. Tolerated as the "you've touched stuff below this
point" cue. An LCS-anchored future pass can fix it.

Verification gap matching Phases 2/3/4: backend was not running this
session, so the live RFCView → real-branch flow wasn't exercised.
Drove the Vite preview sandbox instead — mounted ChangePanel with a
hand-built diffs payload, confirmed three blocks render (mixed edit,
wholly-inserted, wholly-deleted), inserts/deletes carry the expected
computed colors, no console errors. Backend integration suite still
green (125 passed).
This commit is contained in:
Ben Stull
2026-05-25 11:29:03 -07:00
parent 886bbf5512
commit 4afb018bb0
3 changed files with 65 additions and 7 deletions
+19 -5
View File
@@ -39,7 +39,7 @@ import SelectionTooltip from './SelectionTooltip.jsx'
import { marked } from 'marked'
import PromptBar from './PromptBar.jsx'
import ChatPanel from './ChatPanel.jsx'
import ChangePanel from './ChangePanel.jsx'
import ChangePanel, { diffWords } from './ChangePanel.jsx'
import DiffView from './DiffView.jsx'
import PRModal from './PRModal.jsx'
import GraduateDialog from './GraduateDialog.jsx'
@@ -214,12 +214,25 @@ export default function RFCView({ viewer }) {
const current = splitSourceParagraphs(doc)
const baseline = originalSourceLinesRef.current || []
const len = Math.max(current.length, baseline.length)
let changed = 0
// §8.11 Phase 5: per-paragraph word-diff for the pending manual card.
// Naive index-aligned compare against the same baseline Phase 4's
// gutter uses; tolerates the insert-in-middle cascade as the "you've
// touched stuff below this point" cue. An LCS-anchored pass could
// refine this later.
const diffs = []
for (let i = 0; i < len; i++) {
if ((current[i] ?? '') !== (baseline[i] ?? '')) changed++
const a = baseline[i] ?? ''
const b = current[i] ?? ''
if (a === b) continue
diffs.push({
baselineIndex: i,
baselineText: a,
currentText: b,
tokens: diffWords(a, b),
})
}
if (changed > 0) {
setManualPending({ paragraphCount: changed })
if (diffs.length > 0) {
setManualPending({ paragraphCount: diffs.length, diffs })
setManualCountdown({ deadline: Date.now() + MANUAL_IDLE_MS })
} else {
setManualPending(null)
@@ -754,6 +767,7 @@ export default function RFCView({ viewer }) {
focusedChangeId={focusedChangeId}
manualPendingStatus={manualPending ? {
paragraphCount: manualPending.paragraphCount,
diffs: manualPending.diffs,
savingIn: manualCountdownLabel(manualCountdown),
onSaveNow: handleSaveNow,
} : null}