Diff View
experimentalCode review for agent edits: a parsed unified diff with syntax and word highlights, line numbers, and per-hunk Accept / Reject with j/k/a/r keys.
Basic
minimal
Changes1 file+2−22 additions, 2 deletions0 of 2 reviewed
@@ −1,4 +1,4 @@
@@ −8,5 +8,5 @@export async function middleware(request: NextRequest) {
Review session
The agent's whole migration (4 files, 9 hunks) with controlled review state, a scrolling body, pinned file headers and a keyboard legend. Try j, k, a, r, u.
full
Review changes4 files+22−3322 additions, 33 deletions1 of 9 reviewed
JKmoveAacceptRrejectUundo1 accepted · 0 rejected
Structured files
Rename, new file and a binary file from structured input; read-only.
variant
Patch from test-writer3 files+4−14 additions, 1 deletions
@@ −3,3 +3,3 @@export class SessionStore
@@ −1,0 +1,3 @@
Binary file not shown.
Apply accepted changes
A footer bar with a Button acts on the accepted hunks.
composition
kura/auth-client1 file+2−22 additions, 2 deletions0 of 2 reviewed
@@ −1,4 +1,4 @@
@@ −8,5 +8,5 @@export async function middleware(request: NextRequest) {
Accept changes to apply them
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/diff-view.jsonCopies the source into components/kura/diff-view with its dependencies (kura-theme, button, code-block, icon-button, tooltip, use-controllable-state, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- Let a user review the edits a coding agent made (Claude Code, Codex, Cursor-style) before applying or committing them.
- Show `git diff` / `git show` output or a model-written patch as a readable, highlighted diff.
- Per-change approval: accept or reject individual hunks, a whole file, or everything, and act on the resulting decisions.
- A read-only diff of what changed in a run (reviewable={false}).
Reach for something else when
- Listing which files changed without showing contents — use file-changes (it composes well beside DiffView).
- Showing a single code snippet or a whole file — use code-block.
- Command or test output — use terminal.
- Approving a tool call or command before it runs — use approval-prompt.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| diff | string | — | A unified diff (`git diff`, `git show`, `diff -u`, a model's patch). Parsed with parseUnifiedDiff: renames, new/deleted/binary files, quoted paths, CRLF, missing counts, `\ No newline at end of file`, and wrong hunk counts are handled. Ignored when `files` is given. |
| files | readonly DiffFileInput[] | — | Structured files `{ path, oldPath?, status?, language?, binary?, hunks: [{ id?, oldStart?, newStart?, section?, lines }] }`. `lines` may be DiffLine objects or raw strings ("+added", "-removed", " context"); line numbers are computed. |
| title | React.ReactNode | "Changes" | Header title. |
| reviewable | boolean | true | Show per-hunk Accept / Reject, per-file Accept file, Accept all / Reject all and the “2 of 5 reviewed” tally. |
| review | DiffReview | — | Decisions (controlled): `{ [hunkId]: "accepted" | "rejected" }`; absent = pending. Hunk ids are `hunk.id` or `<path>#<index>` (see hunkId()). |
| defaultReview | DiffReview | {} | Initial decisions when uncontrolled. |
| onReviewChange | (review: DiffReview, change: DiffReviewChange) => void | — | Called with the full next review state and `{ path, hunkIds, decision }` where decision is "accepted", "rejected" or "pending" (undo). |
| collapsed | readonly string[] | — | Collapsed file paths (controlled). |
| defaultCollapsed | readonly string[] | [] | Initially collapsed file paths when uncontrolled. |
| onCollapsedChange | (collapsed: string[]) => void | — | Called when a file is collapsed or expanded (by click or by collapseReviewed). |
| collapseReviewed | boolean | true | Collapse a file once every hunk in it has a decision (like GitHub's “Viewed”). |
| autoAdvance | boolean | true | After a decision, move focus to the next pending hunk (Cursor-style review flow). |
| wrap | boolean | false | Soft-wrap long lines instead of scrolling each hunk sideways. |
| maxHeight | number | — | Scroll the files inside this height (px). File headers stay pinned while scrolling (also against page scroll without it). |
| actions | React.ReactNode | — | Extra controls at the end of the header, e.g. a Commit button. |
| empty | React.ReactNode | — | Rendered when there are no files. Default an icon + “No changes”. |
All native attributes of the root element, className and ref pass through.
Anatomy
- DiffView`data-state="empty" | "pending" | "reviewed"`, `data-reviewable`. A container query hides secondary header items below ~26–34rem.
data-slot="diff-view" - HeaderTitle, mono summary (`diff-view-summary`: files, +N −M), review progress (`diff-view-progress`) and Accept all / Reject all.
data-slot="diff-view-header" - File`<section aria-label={path}>` with `data-status="added" | "modified" | "deleted" | "renamed"` and `data-state="open" | "closed"`.
data-slot="diff-view-file" - FileHeaderSticky row: collapse toggle (button, aria-expanded), path (`diff-view-path`: dim directory + bold name, old → new for renames), status chip, +N −M, 5-block ratio bar (`diff-view-ratio`), Accept file / review state (`diff-view-file-review`).
data-slot="diff-view-file-header" - Hunk`role="group"`, roving tabindex, `data-hunk-id`, `data-review="pending" | "accepted" | "rejected"`. Accent bar on the left when focused.
data-slot="diff-view-hunk" - HunkHeader`@@ −a,b +c,d @@` + section heading, and Reject / Accept (`diff-view-hunk-actions`) or the decision (`diff-view-hunk-state`) with Undo.
data-slot="diff-view-hunk-header" - Line`data-kind="context" | "add" | "remove"`: old and new line numbers, sign, highlighted code with word-level change marks.
data-slot="diff-view-line"
Accessibility
- Each file is a labelled section; its header toggle is a button with aria-expanded/aria-controls.
- Hunks are focusable groups named “Change 2 of 9, src/auth/client.ts lines 10–19, pending review”; only the current hunk (and its buttons) is in the Tab order.
- Added/removed lines carry sr-only “added:” / “removed:” prefixes; colour is never the only signal (+/− signs, status chips, labels).
- Decisions are announced in a polite status region (“Accepted hunk in src/middleware.ts. 3 of 9 reviewed.”) — once per action, never per render.
- Accept / Reject tooltips show their single-key shortcuts.
- Reduced motion: Chevron rotation, the review-dimming fade and the progress colour change apply instantly; no motion is essential.
| Keys | Action |
|---|---|
| J / K | Next / previous hunk (skips collapsed files) |
| A | Accept the focused hunk, then move to the next pending one |
| R | Reject the focused hunk, then move to the next pending one |
| U / Backspace | Undo the focused hunk's decision |
| Enter / Space | On a file header: collapse or expand the file |
| Tab | Through file headers and the focused hunk's buttons |
Notes for agents
- Pass the raw `git diff` string to `diff`; don't pre-split it. Use `files` when you already have structured hunks (raw '+'/'-'/' ' strings are fine as lines).
- Review state is keyed by hunk id: `hunk.id` if you set one (do this when hunks can shift, e.g. while an agent keeps editing), else `<path>#<index>`. Export `hunkId(file, hunk, index)` to compute it.
- To apply decisions, walk parseUnifiedDiff(diff) and keep hunks whose id is "accepted"; rejected and pending hunks are not applied.
- Helpers exported alongside: parseUnifiedDiff, normalizeFiles, wordDiff, countChanges, languageFromPath.
- Syntax highlighting uses code-block's synchronous highlighter; the language is inferred from the file extension (override with files[].language).
- Very large diffs render every line; collapse big files with defaultCollapsed (e.g. lockfiles) to keep it fast.