Components
ApprovalCard
Human-in-the-loop approval: what will run, how risky it is, and deny with a reason.
Example
Approval required
High riskRun command
Installs a package from the npm registry and updates package.json and pnpm-lock.yaml.
Interactive, and the same on the components page. Try the keyboard below on it.
Installation
npm i @dgesteves/agent-ui-kit aiimport { 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';npx shadcn@latest add @agent-ui-kit/approval-cardimport { ApprovalCard, ToolApprovalCard } from '@/components/agent-ui/approval-card';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
toolNamerequiredstringName of the tool or action that needs approval.
titleReactNodeHeading. Defaults to the humanized tool name.
descriptionReactNodeWhy the agent wants to do this, e.g. the SDK's
approval.requestReason.inputunknownArguments the action will run with.
riskRiskLevelDefault'medium''low' | 'medium' | 'high' | 'critical'previewReactNodeCustom arguments preview. Defaults to a command line for
{ command }inputs, else JSON.statusApprovalStatusDefault'pending''pending' | 'approved' | 'denied'Approve and deny fire once: further presses (a double click, Y then N) are ignored until
statuschanges, the promise the handler returned settles, or the handler throws.reasonstringReason recorded with the decision, shown once resolved.
automaticbooleanDefaultfalseA 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.shortcutsbooleanDefaulttrueY / N shortcuts while focus is inside the card.
globalShortcutbooleanDefaultfalseAlso approve with ⌘/Ctrl+Enter from anywhere on the page while pending.
autoFocusbooleanDefaultfalseMove focus to the card when it mounts in the pending state.
allowReasonbooleanDefaulttrueOffer an optional free-text reason when denying.
approveLabelstringDefault'Approve'denyLabelstringDefault'Deny'headingLevelHeadingLevelDefault3Heading 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
partrequiredToolPartonRespondrequired(response: ToolApprovalResponse) => void | PromiseLike<void>Matches
useChat().addToolApprovalResponse, so you can pass it directly.metaToolMeta
Also takes every ApprovalCardProps prop above, except toolName, input, status, onApprove, onDeny, reason and automatic.
ToolApprovalResponse
idrequiredstringapprovedrequiredbooleanreasonstring
RiskLevel
type RiskLevel = 'low' | 'medium' | 'high' | 'critical';ApprovalStatus
type ApprovalStatus = 'pending' | 'approved' | 'denied';Accessibility
| Keys | Action |
|---|---|
| Y | Approve |
| N | Deny |
| ⌘/Ctrl↵ | Approve (page-wide with globalShortcut) |
| Esc | Close 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. criticalactions 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.
autoFocusputs focus on a card that arrives pending. - Each pending approval sends one decision: a double click, or Y then N, is ignored until
statuschanges or the handler’s promise settles. - The risk level is spelled out, not shown by color alone.
headingLevelfits 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-riskonapproval-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
/* 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);
}