Diff
A proposed code change, split into hunks that can be accepted or rejected one at a time.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/diff-unified.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/diff-unified.json
Usage & API
Example
import { DiffUnified } from "@/components/ai/diff-unified"
<DiffUnified
path="…"
hunks={[]}
decisions={decisions}
onDecide={() => {}}
/>Props DiffUnified
| Prop | Type & description | Required |
|---|---|---|
path | string | Required |
hunks | Hunk[] | Required |
decisions | Record<string, HunkDecision> | Required |
onDecide | (hunkId: string, decision: HunkDecision) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Unified stacks the old and new numbers, the sign, and the text in one row. The sample path is src/lib/fetch-json.ts and there are two hunks. The first, from line 1, keeps the HttpError import, adds a sleep import, a blank line and const MAX_TRIES = 4. The second starts at old line 6 and new line 9, with context export async function fetchJson. It changes the loop bound from i < 3 to n < MAX_TRIES, adds a throw for status under 500, adds a doubling sleep, and replaces Request failed with a message that names MAX_TRIES. The body scrolls sideways rather than wrapping. Accept or Reject collapses that hunk. Undo opens it again. The reviewed count in the header updates as you go.
Example
import { DiffSplit } from "@/components/ai/diff-split"
<DiffSplit
path="…"
hunks={[]}
decisions={decisions}
onDecide={() => {}}
/>Props DiffSplit
| Prop | Type & description | Required |
|---|---|---|
path | string | Required |
hunks | Hunk[] | Required |
decisions | Record<string, HunkDecision> | Required |
onDecide | (hunkId: string, decision: HunkDecision) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Side by side uses the same header, the same hunk chrome, and the same Accept, Reject and Undo behaviour. Under the header, two columns are labelled Before and After, and the pair has a minimum width of 500 pixels so a narrow card scrolls instead of crushing the lines. Each hunk row is a grid of two cells. Context appears on both sides with its own line number. A removal sits on the left in rose, an addition on the right in green, and the other side is hatched. The sample is the same fetch-json file, with context shortened to fetchJson, no blank line in the first hunk, and a shorter replacement for the final throw: Gave up after retries. The accessible text on a changed cell says added or removed before the code.
Choosing a layout
Use it when the model has proposed a patch and a person should take some hunks and leave others. A retry loop, a new import, or a changed error string can be judged separately, which is the point of splitting the change into hunks instead of replacing the whole file. Store decisions in a record keyed by hunk id. Apply the accepted lines in your own code when the person is done. Rejecting only records the decision and hides the lines. It does not write the inverse patch.
A code block is the better fit when you are showing source to read and copy, with no old version and no accept step. A file tree is the better fit when the question is which paths changed and whether each was added, modified or deleted, not the line diff. Test results come after the edit, when the thing to show is the run. An artifact panel replaces a whole version and can preview it. It does not accept a hunk. Stay with the diff when the person has to keep some lines and drop others.
Where do the lines go after Accept or Reject?
They unmount. The hunk header stays, with Accepted or Rejected and Undo, and the line list animates closed. The added and removed totals in the card header do not drop, because they describe the proposed patch, not the remaining pending lines. Undo sets that hunk back to pending and opens the lines again. Nothing is written to disk either way.
Does the reviewed count treat a rejection as reviewed?
Yes. Any hunk whose decision is not pending counts, whether it was accepted or rejected. A missing entry in the decisions map is treated as pending. The label is announced with a polite live region, so a screen reader hears the count change after a click. There is no control in the header that decides every hunk at once.