Change Preview
Show exactly what an agent is about to change. The user can edit or skip each change, then approve or reject.
≈ 1,401 tokens per use (tool 831 · guidance 304 · typical payload 266)How token costs are estimated
Example: Change Preview
Shift budget from the weak prospecting ad sets to retargeting.
Update 2 ad sets
Meta ad account: Acme Retail
When to use
- Before any write to an external system: records, settings, budgets, permissions or content.
- When the user should see exact before/after values and be able to edit them before applying.
- When several related changes are proposed together and the user may want only some of them.
When not to use
- The user asked a question. Answer it.
- There is no meaningful "before", e.g. sending a new message. Use a draft component instead.
- You can't read the current values. Fetch them first; never guess
before.
Installation
npx shadcn@latest add https://your-site.com/r/change-preview.json
This adds components/genui/change-preview.tsx and change-preview.schema.ts, and installs the shadcn primitives it uses.
Usage
Register the tool with your agent. The exported definition carries the schema and the usage guidance:
import { streamText, tool } from "ai"
import { changePreviewTool } from "@/components/genui/change-preview.schema"
const result = streamText({
model,
messages,
tools: {
// No execute(): the call is completed by the user's decision in the UI.
[changePreviewTool.name]: tool({
description: changePreviewTool.description,
inputSchema: changePreviewTool.inputSchema,
}),
},
})Render the tool call, and return the user's decision as the tool result:
import { ChangePreview } from "@/components/genui/change-preview"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-propose_changes") return null
return (
<ChangePreview
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "propose_changes",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{ action: "confirm", changes: [{ id, value, edited }], skipped: [id] }
{ action: "reject" }
{ action: "revise", note: "Cut the budget by 20%, not 40%" }Request changes (the message icon in the footer) lets the user steer the proposal instead of accepting or rejecting it outright. On revise, apply nothing: address the note and show a new Change Preview.
Apply only the returned values. If the user edited a value, edited is true and value holds their version.
For other models, convert the schema with z.toJSONSchema(changePreviewSchema).
With LangGraph
Push it from a graph node with ui.push({ name: "propose_changes", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
When a conversation is reloaded, pass the stored result back as result. The component renders its finished state (edits, skipped rows and the outcome) and never calls onResult again, so a decision can't be made twice. With the AI SDK, that's the tool call's output:
<ChangePreview
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string | What will happen, as an action: "Update 3 ad sets" |
description | string? | Context for the change. Read to screen readers, not shown |
target | { label, kind? } | Data-bound. The system or record being changed. kind is read to screen readers only |
changes | ChangeItem[] | At least one |
mode | "review" | "overwrite" | review lets the user skip changes individually; overwrite is all or nothing |
risk | "low" | "medium" | "high" | High adds a "High impact" badge and a destructive confirm button; low and medium look the same |
reversible | boolean | false shows a "Can't be undone" badge and a destructive confirm button |
confirmLabel | string | Verb for the confirm button |
ChangeItem
| Field | Type | Notes |
|---|---|---|
id | string | Returned in the result |
label | string | Field name |
group | string? | Rows with the same group are listed under one heading |
kind | text | longtext | number | currency | boolean | select | Controls formatting and the editor |
before | value | null | Data-bound. null means the field is being created |
after | value | null | null means the field is being removed |
editable | boolean | Default true |
options | { value, label }[] | Data-bound. Required for select |
currency | string | ISO 4217 code, for currency |
note | string? | Why this change is proposed. Read to screen readers, not shown on screen |
warning | string? | A side effect the user must know about |
Host props: status (streaming, ready, submitting, applied, rejected, revised), onResult, result and className.