Diff Review
Review an agent’s edits across files before they land. A file tree with counts, accept or reject each change, each file or everything, comments on any line or range that go back to the agent, unified and split views, word-level highlights, undo, streaming edits and an “Edited 4 files” summary for the chat.
Coding agents change several files at once, and people need to keep some of it and throw the rest away. Diff Review puts every edit in one scrolling list with a file tree beside it, so you can accept or reject a single change, a whole file or everything, from the mouse or the keyboard. DiffSummary is the “Edited 4 files +120 −34” card for the chat, with the same actions on each file.
Each decision touches exactly one change. A decided change folds down to a one-line stub with its result, Show and Undo, and a file folds away once every change in it is decided. Bulk actions say how many changes they cover, and every decision, including Accept all and Reject all, can be undone with U or ⌘Z. “Next” always means the next change you haven’t decided yet. The counts and the progress bar come from what is still pending, so the review ends with a clear “All reviewed”.
Hover a line and press + in the gutter, or focus a line and press Enter or C, to comment on it. Drag or Shift-click line numbers, or use Shift+↑↓, to comment on a range. The composer opens as a small popover under the line, and a sent comment stays on the line as a thread you can edit, delete with undo, or collapse, with a count in the gutter. Removed lines are reported on the old side and added or unchanged lines on the new side, so the line numbers always match the file.
Pass a unified diff, or the content before and after and let the block compute the diff. With full content you can show the unchanged lines between changes, 20 at a time upward or downward, or all at once when fewer than 20 are hidden. Removed lines carry a fine striped bar in the gutter and added lines a solid one, so the difference never depends on color. Lines are highlighted with Shiki like Code Block, and changed words inside a line are marked when the two lines are similar enough for that to help. The layout switches to split view when there is room, which is at least 900px by default. Files that are still streaming show live and can’t be decided yet. Files that changed on disk can be rejected but not accepted. Diffs over 400 changed lines wait behind Load diff, and their file-level actions keep working.
Add the Pro registry to components.json
components.json Add your token
Create a token on your account page and put it in
.env.localasHEXTAUI_PRO_TOKEN.Add the block
pnpm dlx shadcn@latest add @hextaui-pro/diff-review
With the AI SDK
Turn edit tool calls into files, including input that is still streaming: a unified diff, a new file’s content, or Claude Code’s old_string and new_string. The summary goes in the chat and the review beside it, sharing one provider.
Send line comments to the agent
onComment gets the file, the side, the line numbers and a diff excerpt, so you can send it as a follow-up message. Return the promise from sendMessage and each comment shows Sending… and then Sent to the agent.
Write what was accepted
applyDecisions rebuilds each file from the hunks you kept, using the original or the edited content. It also returns a patch of only the accepted hunks, and says when a file should be deleted or never created.
A read-only diff
DiffView shows one file’s changes with the same highlighting, gutter bars and unchanged-line expansion, without review controls. It grows with its content, so put it inside a scrolling parent.
From a git diff
parsePatch splits git diff or plain unified diff output into files, with renames, new, deleted and binary files.
Anatomy
The parts you compose, from the outside in.
| Part | Description |
|---|---|
DiffReviewProvider | Holds the files, the decisions and the undo history, so a summary in the chat and a review panel stay in sync. |
DiffReview | The review surface: toolbar, file tree, the changes and the review bar with progress, navigation and bulk actions. |
DiffSummary | A card for the chat: “Edited 4 files” with counts, each file with accept and reject, and bulk actions. |
DiffView | A read-only diff of one file, for showing changes between versions. |
Comment threads | Comments under a line or range, with author, time, delivery status, edit, delete and collapse. |
parsePatch, applyDecisions | Read git diff output into files, and turn decisions back into file contents and an accepted-only patch. |
useDiffReview | The shared state, for building your own controls inside the provider. |
DiffReviewProvider
DiffReview and DiffSummary take the same props when used on their own.
| Prop | Type | Default |
|---|---|---|
filesThe changed files, in the order the agent edited them. | DiffFile[] | – |
decisionsControlled decisions by change id. Changes without an entry are pending. | Record<string, "accepted" | "rejected"> | – |
defaultDecisionsStarting decisions when uncontrolled. | Record<string, "accepted" | "rejected"> | – |
onDecisionsChangeCalled with the next decisions after every action, including undo. | (decisions) => void | – |
onDecideCalled once per action with the decision, its source (change, file, all or undo) and the changes it touched. Use it to write or revert files. | (event: DiffDecideEvent) => void | – |
commentsControlled comments. | DiffComment[] | – |
defaultCommentsStarting comments when uncontrolled. | DiffComment[] | – |
onCommentsChangeCalled after a comment is added, edited, deleted or restored. | (comments: DiffComment[]) => void | – |
onCommentCalled when a comment is sent, and again with the same id when it is edited. Return a promise to show Sending… and Sent to the agent, or Couldn’t send with Retry if it rejects. | (comment: DiffComment) => unknown | – |
authorShown on new comments. | { name: string; image?: string } | { name: "You" } |
| Prop | Type | Default |
|---|---|---|
pathThe file’s path. When an agent edits the same path twice, both edits are listed. | string | – |
patchA unified diff of this file. Hunk headers without line numbers, CRLF and “No newline at end of file” are fine. | string | – |
beforeThe original content. With after, the diff is computed for you and unchanged lines can be shown. | string | – |
afterThe edited content. On its own, it describes a new file. | string | – |
oldPathThe previous path of a renamed file. | string | – |
statusWorked out from the content when left out. | "added" | "deleted" | "modified" | "renamed" | – |
languageShiki language id. Defaults to the file extension. | string | – |
binaryShows “Binary file not shown” and decides the file as a whole. | boolean | – |
streamingThe agent is still writing this file. It updates live and can’t be decided yet. | boolean | – |
staleThe file changed on disk after the edit. Accepting is turned off until you pass a fresh diff. | boolean | – |
| Prop | Type | Default |
|---|---|---|
viewControlled layout. Auto is split when the diff area is at least 900px wide. Split falls back to unified below 600px. | "auto" | "unified" | "split" | – |
defaultViewStarting layout when uncontrolled. | "auto" | "unified" | "split" | "auto" |
onViewChangeCalled when someone picks a layout. | (view: "unified" | "split") => void | – |
advanceAfter deciding with the keyboard, move to the next change that is still pending. | boolean | true |
largeDiffLinesFiles with more changed lines than this wait behind Load diff. | number | 400 |
classNameGive it a height, or place it in a flex column. The list scrolls inside. | string | – |
| Prop | Type | Default |
|---|---|---|
onReviewShows Review and makes each file open the review at that file. | (path?: string) => void | – |
foldAfterFold longer lists behind “Show 3 more files”. | number | 6 |
| Prop | Type | Default |
|---|---|---|
pathThe file the comment is about. | string | – |
sideThe side of the last line: old for a removed line, new for an added or unchanged one. | "old" | "new" | – |
startSideThe side of the first line. It differs from side when a range runs from removed into added lines. | "old" | "new" | – |
linesLine numbers in the file on startSide and side. | { start: number; end: number } | – |
excerptThe selected lines in diff form, such as “-old” and “+new”, so the agent sees the code even after line numbers move. | string | – |
textWhat the reader wrote. | string | – |
id, createdAt, authorFilled in when the comment is created. | string, number, { name; image? } | – |
DiffView
Exported from diff-view.tsx. It has no scroll container of its own: it grows with its content and its change headers stick to the nearest scrolling parent, so place it in one, such as a min-h-0 flex-1 overflow-y-auto element.
| Prop | Type | Default |
|---|---|---|
beforeThe earlier content. | string | – |
afterThe later content. | string | – |
patchA unified diff, instead of before and after. | string | – |
languageShiki language id. Defaults to the extension of path. | string | – |
pathUsed for the language and the optional header. | string | "file" |
viewSplit falls back to unified below 600px. | "unified" | "split" | "unified" |
wrapWrap long lines. Split always wraps. | boolean | false |
headerShow the path and +N / −N counts above the changes. | boolean | false |
classNameClasses for the root. | string | – |
| Prop | Type | Default |
|---|---|---|
decisionPending when a decision was undone. | "accepted" | "rejected" | "pending" | – |
sourceWhat the person did. | "change" | "file" | "all" | "undo" | – |
changesOnly the changes whose decision actually changed. | { file: string; path: string; id: string }[] | – |
applyDecisions(file, decisions, options?)
Returns { path, decision, content?, deleted, patch }. Pending changes count as rejected unless options.pending is "accepted".
| Prop | Type | Default |
|---|---|---|
contentThe file after review, built from before or after. Undefined for binary files or when neither is known. | string | undefined | – |
deletedTrue when the file shouldn’t exist: an accepted deletion or a rejected new file. | boolean | – |
patchThe accepted hunks only, renumbered so git apply works on the original file. | string | – |
| Key | Action |
|---|---|
| J | Moves to the next change that is still pending. K moves back. |
| N | Moves to the next file. P moves to the previous one. |
| Y | Accepts the current change. ⌘↵ does the same. |
| X | Rejects the current change. ⌘⌫ does the same. |
| ⇧Y | Accepts every pending change in the current file. ⇧X rejects them. |
| ⌘⇧↵ | Accepts every change that is ready. ⌘⇧⌫ rejects them. |
| U | Undoes the last decision, including bulk ones. ⌘Z does the same. |
| ↑↓ | Moves through the file tree. Enter jumps to the file, ← and → fold folders, and typing jumps to a file by name. |
| Tab | Moves into a change’s lines. Each change is one stop, and ↑ ↓ Home End move between its lines; in split view ← → switch sides. |
| ⇧↓ | Selects a range of lines. ⇧↑ extends it upward, and Esc clears it. |
| Enter | Opens the comment composer for the focused line or the selected range. C does the same. |
| ⌘↵ | Sends the comment from the composer. Esc closes it and keeps what you wrote for that line. |
- The review is a region named “Review changes”. Each change is a group with a full name, such as “Change 2 of 9, app/page.tsx, lines 40–52, 3 lines added, 1 line removed, pending”.
- Decisions, bulk actions and undo are announced through a polite live region with what is left, for example “Accepted change 2 of 9 in app/page.tsx. 7 changes left.”
- Shortcuts only work while focus is inside the review. They are ignored in text fields, and letters in the file tree jump to files by name. After deciding with the keyboard, focus moves to the next pending change. After clicking, it moves to the Undo button in the same place.
- Added and removed lines are read as “Added:” and “Removed:”, so they don’t rely on color. Changed words are underlined in high contrast mode, and progress is also exposed as a progressbar.
- Folded changes are inert, so they are skipped by Tab and screen readers until you show them. With reduced motion, folding, counts and progress update without animation.
- Each change’s lines are one tab stop with roving focus, like a grid. The focused line shows a tint and a + in the gutter, and forced colors mode draws an outline instead.
- Opening the composer moves focus into it and the line stays highlighted. Esc returns focus to the line visibly. After sending or clicking away, focus goes back to the line without a focus indicator, so the next arrow key continues from there.
- Removed and added lines carry a striped or solid gutter bar as well as the − and + signs, and the bars stay visible in forced colors mode.
- Adding, editing, deleting and restoring comments are announced, and deleted comments can be restored for a few seconds.
Code
13 files, added to components/blocks/diff-review.