# DiffReview

> Accept or reject an agent’s edits hunk by hunk, across files, and get the patched files back.

Source: https://agent-ui-kit-demo.vercel.app/docs/components/diff-review

## Installation

From npm:

```bash
npm i @dgesteves/agent-ui-kit ai
```

```tsx
import { DiffReview } from '@dgesteves/agent-ui-kit';
import '@dgesteves/agent-ui-kit/styles.css'; // once per app, or @import '@dgesteves/agent-ui-kit/tailwind.css' with Tailwind v4
```

Or as source, with the shadcn CLI:

```bash
npx shadcn@latest add @agent-ui-kit/diff-review
```

```tsx
import { DiffReview } from '@/components/agent-ui/diff-review';
```

## Usage

```tsx
<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

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `files` (required) | `readonly FileChange[]` |  |  |
| `title` | `ReactNode` | `'Review changes'` |  |
| `view` | `DiffViewMode` |  |  |
| `defaultView` | `DiffViewMode` ('unified' \| 'split') | `'unified'` |  |
| `onViewChange` | `(view: DiffViewMode) => void` |  |  |
| `decisions` | `Readonly<Record<string, HunkDecision>>` |  | Controlled decisions keyed by hunk id: `${path}:${index}`. A repeated path gets `${path}#2:${index}`, `#3` and so on, skipping a suffix another file's path already has. |
| `defaultDecisions` | `Readonly<Record<string, HunkDecision>>` |  |  |
| `onDecisionsChange` | `(decisions: Record<string, HunkDecision>) => void` |  |  |
| `onSubmit` | `(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. |
| `submitLabel` | `string` |  |  |
| `readOnly` | `boolean` | `false` | Hide review controls, e.g. once the review has been submitted. |
| `context` | `number` | `3` | Lines of context around each change. |
| `autoAdvance` | `boolean` | `true` | After accepting or rejecting with the keyboard, move to the next pending hunk. |
| `headingLevel` | `HeadingLevel` | `3` | Heading level for the title, to fit your document outline. |

Other props go to the root `<section>`: `className`, `id`, `aria-*`, `data-*` and event handlers.

### FileChange

A file edit proposed by an agent. Provide either full contents or a unified patch.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `path` (required) | `string` |  |  |
| `oldPath` | `string` |  | Previous path, for renames. |
| `oldContent` | `string` |  | Original file contents. Omit (or pass '') for new files. |
| `newContent` | `string` |  | Proposed file contents. Omit (or pass '') for deletions. |
| `patch` | `string` |  | A unified diff for this file, used when contents are not available. |
| `language` | `string` |  | Language id for highlighting, inferred from the extension when omitted. |

### DiffReviewResult

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `files` (required) | `DiffReviewFileResult[]` |  |  |
| `accepted` (required) | `number` |  |  |
| `rejected` (required) | `number` |  |  |
| `pending` (required) | `number` |  |  |

### DiffReviewFileResult

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `path` (required) | `string` |  |  |
| `content` (required) | `string` |  | File contents with only accepted hunks applied. `undefined` for patch-only input. |
| `accepted` (required) | `string[]` |  |  |
| `rejected` (required) | `string[]` |  |  |
| `pending` (required) | `string[]` |  |  |

### HunkDecision

```ts
type HunkDecision = 'pending' | 'accepted' | 'rejected';
```

### DiffViewMode

```ts
type DiffViewMode = 'unified' | 'split';
```

## Accessibility

| Keys | Action |
| --- | --- |
| J K | 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

Slots (`data-slot`): `diff-hunk`, `diff-review`, `diff-file`, `diff-submit`.

- `data-decision` on `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`. See https://agent-ui-kit-demo.vercel.app/docs/getting-started#theming.
