{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-run",
  "title": "Tool Run",
  "description": "A single tool call's lifecycle — queued, running, paused on approval, and resolved — with arguments and results one click away.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "approval-request",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/trovecn/ai-workbench/tool-run.tsx",
      "content": "\"use client\";\n\nimport { useId, useState, type ReactNode } from \"react\";\nimport {\n  AlertTriangleIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  Loader2Icon,\n  RotateCcwIcon,\n  XIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport {\n  ApprovalRequest,\n  type ApprovalRequestProps,\n} from \"@/components/trovecn/ai-workbench/approval-request\";\nimport { Button } from \"@/components/ui/button\";\nimport { spring } from \"@/lib/springs\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToolRunStatus =\n  | \"queued\"\n  | \"running\"\n  | \"needs-approval\"\n  | \"success\"\n  | \"error\"\n  | \"cancelled\";\n\nexport interface ToolRunDetail {\n  /** Compact key:value call arguments, rendered in mono. */\n  args?: readonly ReactNode[];\n  /** The call's result, once it has one — body copy, not a key:value list. */\n  result?: ReactNode;\n}\n\nexport interface ToolRunProps {\n  /** Stable identifier for the call. Not rendered — for the caller's own keys/lookups. */\n  id: string;\n  /** The tool being called, e.g. \"search_docs\" or \"Read file\". */\n  tool: ReactNode;\n  /** A short call summary next to the tool name — an argument preview, not the full arguments. */\n  summary?: ReactNode;\n  /** Caller-owned source of truth. This component renders it; it never sets it itself. */\n  status: ToolRunStatus;\n  detail?: ToolRunDetail;\n  /** Trailing metadata — elapsed time, a timestamp. */\n  meta?: ReactNode;\n  errorMessage?: ReactNode;\n  /** A rejected retry re-renders the same anchored error — never a toast. */\n  onRetry?: () => void | Promise<void>;\n  /**\n   * Required while status is \"needs-approval\" — the props ToolRun forwards\n   * to an embedded ApprovalRequest, whose card body replaces ToolRun's own\n   * for the duration of the pause. `variant` and `className` are owned by\n   * ToolRun and can't be overridden here.\n   */\n  approval?: Omit<ApprovalRequestProps, \"variant\" | \"className\">;\n  className?: string;\n}\n\ntype StatusTone = \"muted\" | \"success\" | \"destructive\";\n\nconst STATUS_COPY: Record<\n  Exclude<ToolRunStatus, \"needs-approval\">,\n  { label: string; tone: StatusTone }\n> = {\n  queued: { label: \"Queued\", tone: \"muted\" },\n  running: { label: \"Running\", tone: \"muted\" },\n  success: { label: \"Done\", tone: \"success\" },\n  error: { label: \"Failed\", tone: \"destructive\" },\n  cancelled: { label: \"Cancelled\", tone: \"muted\" },\n};\n\nfunction ToolRunStatusIcon({\n  status,\n  reduceMotion,\n}: {\n  status: ToolRunStatus;\n  reduceMotion: boolean;\n}) {\n  switch (status) {\n    case \"queued\":\n      return <span className=\"size-1.5 rounded-full bg-muted-foreground\" />;\n    case \"running\":\n      return (\n        <Loader2Icon\n          className={cn(\"size-3.5 text-muted-foreground\", !reduceMotion && \"animate-spin\")}\n        />\n      );\n    case \"success\":\n      return <CheckIcon className=\"size-3.5 text-success\" />;\n    case \"error\":\n      return <AlertTriangleIcon className=\"size-3.5 text-destructive\" />;\n    case \"cancelled\":\n      return <XIcon className=\"size-3.5 text-muted-foreground\" />;\n    case \"needs-approval\":\n      return null;\n  }\n}\n\nfunction ToolRunDisclosure({ detail, showResult }: { detail: ToolRunDetail; showResult: boolean }) {\n  const [isOpen, setIsOpen] = useState(false);\n  const contentId = useId();\n  const reduceMotion = useReducedMotion();\n\n  const hasArgs = Boolean(detail.args && detail.args.length > 0);\n  const hasResult = showResult && Boolean(detail.result);\n  if (!hasArgs && !hasResult) return null;\n\n  const label = hasArgs && hasResult ? \"Arguments & result\" : hasResult ? \"Result\" : \"Arguments\";\n\n  return (\n    <div className=\"mt-1.5\">\n      <button\n        type=\"button\"\n        aria-expanded={isOpen}\n        aria-controls={contentId}\n        onClick={() => setIsOpen((open) => !open)}\n        className=\"group relative inline-flex items-center gap-1 text-caption text-muted-foreground transition-colors duration-quick before:absolute before:-inset-3 before:content-[''] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <span>{label}</span>\n        <motion.span\n          animate={{ transform: `rotate(${isOpen ? 180 : 0}deg)` }}\n          transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n        >\n          <ChevronDownIcon className=\"size-3\" />\n        </motion.span>\n      </button>\n      <AnimatePresence initial={false}>\n        {isOpen ? (\n          <motion.div\n            id={contentId}\n            initial={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            animate={reduceMotion ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n            exit={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            transition={reduceMotion ? spring.quick.exit : spring.moderate.enter}\n            className=\"overflow-hidden\"\n          >\n            <div className=\"mt-1.5 flex flex-col gap-1.5\">\n              {hasArgs ? (\n                <div className=\"flex flex-col gap-1 rounded-md bg-muted px-2.5 py-2 font-mono text-meta text-muted-foreground\">\n                  {detail.args?.map((item, index) => (\n                    <span key={index}>{item}</span>\n                  ))}\n                </div>\n              ) : null}\n              {hasResult ? (\n                <div className=\"shadow-well rounded-md bg-background px-2.5 py-2 text-body leading-relaxed text-foreground\">\n                  {detail.result}\n                </div>\n              ) : null}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n/**\n * One tool call's lifecycle: queued, running, paused on approval, and its\n * resolution, with call arguments and results always one click away behind\n * a disclosure. A needs-approval status hands the whole card body off to\n * ApprovalRequest (via its embedded variant) instead of growing a second,\n * competing consent affordance — Retry stays anchored to the call that\n * failed rather than surfacing as a toast elsewhere.\n */\nfunction ToolRun({\n  tool,\n  summary,\n  status,\n  detail,\n  meta,\n  errorMessage,\n  onRetry,\n  approval,\n  className,\n}: ToolRunProps) {\n  const reduceMotion = useReducedMotion();\n  const statusId = useId();\n  const [isRetrying, setIsRetrying] = useState(false);\n  const [retryError, setRetryError] = useState<string | null>(null);\n\n  async function handleRetry() {\n    setRetryError(null);\n    setIsRetrying(true);\n    try {\n      await onRetry?.();\n    } catch {\n      setRetryError(\"Retry failed. Try again.\");\n    } finally {\n      setIsRetrying(false);\n    }\n  }\n\n  const isNeedsApproval = status === \"needs-approval\";\n  const statusCopy = isNeedsApproval ? null : STATUS_COPY[status];\n\n  return (\n    <motion.div\n      data-slot=\"tool-run\"\n      initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 6 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={reduceMotion ? spring.quick.exit : spring.moderate.enter}\n      className={cn(\"shadow-bevel w-full max-w-md rounded-lg bg-card px-4 py-3.5\", className)}\n    >\n      <span id={statusId} role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {statusCopy ? `${tool}: ${statusCopy.label}` : `${tool}: needs approval`}\n      </span>\n\n      <AnimatePresence mode=\"wait\" initial={false}>\n        {isNeedsApproval && approval ? (\n          <motion.div\n            key=\"approval\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0, transition: reduceMotion ? { duration: 0 } : spring.quick.exit }}\n            transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n          >\n            <ApprovalRequest {...approval} variant=\"embedded\" />\n          </motion.div>\n        ) : (\n          <motion.div\n            key=\"call\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0, transition: reduceMotion ? { duration: 0 } : spring.quick.exit }}\n            transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n          >\n            <div className=\"flex items-center gap-2\">\n              <span className=\"flex size-4 shrink-0 items-center justify-center\" aria-hidden=\"true\">\n                <ToolRunStatusIcon status={status} reduceMotion={Boolean(reduceMotion)} />\n              </span>\n              <span\n                className={cn(\n                  \"min-w-0 flex-1 truncate text-control text-foreground\",\n                  status === \"running\" && !reduceMotion && \"agent-activity-shimmer\",\n                )}\n              >\n                {tool}\n                {summary ? (\n                  <span className=\"font-normal text-muted-foreground\"> · {summary}</span>\n                ) : null}\n              </span>\n              {meta ? (\n                <span className=\"shrink-0 font-mono text-micro text-muted-foreground\">{meta}</span>\n              ) : null}\n            </div>\n\n            {detail ? (\n              <ToolRunDisclosure detail={detail} showResult={status === \"success\"} />\n            ) : null}\n\n            {status === \"error\" ? (\n              <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n                <span className=\"text-caption text-destructive\">\n                  {retryError ?? errorMessage ?? \"This call failed.\"}\n                </span>\n                {onRetry ? (\n                  <Button\n                    type=\"button\"\n                    variant=\"secondary\"\n                    size=\"sm\"\n                    disabled={isRetrying}\n                    onClick={() => void handleRetry()}\n                  >\n                    {isRetrying ? (\n                      <Loader2Icon\n                        className={cn(\"size-3.5\", !reduceMotion && \"animate-spin\")}\n                        aria-hidden=\"true\"\n                      />\n                    ) : (\n                      <RotateCcwIcon className=\"size-3.5\" />\n                    )}\n                    {isRetrying ? \"Retrying…\" : \"Retry\"}\n                  </Button>\n                ) : null}\n              </div>\n            ) : null}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\nexport { ToolRun };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}