Elements

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>
  );
}