-

Tool Call Card

PreviousNext

A structured card for showing ZGI agent tool calls, arguments, results, duration, and status.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/tool-call-card.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/tool-call-card.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/tool-call-card.json

Component

components/ui/tool-call-card.tsx
"use client"

import * as React from "react"
import {
  AlertCircleIcon,
  CheckCircle2Icon,
  ChevronDownIcon,
  Clock3Icon,
  CopyIcon,
  Loader2Icon,
  WrenchIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

export type ToolCallStatus = "queued" | "running" | "success" | "error"
export type ToolCallPayload =
  | React.ReactNode
  | Record<string, unknown>
  | unknown[]

export interface ToolCallCardProps
  extends Omit<React.ComponentProps<"div">, "title"> {
  tool: string
  status: ToolCallStatus
  title?: React.ReactNode
  description?: React.ReactNode
  duration?: React.ReactNode
  input?: ToolCallPayload
  output?: ToolCallPayload
  defaultOpen?: boolean
  onCopy?: () => void
}

const statusConfig: Record<
  ToolCallStatus,
  {
    label: string
    icon: React.ComponentType<{ className?: string }>
    className: string
  }
> = {
  queued: {
    label: "Queued",
    icon: Clock3Icon,
    className: "border-border bg-muted text-muted-foreground",
  },
  running: {
    label: "Running",
    icon: Loader2Icon,
    className:
      "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300",
  },
  success: {
    label: "Success",
    icon: CheckCircle2Icon,
    className:
      "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
  },
  error: {
    label: "Error",
    icon: AlertCircleIcon,
    className: "border-destructive/20 bg-destructive/10 text-destructive",
  },
}

function renderPayload(payload: ToolCallPayload | undefined) {
  if (payload == null || React.isValidElement(payload)) {
    return payload
  }

  if (typeof payload === "string") {
    return payload
  }

  return JSON.stringify(payload, null, 2)
}

export function ToolCallCard({
  tool,
  status,
  title,
  description,
  duration,
  input,
  output,
  defaultOpen = false,
  onCopy,
  className,
  ...props
}: ToolCallCardProps) {
  const [open, setOpen] = React.useState(defaultOpen)
  const config = statusConfig[status]
  const Icon = config.icon
  const hasDetails = input != null || output != null
  const renderedInput = renderPayload(input)
  const renderedOutput = renderPayload(output)

  return (
    <Collapsible open={open} onOpenChange={setOpen}>
      <div
        className={cn(
          "border-border bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm",
          className
        )}
        {...props}
      >
        <div className="flex items-start gap-3 p-3">
          <div className="bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-md">
            <WrenchIcon className="size-4" />
          </div>
          <div className="min-w-0 flex-1">
            <div className="flex min-w-0 items-center gap-2">
              <p className="truncate text-sm font-medium">{title ?? tool}</p>
              <Badge
                variant="outline"
                className={cn("h-6 gap-1 rounded-md px-2", config.className)}
              >
                <Icon
                  className={cn(
                    "size-3",
                    status === "running" && "animate-spin"
                  )}
                />
                {config.label}
              </Badge>
            </div>
            {description ? (
              <p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
                {description}
              </p>
            ) : null}
            <div className="text-muted-foreground mt-2 flex flex-wrap items-center gap-2 text-xs">
              <code className="bg-muted rounded px-1.5 py-0.5 font-mono">
                {tool}
              </code>
              {duration ? <span>{duration}</span> : null}
            </div>
          </div>
          <div className="flex shrink-0 items-center gap-1">
            {onCopy ? (
              <Button
                type="button"
                variant="ghost"
                size="icon"
                className="size-8 rounded-md"
                onClick={onCopy}
                aria-label="Copy tool call"
              >
                <CopyIcon className="size-4" />
              </Button>
            ) : null}
            {hasDetails ? (
              <CollapsibleTrigger asChild>
                <Button
                  type="button"
                  variant="ghost"
                  size="icon"
                  className="size-8 rounded-md"
                  aria-label="Toggle tool call details"
                >
                  <ChevronDownIcon
                    className={cn(
                      "size-4 transition-transform",
                      open && "rotate-180"
                    )}
                  />
                </Button>
              </CollapsibleTrigger>
            ) : null}
          </div>
        </div>
        {hasDetails ? (
          <CollapsibleContent>
            <div className="border-border grid gap-3 border-t p-3 sm:grid-cols-2">
              {renderedInput ? (
                <div className="min-w-0 space-y-1.5">
                  <p className="text-muted-foreground text-xs font-medium">
                    Input
                  </p>
                  <pre className="bg-muted max-h-44 overflow-auto rounded-md p-3 text-xs leading-5">
                    {renderedInput}
                  </pre>
                </div>
              ) : null}
              {renderedOutput ? (
                <div className="min-w-0 space-y-1.5">
                  <p className="text-muted-foreground text-xs font-medium">
                    Output
                  </p>
                  <pre className="bg-muted max-h-44 overflow-auto rounded-md p-3 text-xs leading-5">
                    {renderedOutput}
                  </pre>
                </div>
              ) : null}
            </div>
          </CollapsibleContent>
        ) : null}
      </div>
    </Collapsible>
  )
}

Usage

import { ToolCallCard } from "@/components/ui/tool-call-card"
 
export function Example() {
  return (
    <ToolCallCard
      tool="crm.lookup_customer"
      status="success"
      title="Look up customer"
      duration="312 ms"
      input={{ email: "maya@zgi.ai" }}
      output={{ plan: "Pro", openTickets: 1 }}
      defaultOpen
    />
  )
}

Props

PropTypeDefault
toolstringrequired
status"queued" | "running" | "success" | "error"required
titleReactNodetool
descriptionReactNode-
durationReactNode-
inputReactNode | object | array-
outputReactNode | object | array-
defaultOpenbooleanfalse
onCopy() => void-