Changes
ProThe review step after an agent edits code, the way Cursor, Windsurf and Claude Code show it: every file it touched with its diffstat, the diff behind it, and Keep or Undo per file and per hunk. A decided file folds away, and the keyboard walks you through the rest.
Changes
import { cookies } from "next/headers"
import { sign, verify } from "@/lib/auth/jwt"
Added: import { db } from "@/lib/db"
const SESSION_COOKIE = "koala_session"
Removed: const SESSION_TTL = 60 * 60 * 24
Added: const SESSION_TTL = 60 * 60 * 24 * 7
Added: const REFRESH_WINDOW = 60 * 60 * 24export async function getSession() {
const token = (await cookies()).get(SESSION_COOKIE)?.value
if (!token) return null
Removed: return verify(token)
Added: const session = await verify(token)
Added: if (!session) return null
Added:
Added: const expiresIn = session.exp * 1000 - Date.now()
Added: if (expiresIn < REFRESH_WINDOW * 1000) {
Added: await refreshSession(session.userId)
Added: }
Added:
Added: return db.user.findUnique({ where: { id: session.userId } })
}Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
A header with the title, the running diffstat, the file navigator and the bulk decision. Below it, one sheet of files: each row is the trigger (name, status, stat) plus its own Undo and Keep, and opens onto its hunks, each with an optional decision of its own.
import {
Changes,
ChangesHeader,
ChangesTitle,
ChangesSummary,
ChangesNav,
ChangesActions,
ChangesFiles,
ChangesFile,
ChangesFileTrigger,
ChangesFileName,
ChangesFileStatus,
ChangesFileStat,
ChangesFileActions,
ChangesFileDiff,
ChangesFileNote,
ChangesHunk,
ChangesHunkHeader,
ChangesHunkActions,
ChangesHunkCode,
} from "@/components/ui/changes"
<Changes>
<ChangesHeader>
<ChangesTitle />
<ChangesSummary />
<ChangesNav />
<ChangesActions />
</ChangesHeader>
<ChangesFiles>
<ChangesFile>
<ChangesFileTrigger>
<ChangesFileName />
<ChangesFileStatus />
<ChangesFileStat />
</ChangesFileTrigger>
<ChangesFileActions />
<ChangesFileDiff>
<ChangesFileNote />
<ChangesHunk>
<ChangesHunkHeader />
<ChangesHunkActions />
<ChangesHunkCode />
</ChangesHunk>
</ChangesFileDiff>
</ChangesFile>
</ChangesFiles>
</Changes>Per-hunk review#
Add ChangesHunkActions to a hunk and it can be kept or undone on its own. The decided hunk folds its code away and the same spot reads Kept or Undone, so the remaining hunks move up under your pointer. Decide the last hunk of a file and the file settles too: kept if any hunk was kept, undone if none was.
Changes
import { cookies } from "next/headers"
import { sign, verify } from "@/lib/auth/jwt"
Added: import { db } from "@/lib/db"
const SESSION_COOKIE = "koala_session"
Removed: const SESSION_TTL = 60 * 60 * 24
Added: const SESSION_TTL = 60 * 60 * 24 * 7
Added: const REFRESH_WINDOW = 60 * 60 * 24export async function getSession() {
const token = (await cookies()).get(SESSION_COOKIE)?.value
if (!token) return null
Removed: return verify(token)
Added: const session = await verify(token)
Added: if (!session) return null
Added:
Added: const expiresIn = session.exp * 1000 - Date.now()
Added: if (expiresIn < REFRESH_WINDOW * 1000) {
Added: await refreshSession(session.userId)
Added: }
Added:
Added: return db.user.findUnique({ where: { id: session.userId } })
}File states#
Five states, all painted below. Pending is the default and carries no chip, only the actions. Writing while streaming: the name sweeps and there is nothing to decide yet. Conflict when the file changed under the agent: still yours to decide, and never swept up by Keep all. Kept and Undone fold the row; an undone file is struck through and drops out of the summary.
Changes
You edited this file after Koala did. Keep overwrites your change to the plan picker; Undo leaves your version alone.
Streaming#
Render a file as soon as the agent starts writing it, with streaming on and the counts it has so far. It enters in place, its numbers roll as lines arrive, and its actions stay folded until it is finished, so nobody keeps half a file. Keep all and Undo all skip it too.
Changes
Keyboard#
With focus on a file row, the review runs from the keyboard. Keep or Undo moves focus to the next file still to review and, if the one you decided was open, opens it, so a review is a rhythm of reading and one key.
| J↓ | Next file |
| K↑ | Previous file |
| Y | Keep the focused file, then move to the next one to review |
| N | Undo the focused file, then move to the next one to review |
| EnterSpace | Open or close the focused file's diff |
Controlled#
Uncontrolled, Changes keeps its own record (seed it with defaultValue). Controlled, pass value and onValueChange and apply each decision to the working tree yourself. The record is flat: a file's path, or path#hunk for a single hunk.
Changes
- src/proxy.ts
- kept
In a chat#
Inside a Chat turn or an AI Panel, use variant="plain" and density="compact". The frame goes, the header sits on the message's own axis, and the files stay one quiet sheet under the answer, the same way Tool calls read in a turn.
Density#
Like Tool and Plan, Changes reads the density context or a density prop. It tunes the header (48px or 40px), the rows (44px or 40px) and the hunk headers; the header text always starts on the file names' left edge.
Changes
Changes
API reference#
Changes
The review, and the context every part reads: it owns the decisions, counts the registered files and handles the keyboard. Forwards native <div> props. Every part accepts className, merged last, and every part is droppable.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | Record<string, "kept" | "undone"> | - | Controlled decisions. Keys are a file's value, or file#hunk for a single hunk. A file with no key is still to review. |
| defaultValue | Record<string, "kept" | "undone"> | {} | Initial decisions when uncontrolled. |
| onValueChange | (value: ChangesValue) => void | - | Called with the whole next record on every decision. |
| variant | "card" | "plain" | "card" | card sets the file sheet into a tinted frame; plain drops the frame for a Chat turn or an AI Panel. |
| density | "comfortable" | "compact" | - | Header, row and hunk heights. Inherits the nearest DensityProvider (compact with none). |
| Data attribute | Description |
|---|---|
| aria-busy | Set while any file is streaming. |
ChangesHeader
The bar above the files. Wraps on a narrow screen, actions to the right. Renders a <div>.
ChangesTitle
What this review is. Renders an <h3>.
ChangesSummary
4 files · +128 −32, counted from the registered files that are not undone, so the numbers roll down as you undo. Pass children to word it yourself. Renders a <p>.
ChangesNav
Previous and next file still to review, with their keys in the tooltips. Disabled once nothing is left. Renders a role="group" <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| previousLabel | string | "Previous change" | Accessible name and tooltip. |
| nextLabel | string | "Next change" | Accessible name and tooltip. |
ChangesActions
The bulk decision: a ghost Undo all, then the primary Keep all. Pass children for your own buttons. Renders a <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| keepLabel | string | "Keep all" | The primary's label. |
| undoLabel | string | "Undo all" | The ghost's label. |
ChangesFiles
The sheet that holds the file rows, divided by hairlines. A role="list" <div>.
ChangesFile
One file the agent touched. A Radix Collapsible.Root (a listitem), so it forwards its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | Required. The file's id, and its path unless path is set. |
| path | string | - | The path shown by ChangesFileName. |
| additions | number | 0 | Lines added. Rolls when it changes. |
| deletions | number | 0 | Lines removed. Rolls when it changes. |
| streaming | boolean | false | The agent is still writing it: no decision yet, the name sweeps. |
| conflict | boolean | false | The file changed under the agent. Still reviewable, never kept by Keep all. |
| open | boolean | - | Controlled open state of the diff. |
| defaultOpen | boolean | false | Initial open state. |
| onOpenChange | (open: boolean) => void | - | Also called when a decision closes the file or the review opens the next one. |
| Data attribute | Description |
|---|---|
| data-status | pending, streaming, conflict, kept or undone. |
| data-state | open or closed. |
| data-value | The file's value. |
ChangesFileTrigger
The row's clickable line and the disclosure trigger. Adds the caret and the state in words for screen readers. Forwards Radix Collapsible.Trigger props.
ChangesFileName
A FileIcon, the file name in ink and its folder dimmed; the folder truncates first. Renders a <span>.
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | React.ReactNode | - | Replaces the FileIcon. |
| children | React.ReactNode | - | Replaces the name and folder. |
ChangesFileStatus
A chip for every state but pending: Writing, Conflict, Kept, Undone. Forwards Badge props.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | BadgeProps["variant"] | - | Overrides the tone the state picks. |
| size | "sm" | "md" | "lg" | "sm" | Badge size. |
| children | React.ReactNode | - | Overrides the wording. |
ChangesFileStat
+N −M in tabular-nums and a five-block bar of the ratio. Renders a <span>.
| Prop | Type | Default | Description |
|---|---|---|---|
| bar | boolean | true | Show the five-block bar. |
ChangesFileActions
Undo and Keep for the file, as icon buttons with their keys in the tooltips. Folds away once the file is decided or while it streams. Renders a <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| keepLabel | string | "Keep" | Accessible name and tooltip. |
| undoLabel | string | "Undo" | Accessible name and tooltip. |
| Data attribute | Description |
|---|---|
| data-hidden | Present while the actions are folded away. |
ChangesFileDiff
What the row opens: an optional note and the hunks. Tweens its height. Forwards Radix Collapsible.Content props.
ChangesFileNote
A line of context above the hunks, such as why a file conflicts. <strong> steps up to ink (warning ink on a conflict). Renders a <p>.
ChangesHunk
One hunk of the diff. Renders a <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | Required. Unique within its file; decided as file#hunk. |
| Data attribute | Description |
|---|---|
| data-status | pending, kept or undone (inherits the file's decision). |
| data-decided | Present once decided; the code folds away. |
ChangesHunkHeader
The @@ range and the enclosing symbol, in mono. Struck through when undone. Renders a <div>.
ChangesHunkActions
Undo and Keep for the hunk; once decided, the same spot reads Kept or Undone. Renders a <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| keepLabel | string | "Keep" | The keep button's label. |
| undoLabel | string | "Undo" | The undo button's label. |
ChangesHunkCode
The hunk's lines, a CodeSnippet set flush into the sheet. Takes every CodeSnippet prop: mark lines with diff={{ added, removed }}.
useChanges
The review's API, for your own controls: decide(file, decision), decideHunk(file, hunk, decision), keepAll(), undoAll(), stateOf(file), move(direction), focusFile(file) and pendingCount. Pass null as a decision to reopen a file.