{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tool-activity","type":"registry:ui","title":"Tool Activity","description":"The tools an AI assistant used to get its answer, step by step: their status, how long they took, and their input and output.","dependencies":["lucide-react","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json"],"files":[{"path":"registry/dashral/tool-activity.tsx","type":"registry:file","target":"components/dashral/tool-activity.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\"\nimport { ChevronDown, CircleCheck, CircleDashed, CircleX, LoaderCircle } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\n\n/** \"pending\" waits its turn, \"running\" is at work, \"done\" finished, \"error\" failed. */\ntype ToolStatus = \"pending\" | \"running\" | \"done\" | \"error\"\n\ntype ToolStep = {\n  /** What the tool did, in words: \"Search orders\", \"Read invoice #1042\". */\n  name: React.ReactNode\n  status: ToolStatus\n  /** A line under the name: what it found, or why it failed. */\n  description?: React.ReactNode\n  /** What it was called with, and what it returned: an object (shown as JSON), text, or your own rendering. */\n  input?: unknown\n  output?: unknown\n  /** How long it took, in milliseconds. */\n  duration?: number\n  /** Replaces the status icon, e.g. the tool's own. */\n  icon?: React.ReactNode\n  /** Opens its input and output from the start. */\n  defaultOpen?: boolean\n  /** A stable key when names repeat. */\n  id?: string\n}\n\ntype ToolActivityProps = Omit<React.ComponentProps<\"ol\">, \"children\"> & {\n  /** The tool calls, in the order they ran. */\n  steps: ToolStep[]\n  /** Names the list for screen readers. \"Tool activity\" by default. */\n  \"aria-label\"?: string\n  classNames?: { step?: string; trigger?: string; details?: string; code?: string }\n}\n\nconst icons: Record<ToolStatus, { icon: React.ElementType; className: string }> = {\n  pending: { icon: CircleDashed, className: \"text-muted-foreground\" },\n  running: { icon: LoaderCircle, className: \"text-primary-text motion-safe:animate-spin\" },\n  done: { icon: CircleCheck, className: \"text-success-text\" },\n  error: { icon: CircleX, className: \"text-destructive-text\" },\n}\n\n/** An object as indented JSON, text as it is, an element rendered. */\nfunction show(value: unknown): React.ReactNode {\n  if (value === undefined || value === null) return null\n  if (React.isValidElement(value)) return value\n  if (typeof value === \"string\") return value\n  try {\n    return JSON.stringify(value, null, 2)\n  } catch {\n    return String(value)\n  }\n}\n\n/**\n * The tools an assistant used to get its answer, one after another: each with its status (waiting, running, done or\n * failed), what it found, how long it took, and its input and output to open.\n */\nfunction ToolActivity({ steps, className, classNames, \"aria-label\": ariaLabel, ...props }: ToolActivityProps) {\n  const { t } = useLocale()\n  return (\n    <ol data-slot=\"tool-activity\" aria-label={ariaLabel ?? t(\"toolActivity\")} className={cn(\"grid text-sm\", className)} {...props}>\n      {steps.map((step, index) => (\n        <ToolStepRow key={step.id ?? index} step={step} last={index === steps.length - 1} classNames={classNames} />\n      ))}\n    </ol>\n  )\n}\n\nfunction ToolStepRow({ step, last, classNames }: { step: ToolStep; last: boolean; classNames: ToolActivityProps[\"classNames\"] }) {\n  const { t, locale } = useLocale()\n  const [open, setOpen] = React.useState(step.defaultOpen ?? false)\n  const statusWord = { pending: t(\"toolPending\"), running: t(\"toolRunning\"), done: t(\"toolDone\"), error: t(\"toolFailed\") }[step.status]\n  const input = show(step.input)\n  const output = show(step.output)\n  const hasDetails = input !== null || output !== null\n  const { icon: Icon, className: iconClass } = icons[step.status]\n  const duration =\n    step.duration === undefined\n      ? null\n      : step.duration < 1000\n        ? new Intl.NumberFormat(locale, { style: \"unit\", unit: \"millisecond\", unitDisplay: \"narrow\" }).format(Math.round(step.duration))\n        : new Intl.NumberFormat(locale, { style: \"unit\", unit: \"second\", unitDisplay: \"narrow\", maximumFractionDigits: 1 }).format(step.duration / 1000)\n\n  const row = (\n    <>\n      <span className=\"min-w-0 flex-1\">\n        <span className={cn(\"block truncate font-medium\", step.status === \"pending\" && \"text-muted-foreground\")} dir=\"auto\">\n          {step.name}\n        </span>\n        {step.description && (\n          <span className={cn(\"block text-xs\", step.status === \"error\" ? \"text-destructive-text\" : \"text-muted-foreground\")} dir=\"auto\">\n            {step.description}\n          </span>\n        )}\n      </span>\n      {duration && step.status !== \"running\" && <span className=\"shrink-0 text-xs text-muted-foreground tabular-nums\">{duration}</span>}\n      {hasDetails && (\n        <ChevronDown\n          aria-hidden\n          className=\"size-3.5 shrink-0 text-muted-foreground transition-transform duration-(--duration-standard) ease-standard group-data-[state=open]/tool:rotate-180 motion-reduce:transition-none\"\n        />\n      )}\n    </>\n  )\n\n  return (\n    <li data-slot=\"tool-step\" data-status={step.status} className={cn(\"relative flex gap-3\", classNames?.step)}>\n      {/* The status, and a line down to the next step. */}\n      <span aria-hidden className=\"relative flex w-5 shrink-0 flex-col items-center\">\n        <span className={cn(\"mt-2 grid size-5 place-items-center\", iconClass)}>{step.icon ?? <Icon className=\"size-4\" />}</span>\n        {!last && <span className=\"mt-1 w-px flex-1 bg-border\" />}\n      </span>\n      {/* Heard as each step changes (\"Search orders: done\"), not the whole list again. */}\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {typeof step.name === \"string\" ? t(\"toolStatus\", { name: step.name, status: statusWord }) : statusWord}\n      </span>\n      <CollapsiblePrimitive.Root open={open} onOpenChange={setOpen} className={cn(\"min-w-0 flex-1\", !last && \"pb-2\")}>\n        {hasDetails ? (\n          <CollapsiblePrimitive.Trigger\n            className={cn(\n              \"group/tool flex w-full items-center gap-3 rounded-md py-1.5 text-start outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\",\n              classNames?.trigger\n            )}\n          >\n            {row}\n          </CollapsiblePrimitive.Trigger>\n        ) : (\n          <div className={cn(\"flex items-center gap-3 py-1.5\", classNames?.trigger)}>{row}</div>\n        )}\n        {hasDetails && (\n          <CollapsiblePrimitive.Content className=\"overflow-hidden duration-(--duration-standard) ease-standard data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down motion-reduce:animate-none\">\n            <div className={cn(\"mt-1 mb-2 grid gap-2\", classNames?.details)}>\n              {input !== null && <ToolValue label={t(\"toolInput\")} value={input} className={classNames?.code} />}\n              {output !== null && <ToolValue label={t(\"toolOutput\")} value={output} className={classNames?.code} />}\n            </div>\n          </CollapsiblePrimitive.Content>\n        )}\n      </CollapsiblePrimitive.Root>\n    </li>\n  )\n}\n\nfunction ToolValue({ label, value, className }: { label: string; value: React.ReactNode; className?: string }) {\n  const text = typeof value === \"string\"\n  return (\n    <div className=\"grid gap-1\">\n      <span className=\"text-[11px] font-medium tracking-wide text-muted-foreground uppercase\">{label}</span>\n      {text ? (\n        <pre className={cn(\"max-h-48 overflow-auto rounded-lg border bg-muted/40 px-3 py-2 font-mono text-xs leading-relaxed whitespace-pre-wrap [overflow-wrap:anywhere]\", className)}>\n          {value}\n        </pre>\n      ) : (\n        <div className={cn(\"rounded-lg border bg-muted/40 px-3 py-2 text-xs\", className)}>{value}</div>\n      )}\n    </div>\n  )\n}\n\nexport { ToolActivity, type ToolActivityProps, type ToolStep, type ToolStatus }\n"}],"cssVars":{"theme":{"color-primary-text":"var(--primary-text, var(--primary))","color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)"}}}