Skip to content

Docs

Components

ApprovalCard

Human-in-the-loop approval: what will run, how risky it is, and deny with a reason.

Example

Approval required

High risk

Run command

Installs a package from the npm registry and updates package.json and pnpm-lock.yaml.

~/acme/chat-app
pnpm add @upstash/ratelimit

Interactive, and the same on the components page. Try the keyboard below on it.

Installation

npm i @dgesteves/agent-ui-kit ai
import { ApprovalCard, ToolApprovalCard } from '@dgesteves/agent-ui-kit';
// Once per app (with Tailwind v4: @import '@dgesteves/agent-ui-kit/tailwind.css'; in your CSS)
import '@dgesteves/agent-ui-kit/styles.css';

The styles are once per app; Getting started has the Tailwind v4 and plain CSS options.

Usage

// Bound to an AI SDK tool part in the approval flow:
<ToolApprovalCard part={part} onRespond={addToolApprovalResponse} risk="high" autoFocus />

// Or on its own:
<ApprovalCard
  toolName="run_command"
  input={{ command: 'pnpm add @upstash/ratelimit' }}
  risk="high"
  onApprove={() => approve()}
  onDeny={(reason) => deny(reason)}
/>

API reference

Generated from the types the package ships, so it matches the version you install.

ApprovalCard

Human-in-the-loop approval for a pending agent action. Shows what will run, how risky it is, and lets the user approve or deny by mouse or keyboard. Critical actions require a second confirming press.

Props

toolNamerequired
string

Name of the tool or action that needs approval.

title
ReactNode

Heading. Defaults to the humanized tool name.

description
ReactNode

Why the agent wants to do this, e.g. the SDK's approval.requestReason.

input
unknown

Arguments the action will run with.

risk
RiskLevelDefault 'medium'
'low' | 'medium' | 'high' | 'critical'
preview
ReactNode

Custom arguments preview. Defaults to a command line for { command } inputs, else JSON.

status
ApprovalStatusDefault 'pending'
'pending' | 'approved' | 'denied'

Approve and deny fire once: further presses (a double click, Y then N) are ignored until status changes, the promise the handler returned settles, or the handler throws.

reason
string

Reason recorded with the decision, shown once resolved.

automatic
booleanDefault false

A policy, not a person, made the decision (the SDK's approval.isAutomatic): once resolved, the card reads "Auto-approved" or "Blocked by policy" instead of "Approved" or "Denied".

onApprove
() => void | PromiseLike<void>

Fires once per decision: see status. May return a promise.

onDeny
(reason?: string) => void | PromiseLike<void>

Fires once per decision: see status. May return a promise.

shortcuts
booleanDefault true

Y / N shortcuts while focus is inside the card.

globalShortcut
booleanDefault false

Also approve with ⌘/Ctrl+Enter from anywhere on the page while pending.

autoFocus
booleanDefault false

Move focus to the card when it mounts in the pending state.

allowReason
booleanDefault true

Offer an optional free-text reason when denying.

approveLabel
stringDefault 'Approve'
denyLabel
stringDefault 'Deny'
headingLevel
HeadingLevelDefault 3

Heading level for the title, to fit your document outline.

Other props go to the root <section>: className (merged with tailwind-merge), id, aria-*, data-* and event handlers.

ToolApprovalCard

ApprovalCard bound to an AI SDK tool part in the approval flow. Renders nothing for parts without one, or while a policy's automatic decision is still arriving: nobody needs to act on it.

Props

partrequired
ToolPart
onRespondrequired
(response: ToolApprovalResponse) => void | PromiseLike<void>

Matches useChat().addToolApprovalResponse, so you can pass it directly.

meta
ToolMeta

Also takes every ApprovalCardProps prop above, except toolName, input, status, onApprove, onDeny, reason and automatic.

ToolApprovalResponse

idrequired
string
approvedrequired
boolean
reason
string

RiskLevel

type RiskLevel = 'low' | 'medium' | 'high' | 'critical';

ApprovalStatus

type ApprovalStatus = 'pending' | 'approved' | 'denied';

Accessibility

Keyboard
KeysAction
YApprove
NDeny
⌘/Ctrl↵Approve (page-wide with globalShortcut)
EscClose the reason field and return to the card
  • A <section> named by its title and described by its description and a visually hidden line that spells out the shortcuts.
  • Y and N only work while focus is inside the card, with no modifier and not while typing, which keeps them within WCAG 2.1.4. The buttons carry aria-keyshortcuts.
  • critical actions need a second press within four seconds, and say so: "Critical action. Press approve again to confirm."
  • Deciding announces "Approved" or "Denied" and moves focus to the card, so it isn’t lost when the buttons go away. autoFocus puts focus on a card that arrives pending.
  • Each pending approval sends one decision: a double click, or Y then N, is ignored until status changes or the handler’s promise settles.
  • The risk level is spelled out, not shown by color alone. headingLevel fits the title into your outline.

Theming

Every color, radius and font is a CSS variable, so the overrides in Theming apply, on :root or scoped to any element. These hooks are read from the component's markup.

Slots (data-slot)
approval-cardapproval-denyapproval-approve
State attributes
data-statuson approval-card'pending' | 'approved' | 'denied'data-riskon approval-card'low' | 'medium' | 'high' | 'critical'
Tokens it uses
--aui-accent--aui-accent-fg--aui-bg--aui-border--aui-border-strong--aui-fg--aui-fg-muted--aui-fg-subtle--aui-font-mono--aui-font-sans--aui-hot--aui-hot-fg--aui-on-accent--aui-on-hot--aui-radius--aui-ring--aui-surface--aui-surface-2--aui-warn--aui-warn-fg
app/globals.css
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='approval-card'] {
  --aui-radius: 4px;
}

[data-slot='approval-card'][data-status='pending'] {
  outline: 1px solid var(--aui-accent);
}