Components
DiffReview
Accept or reject an agent’s edits hunk by hunk, across files, and get the patched files back.
Example
Review changes
2 files · 4 hunks · +21 −1
2/4 reviewed · unreviewed hunks are skipped
Keyboard: J or K to move between hunks, A to accept, R to reject, U to reset, Shift A or Shift R for all hunks, Ctrl Enter to apply.
Interactive, and the same on the components page. Try the keyboard below on it.
Installation
npm i @dgesteves/agent-ui-kit aiimport { DiffReview } from '@dgesteves/agent-ui-kit';
// Once per app (with Tailwind v4: @import '@dgesteves/agent-ui-kit/tailwind.css'; in your CSS)
import '@dgesteves/agent-ui-kit/styles.css';npx shadcn@latest add @agent-ui-kit/diff-reviewimport { DiffReview } from '@/components/agent-ui/diff-review';The styles are once per app; Getting started has the Tailwind v4 and plain CSS options.
Usage
<DiffReview
files={[{ path: 'app/api/chat/route.ts', oldContent, newContent }]}
// Each file comes back with only the accepted hunks applied.
onSubmit={(result) => addToolOutput({ tool: 'review_changes', toolCallId, output: result })}
/>API reference
Generated from the types the package ships, so it matches the version you install.
DiffReview
Review agent file edits hunk by hunk, in unified or split view, with word-level highlights. Keyboard: J/K or arrows move between hunks, A accepts, R rejects, U resets, Shift+A / Shift+R decide all, ⌘/Ctrl+Enter applies.
Props
filesrequiredreadonly FileChange[]titleReactNodeDefault'Review changes'viewDiffViewModedefaultViewDiffViewModeDefault'unified''unified' | 'split'onViewChange(view: DiffViewMode) => voiddecisionsReadonly<Record<string, HunkDecision>>Controlled decisions keyed by hunk id:
${path}:${index}. A repeated path gets${path}#2:${index},#3and so on, skipping a suffix another file's path already has.defaultDecisionsReadonly<Record<string, HunkDecision>>onDecisionsChange(decisions: Record<string, HunkDecision>) => voidonSubmit(result: DiffReviewResult) => void | PromiseLike<void>Called with the reviewed result. Pending hunks are not applied. Fires once per set of decisions, so a double click or a repeated ⌘/Ctrl+Enter sends one review. Submitting again needs a changed decision, changed
files, the returned promise to settle, or the handler to throw. Files compare by content and decisions by value: an equal new array or object does not count as a change.submitLabelstringreadOnlybooleanDefaultfalseHide review controls, e.g. once the review has been submitted.
contextnumberDefault3Lines of context around each change.
autoAdvancebooleanDefaulttrueAfter accepting or rejecting with the keyboard, move to the next pending hunk.
headingLevelHeadingLevelDefault3Heading level for the title, to fit your document outline.
Other props go to the root <section>: className (merged with tailwind-merge), id, aria-*, data-* and event handlers.
FileChange
A file edit proposed by an agent. Provide either full contents or a unified patch.
pathrequiredstringoldPathstringPrevious path, for renames.
oldContentstringOriginal file contents. Omit (or pass '') for new files.
newContentstringProposed file contents. Omit (or pass '') for deletions.
patchstringA unified diff for this file, used when contents are not available.
languagestringLanguage id for highlighting, inferred from the extension when omitted.
DiffReviewResult
filesrequiredDiffReviewFileResult[]acceptedrequirednumberrejectedrequirednumberpendingrequirednumber
DiffReviewFileResult
pathrequiredstringcontentrequiredstringFile contents with only accepted hunks applied.
undefinedfor patch-only input.acceptedrequiredstring[]rejectedrequiredstring[]pendingrequiredstring[]
HunkDecision
type HunkDecision = 'pending' | 'accepted' | 'rejected';DiffViewMode
type DiffViewMode = 'unified' | 'split';Accessibility
| Keys | Action |
|---|---|
| JK | Next or previous hunk (↓ and ↑ too, from a hunk) |
| A | Accept the hunk, and move to the next undecided one |
| R | Reject the hunk, and move on (X works too) |
| U | Reset the hunk |
| ⇧A⇧R | Accept or reject every hunk |
| ⌘/Ctrl↵ | Apply the review |
- A
<section>named by its title. Hunks use a roving tabindex, so the whole review is one tab stop that J, K and the arrows move through. - Each hunk is a group named like "Hunk 2 of 4, app/api/chat/route.ts, lines 12 to 20, accepted".
- Every shortcut is also a button: "Accept hunk 2", "Reject hunk 2" and "Reset hunk 2" with
aria-pressed, the layout toggle and Apply. Shortcuts only work while focus is inside the review. - Progress is announced: "Hunk 2 of 4 accepted. 2 remaining."
- Changed lines keep their + and − glyphs and are read as "Added:" or "Removed:", so cyan and magenta never carry the meaning alone.
Theming
Every color, radius and font is a CSS variable, so the overrides in Theming apply, on :root or scoped to any element. These hooks are read from the component's markup.
- Slots (data-slot)
- diff-hunkdiff-reviewdiff-filediff-submit
- State attributes
- data-decisionon
diff-hunk'pending' | 'accepted' | 'rejected'data-line - Tokens it uses
- --aui-accent--aui-accent-fg--aui-add-bg--aui-add-strong--aui-bg--aui-border--aui-border-strong--aui-del-bg--aui-del-strong--aui-fg--aui-fg-muted--aui-fg-subtle--aui-font-mono--aui-font-sans--aui-hot--aui-hot-fg--aui-on-accent--aui-radius--aui-ring--aui-surface--aui-surface-2--aui-warn--aui-warn-fg
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='diff-review'] {
--aui-radius: 4px;
}
[data-slot='diff-hunk'][data-decision='pending'] {
outline: 1px solid var(--aui-accent);
}