Changes

Pro

The 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

@@ -1,5 +1,7 @@
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 * 24Added: const SESSION_TTL = 60 * 60 * 24 * 7Added: const REFRESH_WINDOW = 60 * 60 * 24
@@ -24,5 +26,13 @@ export async function getSession()
export async function getSession() {  const token = (await cookies()).get(SESSION_COOKIE)?.value  if (!token) return nullRemoved:   return verify(token)Added:   const session = await verify(token)Added:   if (!session) return nullAdded: 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#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Activate your Pro license (once per machine)npx koalaui-cli@latest login <key># Add this component (its dependencies come along)npx koalaui-cli@latest add changes

Usage#

This example is part of Koala UI Pro

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

@@ -1,5 +1,7 @@
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 * 24Added: const SESSION_TTL = 60 * 60 * 24 * 7Added: const REFRESH_WINDOW = 60 * 60 * 24
@@ -24,5 +26,13 @@ export async function getSession()
export async function getSession() {  const token = (await cookies()).get(SESSION_COOKIE)?.value  if (!token) return nullRemoved:   return verify(token)Added:   const session = await verify(token)Added:   if (!session) return nullAdded: 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
YKeep the focused file, then move to the next one to review
NUndo the focused file, then move to the next one to review
EnterSpaceOpen 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.

Sessions log people out after a day. Can they last a week?

Done. Sessions now last seven days and refresh quietly in the last one, and the proxy reads the same session as the API, so the two can't drift.

Changes

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.

PropTypeDefaultDescription
valueRecord<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.
defaultValueRecord<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 attributeDescription
aria-busySet 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>.

PropTypeDefaultDescription
previousLabelstring"Previous change"Accessible name and tooltip.
nextLabelstring"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>.

PropTypeDefaultDescription
keepLabelstring"Keep all"The primary's label.
undoLabelstring"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.

PropTypeDefaultDescription
valuestring-Required. The file's id, and its path unless path is set.
pathstring-The path shown by ChangesFileName.
additionsnumber0Lines added. Rolls when it changes.
deletionsnumber0Lines removed. Rolls when it changes.
streamingbooleanfalseThe agent is still writing it: no decision yet, the name sweeps.
conflictbooleanfalseThe file changed under the agent. Still reviewable, never kept by Keep all.
openboolean-Controlled open state of the diff.
defaultOpenbooleanfalseInitial open state.
onOpenChange(open: boolean) => void-Also called when a decision closes the file or the review opens the next one.
Data attributeDescription
data-statuspending, streaming, conflict, kept or undone.
data-stateopen or closed.
data-valueThe 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>.

PropTypeDefaultDescription
iconReact.ReactNode-Replaces the FileIcon.
childrenReact.ReactNode-Replaces the name and folder.

ChangesFileStatus

A chip for every state but pending: Writing, Conflict, Kept, Undone. Forwards Badge props.

PropTypeDefaultDescription
variantBadgeProps["variant"]-Overrides the tone the state picks.
size"sm" | "md" | "lg""sm"Badge size.
childrenReact.ReactNode-Overrides the wording.

ChangesFileStat

+N −M in tabular-nums and a five-block bar of the ratio. Renders a <span>.

PropTypeDefaultDescription
barbooleantrueShow 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>.

PropTypeDefaultDescription
keepLabelstring"Keep"Accessible name and tooltip.
undoLabelstring"Undo"Accessible name and tooltip.
Data attributeDescription
data-hiddenPresent 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>.

PropTypeDefaultDescription
valuestring-Required. Unique within its file; decided as file#hunk.
Data attributeDescription
data-statuspending, kept or undone (inherits the file's decision).
data-decidedPresent 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>.

PropTypeDefaultDescription
keepLabelstring"Keep"The keep button's label.
undoLabelstring"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.

FAQ#

No. It is the review surface: it records what you decided and tells you through onValueChange. Writing the kept version or restoring the old one is your agent's job, with whatever file system or git access it has.