FancyDiff
fancy-diffControlled split / inline diff viewer; per-hunk accept/reject with a merged result, render-prop customization, and a git unified-diff datasource.
Why
Reviewing a change is the moment a human stays in the loop — and in a Human+ app the change usually comes from an agent: it drafts an edit, the human reads it side by side and accepts or rejects it hunk by hunk. Every team rebuilds this surface (a diff view, a per-hunk gutter, an accept/reject toggle, a merged result) and most either ship a read-only diff with no acceptance, or wire acceptance to opaque DOM the agent can't drive.
FancyDiff is the trust-but-verify review surface both a human and an embedded agent operate through the same controlled state.What
FancyDiff is a controlled, client-side side-by-side (or inline) diff viewer with per-hunk acceptance and a live merged document. Acceptance is controlled — value is a Record<hunkId, "accepted" | "rejected" | "pending"> map, with onChange; onResult / getMergedResult() fold the diff + acceptance into the merged text (accepted add included, accepted remove dropped, accepted replace takes the after-lines, anything pending/rejected keeps the original). The source is a JSON-friendly discriminated union with three datasources: { before, after } (the in-house zero-dep LCS engine computes the hunks), { unified } (parse a git unified diff), or { diff } (a pre-built structured Diff). mode is "split" | "inline". Render-prop slots (renderHunk / renderToolbar / renderGutter) wrap rather than replace, and every hunk carries a stable data-fancy-diff-hunk handle so an agent reads/writes acceptance by id — never the DOM. Caveat: a git unified diff carries only the changed hunks plus a little context, so parsed files are flagged partial and getMergedResult() over them reconstructs only the lines in the diff window — feed full { before, after } documents when you need a fully merged file.How
Hold the acceptance map in state and render:
<FancyDiff source={{ before, after }} value={value} onChange={setValue} onResult={(r) => setMerged(r.text)} />. Import @particle-academy/fancy-diff/styles.css once. For a git diff, swap the source: source={{ unified }} mode="inline". Pull the merged document imperatively via a ref: const { text } = ref.current.getMergedResult(). With pendingMode, accept/reject become proposals (onProposal) so an agent stages a merge a human confirms; a sketched MCP bridge maps diff_accept_hunk / diff_reject_hunk onto the same value/onChange loop.