24 / 59 · Agents
Approval card
Human in the loop: the agent asks before it runs anything with side effects.
Run command
The agent wants to run a shell command
pnpm vitest run --changed
Installation
npx shadcn@latest add "@assistant-ui/elements-approval-card"
Usage
import { ApprovalCard } from "@/components/elements/approval-card";
<ApprovalCard
state="request"
command="pnpm db:migrate"
title="Run migration"
subtitle="Needs your approval"
onAllowOnce={() => approve()}
onDeny={() => deny()}
/>Props
state*"request" | "running" | "done" | "denied"Lifecycle of the card: request with actions, running spinner, done check, or denied.
command*stringCommand shown in the mono field the agent wants to run.
title*stringHeadline naming the action that needs approval.
subtitle*stringSupporting line under the title explaining the request.
onAllowOnce() => voidCalled when the user clicks Allow once.
onAlwaysAllow() => voidCalled when the user clicks Always allow.
onDeny() => voidCalled when the user clicks Deny.
classNamestringExtra classes merged onto the root.
Source
approval-card.tsx"use client";
import { CheckIcon, Loader2Icon, TerminalIcon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, inkButton, paper } from "./surfaces";
export type ApprovalState = "request" | "running" | "done" | "denied";
export function ApprovalCard({
state,
command,
title,
subtitle,
onAllowOnce,
onAlwaysAllow,
onDeny,
className,
}: {
state: ApprovalState;
command: string;
title: string;
subtitle: string;
onAllowOnce?: () => void;
onAlwaysAllow?: () => void;
onDeny?: () => void;
className?: string;
}) {
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-3.5 rounded-[20px] p-4",
className,
)}
>
<div className="flex items-center gap-3">
<span className="bg-foreground/[0.05] text-foreground/45 flex size-9 shrink-0 items-center justify-center rounded-xl">
<TerminalIcon className="size-4" />
</span>
<div className="flex flex-col">
<p className="text-[13.5px] font-medium">{title}</p>
<p className="text-foreground/45 text-xs">{subtitle}</p>
</div>
</div>
<div
className={cn(
field,
"text-foreground/70 rounded-xl px-3.5 py-2.5 font-mono text-xs",
)}
>
{command}
</div>
<div className="flex h-8 items-center justify-end gap-2">
{state === "request" ? (
<>
<button
type="button"
onClick={onDeny}
className="text-foreground/55 hover:bg-foreground/[0.06] hover:text-foreground/90 h-8 rounded-full px-3.5 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
>
Deny
</button>
<button
type="button"
onClick={onAlwaysAllow}
className="text-foreground/55 hover:bg-foreground/[0.06] hover:text-foreground/90 h-8 rounded-full px-3.5 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
>
Always allow
</button>
<button
type="button"
onClick={onAllowOnce}
className={cn(
inkButton,
"flex h-8 items-center rounded-full px-3.5 text-xs font-medium",
)}
>
Allow once
</button>
</>
) : (
<div
key={state}
className="fade-in animate-in text-foreground/55 flex items-center gap-2 text-xs duration-300"
>
{state === "running" ? (
<>
<Loader2Icon className="text-foreground/45 size-3.5 animate-spin" />
Approved, running
</>
) : state === "denied" ? (
<>
<XIcon className="text-foreground/45 size-3.5" />
Denied
</>
) : (
<>
<CheckIcon className="size-3.5 text-emerald-500" />
Finished with exit 0
</>
)}
</div>
)}
</div>
</div>
);
}