56 / Code
Reviewable Diff
A proposed change reviewed a hunk at a time: take three of the seven, leave the rest, apply what you took.
- -export function retry(run, attempts = 3) {+export function retry(run, attempts = 3, wait = 200) {let last
- } catch (error) {last = error+ sleep(wait * 2 ** at)}}
- export function onReconnect(socket, run) {+ socket.removeEventListener("open", run)socket.addEventListener("open", run)}
"use client"
import * as React from "react"
import { ReviewableDiff } from "mischief-ui/reviewable-diff"
const before = `export function retry(run, attempts = 3) {
let last
for (let at = 0; at < attempts; at += 1) {
try {
return run()
} catch (error) {
last = error
}
}
throw last
}
export function onReconnect(socket, run) {
socket.addEventListener("open", run)
}`
const after = `export function retry(run, attempts = 3, wait = 200) {
let last
for (let at = 0; at < attempts; at += 1) {
try {
return run()
} catch (error) {
last = error
sleep(wait * 2 ** at)
}
}
throw last
}
export function onReconnect(socket, run) {
socket.removeEventListener("open", run)
socket.addEventListener("open", run)
}`
export function ReviewableDiffDemo() {
const [applied, setApplied] = React.useState<number | null>(null)
return (
<div className="w-full max-w-xl space-y-3">
<ReviewableDiff
before={before}
after={after}
filename="src/upload/retry.ts"
context={2}
onApply={(hunks) => setApplied(hunks.length)}
/>
{applied !== null ? (
<p className="text-muted-foreground text-xs">
Applied {applied} {applied === 1 ? "hunk" : "hunks"}. The rest of the
patch is still here, unchanged.
</p>
) : null}
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/reviewable-diffimport { ReviewableDiff } from "mischief-ui/reviewable-diff"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react" import { diffLines, toHunks, type DiffHunk, type DiffLineKind,} from "@/registry/default/diff-view/diff-view"import { cn } from "@/lib/utils" export type ReviewableDiffProps = Omit< React.HTMLAttributes<HTMLDivElement>,Usage
export function Review({ before, after }) {
return (
<ReviewableDiff
before={before}
after={after}
filename="src/upload/retry.ts"
onApply={(hunks) => stage(hunks)}
/>
)
}All or nothing is the wrong shape
Diff View accepts or rejects a whole change, which is right when a person wrote it and knew what they meant. A patch from an agent is different: the part that fixes the bug and the part that misread the codebase usually arrive together.
Rejecting the lot to avoid one bad hunk throws away the work. Accepting the lot to keep the good hunk lets the bad one in. So the unit of the decision is the hunk, and apply hands back only the ones that were staged.
<ReviewableDiff
before={before}
after={after}
onApply={(hunks) => apply(hunks)}
/>It borrows the diff, not the display
The line comparison and the hunk splitting come from Diff View, which already exports both. Installing this installs that too, which is why it is a block rather than a component: one diff implementation arrives, not two that drift, and the one it sits on is worth having on its own.
Pass hunks directly when you have a real patch from git, and no comparison runs at all. Pass before and after, and it computes them with the context you ask for.
Saying how much is staged
The header counts staged hunks and the lines they carry, and it updates as boxes are ticked. Unstaged hunks stay visible and dimmed rather than disappearing, because a hunk that vanishes when you untick it makes the patch harder to reason about, not easier.
API
before, afterstringThe two versions, compared here.hunksreadonly DiffHunk[]Already split, when you have a real patch. Skips the comparison.filenamestringShown in the header.contextnumberUnchanged lines around a change. Defaults to 3.defaultStagedreadonly number[]Hunk indexes that start staged. Defaults to all of them.stagedreadonly number[]Hold the staged set yourself.onStagedChange(staged: number[]) => voidThe staged set changed.onApply(hunks: DiffHunk[]) => voidApply was pressed, with the staged hunks.applyLabelstringThe apply button's text....rootPropsHTMLAttributes<HTMLDivElement>Native root attributes.Accessibility
Each hunk is a native checkbox inside its own label, so the whole row is a target, the set is walkable with Tab and Space, and no ARIA is invented for a control the platform already has. The group carries a name that includes the filename. How much is staged is announced through a polite live region as hunks and lines rather than left to the header's colour, and an unstaged hunk is dimmed and unticked rather than hidden, so it stays readable and reachable.