Change Preview

Show exactly what an agent is about to change. The user can edit or skip each change, then approve or reject.

Pattern

≈ 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

Prospecting · Lookalike 3%
  • Resets the learning phase.

Retargeting · Cart abandoners

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:

app/api/chat/route.ts
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:

components/message.tsx
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

PropTypeNotes
titlestringWhat will happen, as an action: "Update 3 ad sets"
descriptionstring?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
changesChangeItem[]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
reversiblebooleanfalse shows a "Can't be undone" badge and a destructive confirm button
confirmLabelstringVerb for the confirm button

ChangeItem

FieldTypeNotes
idstringReturned in the result
labelstringField name
groupstring?Rows with the same group are listed under one heading
kindtext | longtext | number | currency | boolean | selectControls formatting and the editor
beforevalue | nullData-bound. null means the field is being created
aftervalue | nullnull means the field is being removed
editablebooleanDefault true
options{ value, label }[]Data-bound. Required for select
currencystringISO 4217 code, for currency
notestring?Why this change is proposed. Read to screen readers, not shown on screen
warningstring?A side effect the user must know about

Host props: status (streaming, ready, submitting, applied, rejected, revised), onResult, result and className.

Source

On this page