Mischief

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.

src/upload/retry.ts3/3 hunks+3-1
  • -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)
    }
3 of 3 hunks staged, 3 added and 1 removed

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/reviewable-diff
import { 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.

registry/default/reviewable-diff/reviewable-diff.tsx
"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)}
/>
onApply receives the staged hunks, in file order, and nothing else.

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.