{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-group",
  "type": "registry:ui",
  "dependencies": [
    "@base-ui/react",
    "@tabler/icons-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://agent-elements.21st.dev/r/generic-tool.json",
    "https://agent-elements.21st.dev/r/text-shimmer.json"
  ],
  "files": [
    {
      "path": "registry/agent-elements/components/tools/tool-group.tsx",
      "content": "import { memo, useEffect, useMemo, useRef, useState } from \"react\";\nimport { toolRegistry } from \"./tool-registry\";\nimport { GenericTool } from \"./generic-tool\";\nimport { getToolStatus } from \"../utils/format-tool\";\nimport { cn } from \"../utils/cn\";\nimport { ToolRowBase } from \"./tool-row-base\";\n\nexport type ToolGroupProps = {\n  part: any;\n  nestedTools?: any[];\n  chatStatus?: string;\n  completeLabel: string;\n  shimmerLabel?: string;\n  interruptedLabel: string;\n  maxVisibleTools?: number;\n  defaultOpen?: boolean;\n  showElapsed?: boolean;\n};\n\nfunction formatElapsedTime(ms: number): string {\n  if (ms < 1000) return \"\";\n  const seconds = Math.floor(ms / 1000);\n  if (seconds < 60) return `${seconds}s`;\n  const minutes = Math.floor(seconds / 60);\n  const remainingSeconds = seconds % 60;\n  if (remainingSeconds === 0) return `${minutes}m`;\n  return `${minutes}m ${remainingSeconds}s`;\n}\n\nfunction formatCount(value: number, label: string): string {\n  return `${value} ${value === 1 ? label : `${label}s`}`;\n}\n\nfunction summarizeNestedTools(nestedTools: any[]): string {\n  if (nestedTools.length === 0) return \"\";\n  const fileTypes = new Set([\"tool-Read\", \"tool-Edit\", \"tool-Write\"]);\n  const searchTypes = new Set([\n    \"tool-Search\",\n    \"tool-Grep\",\n    \"tool-Glob\",\n    \"tool-WebSearch\",\n  ]);\n  const commandTypes = new Set([\"tool-Bash\"]);\n\n  let fileCount = 0;\n  let searchCount = 0;\n  let commandCount = 0;\n\n  for (const tool of nestedTools) {\n    if (fileTypes.has(tool.type)) fileCount += 1;\n    else if (searchTypes.has(tool.type)) searchCount += 1;\n    else if (commandTypes.has(tool.type)) commandCount += 1;\n  }\n\n  const parts: string[] = [];\n  if (fileCount > 0) parts.push(formatCount(fileCount, \"file\"));\n  if (searchCount > 0)\n    parts.push(`${searchCount} ${searchCount === 1 ? \"search\" : \"searches\"}`);\n  if (commandCount > 0) parts.push(formatCount(commandCount, \"command\"));\n\n  if (parts.length === 0) return \"\";\n  if (parts.length === 1) return parts[0];\n  if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;\n  return `${parts.slice(0, -1).join(\", \")}, and ${parts[parts.length - 1]}`;\n}\n\nfunction getNestedCounts(nestedTools: any[]) {\n  const fileTypes = new Set([\"tool-Read\", \"tool-Edit\", \"tool-Write\"]);\n  const searchTypes = new Set([\n    \"tool-Search\",\n    \"tool-Grep\",\n    \"tool-Glob\",\n    \"tool-WebSearch\",\n  ]);\n  let fileCount = 0;\n  let searchCount = 0;\n\n  for (const tool of nestedTools) {\n    if (fileTypes.has(tool.type)) fileCount += 1;\n    else if (searchTypes.has(tool.type)) searchCount += 1;\n  }\n\n  return { fileCount, searchCount };\n}\n\nfunction formatStreamCounts(fileCount: number, searchCount: number): string {\n  const parts: string[] = [];\n  if (fileCount > 0) parts.push(formatCount(fileCount, \"file\"));\n  if (searchCount > 0)\n    parts.push(`${searchCount} ${searchCount === 1 ? \"search\" : \"searches\"}`);\n  return parts.join(\", \");\n}\n\nexport const ToolGroup = memo(function ToolGroup({\n  part,\n  nestedTools = [],\n  chatStatus,\n  completeLabel,\n  shimmerLabel,\n  interruptedLabel,\n  maxVisibleTools = 5,\n  defaultOpen,\n  showElapsed = true,\n}: ToolGroupProps) {\n  const { isPending, isInterrupted } = getToolStatus(part, chatStatus);\n  const description = part.input?.description || \"\";\n  const [elapsedMs, setElapsedMs] = useState(0);\n  const [expanded, setExpanded] = useState(defaultOpen ?? false);\n  const [visibleCount, setVisibleCount] = useState(0);\n  const startedAt =\n    (part.callProviderMetadata?.custom?.startedAt as number | undefined) ??\n    (part.startedAt as number | undefined);\n  const hasNestedTools = nestedTools.length > 0;\n  const streamKey = part.toolCallId ?? part.id ?? \"\";\n  const outputDuration =\n    part.output?.totalDurationMs ||\n    part.output?.duration ||\n    part.output?.duration_ms;\n  const maskThreshold = 4;\n  const streamHeight = Math.max(1, maxVisibleTools) * 28;\n  const visibleToolCount = isPending\n    ? Math.max(visibleCount, 0)\n    : nestedTools.length;\n  const wasPendingRef = useRef(isPending);\n  const userToggledRef = useRef(false);\n  const openTimerRef = useRef<number | null>(null);\n  const { fileCount, searchCount } = useMemo(() => {\n    const visibleTools = isPending\n      ? nestedTools.slice(0, Math.max(visibleCount, 0))\n      : nestedTools;\n    return getNestedCounts(visibleTools);\n  }, [isPending, nestedTools, visibleCount]);\n  const streamCounts = formatStreamCounts(fileCount, searchCount);\n  const listRef = useRef<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    if (isPending && startedAt) {\n      setElapsedMs(Date.now() - startedAt);\n      const interval = setInterval(() => {\n        setElapsedMs(Date.now() - startedAt);\n      }, 1000);\n      return () => clearInterval(interval);\n    }\n  }, [isPending, startedAt]);\n\n  useEffect(() => {\n    const wasPending = wasPendingRef.current;\n    if (openTimerRef.current) {\n      window.clearTimeout(openTimerRef.current);\n      openTimerRef.current = null;\n    }\n    if (isPending && !wasPending) {\n      if (!userToggledRef.current && defaultOpen !== false) {\n        setExpanded(false);\n        openTimerRef.current = window.setTimeout(() => {\n          setExpanded(true);\n        }, 60);\n      }\n    }\n    if (!isPending && wasPending) {\n      setExpanded(false);\n      userToggledRef.current = false;\n    }\n    wasPendingRef.current = isPending;\n    return () => {\n      if (openTimerRef.current) {\n        window.clearTimeout(openTimerRef.current);\n        openTimerRef.current = null;\n      }\n    };\n  }, [defaultOpen, isPending]);\n\n  useEffect(() => {\n    if (!isPending || nestedTools.length === 0) {\n      setVisibleCount(nestedTools.length);\n      return;\n    }\n    let index = 1;\n    setVisibleCount(Math.min(index, nestedTools.length));\n    const interval = setInterval(() => {\n      index += 1;\n      setVisibleCount(Math.min(index, nestedTools.length));\n      if (index >= nestedTools.length) clearInterval(interval);\n    }, 450);\n    return () => clearInterval(interval);\n  }, [isPending, nestedTools.length, streamKey]);\n\n  useEffect(() => {\n    if (!isPending || !listRef.current) return;\n    listRef.current.scrollTop = listRef.current.scrollHeight;\n  }, [isPending, visibleCount]);\n\n  const subtitle = (() => {\n    if (isPending && hasNestedTools) {\n      return streamCounts;\n    }\n\n    if (!isPending && hasNestedTools) {\n      const summary = summarizeNestedTools(nestedTools);\n      if (summary) return summary;\n    }\n\n    if (!description) return \"\";\n    return description.length > 60\n      ? `${description.slice(0, 57)}...`\n      : description;\n  })();\n  const elapsedTimeDisplay = formatElapsedTime(\n    !isPending && outputDuration ? outputDuration : elapsedMs,\n  );\n\n  if (isInterrupted && !part.output) {\n    return <ToolRowBase completeLabel={interruptedLabel} isAnimating={false} />;\n  }\n\n  return (\n    <ToolRowBase\n      completeLabel={completeLabel}\n      shimmerLabel={shimmerLabel}\n      isAnimating={isPending}\n      detail={subtitle}\n      expandable={hasNestedTools}\n      expanded={expanded}\n      onToggleExpand={() => {\n        userToggledRef.current = true;\n        setExpanded((prev) => !prev);\n      }}\n      trailingContent={\n        showElapsed && elapsedTimeDisplay ? (\n          <span className=\"font-normal tabular-nums shrink-0 text-an-foreground-muted/60\">\n            {elapsedTimeDisplay}\n          </span>\n        ) : undefined\n      }\n    >\n      <div className=\"relative\">\n        {isPending && expanded && visibleToolCount > maskThreshold && (\n          <div className=\"absolute inset-x-0 top-0 h-10 z-10 pointer-events-none bg-linear-to-b from-an-background to-transparent\" />\n        )}\n        <div\n          ref={listRef}\n          className={cn(\n            nestedTools.length > 1 ? \"space-y-2\" : \"space-y-0\",\n            isPending &&\n              expanded &&\n              visibleToolCount > maskThreshold &&\n              \"overflow-y-auto\",\n          )}\n          style={\n            isPending && expanded && visibleToolCount > maskThreshold\n              ? { height: `${streamHeight}px` }\n              : undefined\n          }\n        >\n          {(isPending\n            ? nestedTools.slice(0, Math.max(visibleCount, 0))\n            : nestedTools\n          ).map((nestedPart, idx) => {\n            const derivedPart = isPending\n              ? {\n                  ...nestedPart,\n                  state:\n                    idx === visibleCount - 1\n                      ? \"input-streaming\"\n                      : \"output-available\",\n                }\n              : nestedPart;\n            const nestedMeta = toolRegistry[derivedPart.type];\n            if (!nestedMeta) return null;\n            const { isPending: nestedIsPending, isError: nestedIsError } =\n              getToolStatus(derivedPart, chatStatus);\n            return (\n              <GenericTool\n                key={idx}\n                icon={nestedMeta.icon}\n                title={nestedMeta.title(derivedPart)}\n                subtitle={nestedMeta.subtitle?.(derivedPart)}\n                isPending={nestedIsPending}\n                isError={nestedIsError}\n              />\n            );\n          })}\n        </div>\n      </div>\n    </ToolRowBase>\n  );\n});\n",
      "type": "registry:ui",
      "target": "components/agent-elements/tools/tool-group.tsx"
    },
    {
      "path": "registry/agent-elements/components/tools/tool-registry.ts",
      "content": "import type React from \"react\";\nimport {\n  IconSearch as Search,\n  IconEye as Eye,\n  IconFolderSearch as FolderSearch,\n  IconGitBranch as GitBranch,\n  IconTerminal2 as Terminal,\n  IconCircleX as XCircle,\n  IconFileCode as FileCode2,\n  IconSparkles as Sparkles,\n  IconGlobe as Globe,\n  IconFilePlus as FilePlus,\n  IconChecklist as ListTodo,\n  IconLogout as LogOut,\n} from \"@tabler/icons-react\";\n\nexport type ToolVariant = \"simple\" | \"collapsible\";\n\nexport type ToolMeta = {\n  icon: React.ComponentType<{ className?: string }>;\n  title: (part: any) => string;\n  subtitle?: (part: any) => string;\n  variant: ToolVariant;\n};\n\nfunction getDisplayPath(filePath: string): string {\n  if (!filePath) return \"\";\n  const prefixes = [\n    \"/project/sandbox/repo/\",\n    \"/project/sandbox/\",\n    \"/project/\",\n    \"/workspace/\",\n  ];\n  for (const prefix of prefixes) {\n    if (filePath.startsWith(prefix)) return filePath.slice(prefix.length);\n  }\n  const worktreeMatch = filePath.match(\n    /\\.21st\\/worktrees\\/[^/]+\\/[^/]+\\/(.+)$/,\n  );\n  if (worktreeMatch) return worktreeMatch[1]!;\n  if (filePath.startsWith(\"/\")) {\n    const parts = filePath.split(\"/\");\n    const rootIndicators = [\"apps\", \"packages\", \"src\", \"lib\", \"components\"];\n    const rootIndex = parts.findIndex((p) => rootIndicators.includes(p));\n    if (rootIndex > 0) return parts.slice(rootIndex).join(\"/\");\n  }\n  return filePath;\n}\n\nfunction calculateDiffStats(oldString: string, newString: string) {\n  const oldLines = oldString.split(\"\\n\");\n  const newLines = newString.split(\"\\n\");\n  const maxLines = Math.max(oldLines.length, newLines.length);\n  let addedLines = 0;\n  let removedLines = 0;\n  for (let i = 0; i < maxLines; i++) {\n    if (oldLines[i] !== undefined && newLines[i] !== undefined) {\n      if (oldLines[i] !== newLines[i]) {\n        removedLines++;\n        addedLines++;\n      }\n    } else if (oldLines[i] !== undefined) {\n      removedLines++;\n    } else if (newLines[i] !== undefined) {\n      addedLines++;\n    }\n  }\n  return { addedLines, removedLines };\n}\n\nexport const toolRegistry: Record<string, ToolMeta> = {\n  \"tool-Task\": {\n    icon: Sparkles,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      const subagentType = part.input?.subagent_type || \"Agent\";\n      return isPending\n        ? `Running ${subagentType}`\n        : `${subagentType} completed`;\n    },\n    subtitle: (part) => {\n      const desc = part.input?.description || \"\";\n      return desc.length > 50 ? desc.slice(0, 47) + \"...\" : desc;\n    },\n    variant: \"simple\",\n  },\n  // Agent tool — renamed from \"Task\" in claude-agent-sdk 0.2.63+\n  \"tool-Agent\": {\n    icon: Sparkles,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      const subagentType = part.input?.subagent_type || \"Agent\";\n      return isPending\n        ? `Running ${subagentType}`\n        : `${subagentType} completed`;\n    },\n    subtitle: (part) => {\n      const desc = part.input?.description || \"\";\n      return desc.length > 50 ? desc.slice(0, 47) + \"...\" : desc;\n    },\n    variant: \"simple\",\n  },\n  \"tool-Skill\": {\n    icon: Sparkles,\n    title: () => \"Skill\",\n    subtitle: (part) => part.input?.skill || \"\",\n    variant: \"simple\",\n  },\n  \"tool-Grep\": {\n    icon: Search,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      if (isPending) return \"Grepping\";\n      const numFiles = part.output?.numFiles || 0;\n      return numFiles > 0 ? `Grepped ${numFiles} files` : \"No matches\";\n    },\n    subtitle: (part) => {\n      const pattern = part.input?.pattern || \"\";\n      const path = part.input?.path || \"\";\n      if (path) {\n        const combined = `${pattern} in ${getDisplayPath(path)}`;\n        return combined.length > 40 ? combined.slice(0, 37) + \"...\" : combined;\n      }\n      return pattern.length > 40 ? pattern.slice(0, 37) + \"...\" : pattern;\n    },\n    variant: \"simple\",\n  },\n  \"tool-Glob\": {\n    icon: FolderSearch,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      if (isPending) return \"Exploring files\";\n      const numFiles = part.output?.numFiles || 0;\n      return numFiles > 0 ? `Found ${numFiles} files` : \"No files found\";\n    },\n    subtitle: (part) => {\n      const pattern = part.input?.pattern || \"\";\n      return pattern.length > 40 ? pattern.slice(0, 37) + \"...\" : pattern;\n    },\n    variant: \"simple\",\n  },\n  \"tool-Read\": {\n    icon: Eye,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Reading\" : \"Read\";\n    },\n    subtitle: (part) => {\n      const filePath = part.input?.file_path || \"\";\n      if (!filePath) return \"\";\n      return filePath.split(\"/\").pop() || \"\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-Edit\": {\n    icon: FileCode2,\n    title: (part) => {\n      const filePath = part.input?.file_path || \"\";\n      if (!filePath) return \"Edit\";\n      return filePath.split(\"/\").pop() || \"Edit\";\n    },\n    subtitle: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      if (isPending) return \"\";\n      const oldString = part.input?.old_string || \"\";\n      const newString = part.input?.new_string || \"\";\n      if (!oldString && !newString) return \"\";\n      if (oldString !== newString) {\n        const { addedLines, removedLines } = calculateDiffStats(\n          oldString,\n          newString,\n        );\n        return `+${addedLines} -${removedLines}`;\n      }\n      return \"\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-Write\": {\n    icon: FilePlus,\n    title: () => \"Create\",\n    subtitle: (part) => {\n      const filePath = part.input?.file_path || \"\";\n      if (!filePath) return \"\";\n      return filePath.split(\"/\").pop() || \"\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-Bash\": {\n    icon: Terminal,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Running command\" : \"Ran command\";\n    },\n    subtitle: (part) => {\n      const command = part.input?.command || \"\";\n      if (!command) return \"\";\n      let normalized = command.replace(/\\\\\\s*\\n\\s*/g, \" \").trim();\n      normalized = normalized.replace(\n        /\\/(?:Users|home|root)\\/[^\\s\"']+/g,\n        (match: string) => getDisplayPath(match),\n      );\n      return normalized.length > 50\n        ? normalized.slice(0, 47) + \"...\"\n        : normalized;\n    },\n    variant: \"simple\",\n  },\n  \"tool-WebFetch\": {\n    icon: Globe,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Fetching\" : \"Fetched\";\n    },\n    subtitle: (part) => {\n      const url = part.input?.url || \"\";\n      try {\n        return new URL(url).hostname.replace(\"www.\", \"\");\n      } catch {\n        return url.slice(0, 30);\n      }\n    },\n    variant: \"simple\",\n  },\n  \"tool-WebSearch\": {\n    icon: Search,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Searching web\" : \"Searched web\";\n    },\n    subtitle: (part) => {\n      const query = part.input?.query || \"\";\n      return query.length > 40 ? query.slice(0, 37) + \"...\" : query;\n    },\n    variant: \"collapsible\",\n  },\n  \"tool-TodoWrite\": {\n    icon: ListTodo,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      const action = part.input?.action || \"update\";\n      if (isPending) return action === \"add\" ? \"Adding todo\" : \"Updating todos\";\n      return action === \"add\" ? \"Added todo\" : \"Updated todos\";\n    },\n    subtitle: (part) => {\n      const todos = part.input?.todos || [];\n      if (todos.length === 0) return \"\";\n      return `${todos.length} ${todos.length === 1 ? \"item\" : \"items\"}`;\n    },\n    variant: \"simple\",\n  },\n  \"tool-PlanWrite\": {\n    icon: Sparkles,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      const action = part.input?.action || \"create\";\n      if (isPending) {\n        if (action === \"create\") return \"Creating plan\";\n        if (action === \"approve\") return \"Approving plan\";\n        return \"Updating plan\";\n      }\n      const status = part.input?.plan?.status;\n      if (status === \"awaiting_approval\") return \"Plan ready for review\";\n      if (status === \"approved\") return \"Plan approved\";\n      if (status === \"completed\") return \"Plan completed\";\n      return action === \"create\" ? \"Created plan\" : \"Updated plan\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-ExitPlanMode\": {\n    icon: LogOut,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Finishing plan\" : \"Plan complete\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-NotebookEdit\": {\n    icon: FileCode2,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Editing notebook\" : \"Edited notebook\";\n    },\n    subtitle: (part) => {\n      const filePath = part.input?.file_path || \"\";\n      if (!filePath) return \"\";\n      return filePath.split(\"/\").pop() || \"\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-BashOutput\": {\n    icon: Terminal,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Getting output\" : \"Command output\";\n    },\n    subtitle: (part) => {\n      const output = part.output;\n      if (typeof output === \"string\" && output.trim()) return output.trim();\n      const command = part.input?.command || \"\";\n      if (!command) return \"\";\n      let normalized = command.replace(/\\\\\\s*\\n\\s*/g, \" \").trim();\n      normalized = normalized.replace(\n        /\\/(?:Users|home|root)\\/[^\\s\"']+/g,\n        (match: string) => getDisplayPath(match),\n      );\n      return normalized.length > 50\n        ? normalized.slice(0, 47) + \"...\"\n        : normalized;\n    },\n    variant: \"simple\",\n  },\n  \"tool-KillShell\": {\n    icon: XCircle,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Stopping shell\" : \"Shell stopped\";\n    },\n    subtitle: (part) => {\n      const pid = part.input?.pid;\n      return typeof pid === \"number\" ? `pid ${pid}` : \"\";\n    },\n    variant: \"simple\",\n  },\n  \"tool-cloning\": {\n    icon: GitBranch,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Cloning repo\" : \"Repo cloned\";\n    },\n    subtitle: (part) => part.input?.repo ?? \"\",\n    variant: \"simple\",\n  },\n  \"tool-Thinking\": {\n    icon: Sparkles,\n    title: (part) => {\n      const isPending =\n        part.state !== \"output-available\" && part.state !== \"output-error\";\n      return isPending ? \"Thinking...\" : \"Thought\";\n    },\n    variant: \"collapsible\",\n  },\n};\n\n// MCP tool parsing\nconst MCP_TOOL_PREFIX = \"tool-mcp__\";\n\nexport type McpToolInfo = {\n  serverName: string;\n  toolName: string;\n  displayName: string;\n  category: string;\n};\n\nconst BUILTIN_MCP_TOOLS: Record<string, McpToolInfo> = {\n  \"tool-ListMcpResources\": {\n    serverName: \"mcp\",\n    toolName: \"list_resources\",\n    displayName: \"List Resources\",\n    category: \"list\",\n  },\n  \"tool-ListMcpResourcesTool\": {\n    serverName: \"mcp\",\n    toolName: \"list_resources\",\n    displayName: \"List Resources\",\n    category: \"list\",\n  },\n  \"tool-ReadMcpResource\": {\n    serverName: \"mcp\",\n    toolName: \"read_resource\",\n    displayName: \"Read Resource\",\n    category: \"get\",\n  },\n  \"tool-ReadMcpResourceTool\": {\n    serverName: \"mcp\",\n    toolName: \"read_resource\",\n    displayName: \"Read Resource\",\n    category: \"get\",\n  },\n};\n\nexport function parseMcpToolType(partType: string): McpToolInfo | null {\n  const builtin = BUILTIN_MCP_TOOLS[partType];\n  if (builtin) return builtin;\n  if (!partType.startsWith(MCP_TOOL_PREFIX)) return null;\n  const withoutPrefix = partType.slice(MCP_TOOL_PREFIX.length);\n  const separatorIndex = withoutPrefix.indexOf(\"__\");\n  if (separatorIndex === -1) return null;\n  const serverName = withoutPrefix.slice(0, separatorIndex);\n  const toolName = withoutPrefix.slice(separatorIndex + 2);\n  return {\n    serverName,\n    toolName,\n    displayName: toolName\n      .replace(/_/g, \" \")\n      .replace(/\\b\\w/g, (c) => c.toUpperCase())\n      .trim(),\n    category: \"other\",\n  };\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/tools/tool-registry.ts"
    },
    {
      "path": "registry/agent-elements/utils/format-tool.ts",
      "content": "/**\n * Tool state cache for detecting AI SDK in-place mutations.\n * AI SDK mutates objects in-place during streaming, so we must\n * cache state externally and compare cached values.\n */\n\ntype CachedToolState = {\n  state: string | undefined;\n  inputJson: string;\n  outputJson: string;\n};\n\nconst toolStateCache = new Map<string, CachedToolState>();\n\nfunction getToolStateSnapshot(part: any): CachedToolState {\n  return {\n    state: part.state,\n    inputJson: JSON.stringify(part.input || {}),\n    outputJson: JSON.stringify(part.output || {}),\n  };\n}\n\nfunction hasToolStateChanged(toolCallId: string, part: any): boolean {\n  const cached = toolStateCache.get(toolCallId);\n  const current = getToolStateSnapshot(part);\n\n  if (!cached) {\n    toolStateCache.set(toolCallId, current);\n    return true;\n  }\n\n  const changed =\n    cached.state !== current.state ||\n    cached.inputJson !== current.inputJson ||\n    cached.outputJson !== current.outputJson;\n\n  if (changed) {\n    toolStateCache.set(toolCallId, current);\n  }\n\n  return changed;\n}\n\nfunction arePartsEqual(prev: any, next: any): boolean {\n  if (prev.toolCallId !== next.toolCallId) return false;\n  if (prev.type !== next.type) return false;\n\n  const toolCallId = next.toolCallId;\n  if (!toolCallId) {\n    return prev.state === next.state;\n  }\n\n  const changed = hasToolStateChanged(toolCallId, next);\n  return !changed;\n}\n\nfunction isToolCompleted(part: any): boolean {\n  if (part.output !== undefined && part.output !== null) return true;\n  if (part.state === \"error\") return true;\n  if (part.state === \"result\") return true;\n  return false;\n}\n\n/** Deep compare function for tool part props. Used with React.memo(). */\nexport function areToolPropsEqual(\n  prevProps: { part: any; chatStatus?: string },\n  nextProps: { part: any; chatStatus?: string },\n): boolean {\n  const partsEqual = arePartsEqual(prevProps.part, nextProps.part);\n  if (!partsEqual) return false;\n  if (isToolCompleted(nextProps.part)) return true;\n  if (prevProps.chatStatus !== nextProps.chatStatus) return false;\n  return true;\n}\n\n/** Get tool status from part state */\nexport function getToolStatus(part: any, chatStatus?: string) {\n  const basePending =\n    part.state !== \"output-available\" && part.state !== \"output-error\";\n  const isError =\n    part.state === \"output-error\" ||\n    (part.state === \"output-available\" && part.output?.success === false);\n  const isSuccess = part.state === \"output-available\" && !isError;\n  const isPending = basePending && chatStatus === \"streaming\";\n  const isInterrupted =\n    basePending && chatStatus !== \"streaming\" && chatStatus !== undefined;\n\n  return { isPending, isError, isSuccess, isInterrupted };\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/format-tool.ts"
    },
    {
      "path": "registry/agent-elements/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/cn.ts"
    },
    {
      "path": "registry/agent-elements/components/tools/tool-row-base.tsx",
      "content": "import type { ReactNode } from \"react\";\nimport { Collapsible } from \"@base-ui/react/collapsible\";\nimport { TextShimmer } from \"../text-shimmer\";\nimport { IconChevronRight } from \"@tabler/icons-react\";\nimport { cn } from \"../utils/cn\";\n\nexport type ToolRowBaseProps = {\n  icon?: ReactNode;\n  shimmerLabel?: string;\n  completeLabel: string;\n  isAnimating: boolean;\n  detail?: string;\n  trailingContent?: ReactNode;\n  expandable?: boolean;\n  expanded?: boolean;\n  defaultOpen?: boolean;\n  onToggleExpand?: () => void;\n  children?: ReactNode;\n};\n\nexport function ToolRowBase({\n  icon,\n  shimmerLabel,\n  completeLabel,\n  isAnimating,\n  detail,\n  trailingContent,\n  expandable = false,\n  expanded,\n  defaultOpen = false,\n  onToggleExpand,\n  children,\n}: ToolRowBaseProps) {\n  const isComplete = !isAnimating;\n  const isExpanded = expanded ?? false;\n  const canToggle = expandable && (isComplete || isExpanded || isAnimating);\n\n  const row = (\n    <div\n      className={cn(\n        \"flex items-center max-w-full select-none gap-1 rounded-an-tool-border-radius\",\n        canToggle ? \"cursor-pointer\" : \"cursor-default\",\n      )}\n    >\n      <div className=\"flex items-center gap-2 min-w-0 text-sm text-muted-foreground\">\n        {icon && (\n          <span className=\"flex items-center justify-center size-3 shrink-0\">\n            {icon}\n          </span>\n        )}\n        <span className=\"font-[450] whitespace-nowrap shrink-0\">\n          {isAnimating && shimmerLabel ? (\n            <TextShimmer\n              as=\"span\"\n              duration={1.2}\n              className=\"inline-flex items-center leading-none h-4 m-0\"\n            >\n              {shimmerLabel}\n            </TextShimmer>\n          ) : (\n            completeLabel\n          )}\n        </span>\n        {detail && (\n          <span className=\"font-normal truncate min-w-0 flex-1 text-an-foreground-muted/60\">\n            {detail}\n          </span>\n        )}\n        {trailingContent}\n      </div>\n      {expandable && (isComplete || isExpanded || isAnimating) && (\n        <div>\n          <IconChevronRight\n            className={cn(\n              \"shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n              \"size-3\",\n              \"rotate-0 group-data-panel-open:rotate-90\",\n            )}\n          />\n        </div>\n      )}\n    </div>\n  );\n\n  if (!expandable) {\n    return <div className=\"flex flex-col gap-1\">{row}</div>;\n  }\n\n  const rootProps =\n    expanded === undefined\n      ? { defaultOpen }\n      : { open: expanded, onOpenChange: onToggleExpand };\n\n  return (\n    <Collapsible.Root className=\"flex flex-col gap-2 w-full\" {...rootProps}>\n      <Collapsible.Trigger\n        className=\"group flex\"\n        disabled={!canToggle}\n        aria-disabled={!canToggle}\n      >\n        {row}\n      </Collapsible.Trigger>\n      <Collapsible.Panel\n        className={cn(\n          \"overflow-hidden\",\n          \"h-[var(--collapsible-panel-height)] transition-all duration-150 ease-out\",\n          \"data-ending-style:h-0 data-starting-style:h-0\",\n          \"[&[hidden]:not([hidden='until-found'])]:hidden\",\n        )}\n      >\n        {children}\n      </Collapsible.Panel>\n    </Collapsible.Root>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/tools/tool-row-base.tsx"
    },
    {
      "path": "registry/agent-elements/types/timeline.ts",
      "content": "export type StepState = \"pending\" | \"animating\" | \"complete\";\n\nexport type TimelineStep =\n  | {\n      id: string;\n      type: \"input-typing\";\n      content: string;\n      image?: string;\n      duration: number;\n    }\n  | {\n      id: string;\n      type: \"user-message\";\n      content: string;\n      image?: string;\n    }\n  | {\n      id: string;\n      type: \"tool-call\";\n      toolName: string;\n      toolDetail: string;\n      duration: number;\n      toolVariant?: \"thinking\" | \"action\" | \"search\";\n      thoughtContent?: string;\n      searchQuery?: string;\n      searchSource?: string;\n      filePath?: string;\n      diffStats?: string;\n      diffLines?: { type: \"add\" | \"remove\" | \"context\"; content: string }[];\n      bashCommand?: string;\n      bashOutput?: string;\n      bashSuccess?: boolean;\n    }\n  | {\n      id: string;\n      type: \"assistant-stream\";\n      content: string;\n    }\n  | {\n      id: string;\n      type: \"pause\";\n      duration: number;\n    };\n\nexport type Turn = { userStep?: TimelineStep; steps: TimelineStep[] };\n",
      "type": "registry:lib",
      "target": "components/agent-elements/types/timeline.ts"
    },
    {
      "path": "registry/agent-elements/hooks/use-tool-complete.ts",
      "content": "import { useEffect, useRef } from \"react\";\n\nexport function useToolComplete(\n  isAnimating: boolean,\n  duration: number,\n  onComplete: () => void,\n) {\n  const onCompleteRef = useRef(onComplete);\n  onCompleteRef.current = onComplete;\n\n  useEffect(() => {\n    if (!isAnimating) return;\n    const t = setTimeout(() => onCompleteRef.current(), duration);\n    return () => clearTimeout(t);\n  }, [isAnimating, duration]);\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/hooks/use-tool-complete.ts"
    },
    {
      "path": "registry/agent-elements/agent-ui.css",
      "content": "/* Agent Elements tokens + utilities */\n\n/*\n * AN Agent Chat - CSS Custom Properties\n * These are the default values. Override by redefining these vars in your app CSS.\n */\n:root {\n  /* Geometry — all radii derive from --an-border-radius */\n  --an-border-radius: 16px;\n  --an-message-border-radius: var(--an-border-radius);\n  --an-input-border-radius: var(--an-border-radius);\n  --an-tool-border-radius: 10px;\n  --an-message-radius-inner-offset: 4px;\n  --an-max-width: 420px;\n\n  /* Colors - Light mode defaults */\n  --an-background: #ffffff;\n  --an-background-secondary: #f0f0f0;\n  --an-background-tertiary: #f8f8f8;\n  --an-foreground: #1a1a1a;\n  --an-foreground-muted: #737373;\n  --an-foreground-subtle: #a3a3a3;\n  --an-border-color: #e4e4e7;\n  --an-primary-color: #3b82f6;\n\n  /* User Messages */\n  --an-user-message-bg: #f5f5f5;\n  --an-user-message-text: #1a1a1a;\n\n  /* Input */\n  --an-input-background: #ffffff;\n  --an-input-border-color: #e4e4e7;\n  --an-input-color: #1a1a1a;\n  --an-input-placeholder-color: #a3a3a3;\n  --an-input-focus-outline: transparent;\n  --an-context-padding: 10px;\n\n  /* Send/Stop Buttons */\n  --an-send-button-bg: #3b82f6;\n  --an-send-button-color: #ffffff;\n\n  /* Tools */\n  --an-tool-background: #f5f5f5;\n  --an-tool-border-color: #e4e4e7;\n  --an-tool-color: #1a1a1a;\n  --an-tool-color-muted: #737373;\n\n  /* Code */\n  --an-code-background: #1e1e1e;\n  --an-code-color: #d4d4d4;\n\n  /* Diff colors */\n  --an-diff-added-bg: rgba(34, 197, 94, 0.1);\n  --an-diff-added-border: rgba(34, 197, 94, 0.5);\n  --an-diff-added-text: #15803d;\n  --an-diff-removed-bg: rgba(239, 68, 68, 0.1);\n  --an-diff-removed-border: rgba(239, 68, 68, 0.5);\n  --an-diff-removed-text: #dc2626;\n}\n\n/* Dark mode defaults */\n.dark {\n  --an-background: #0a0a0a;\n  --an-background-secondary: #242424;\n  --an-background-tertiary: #141414;\n  --an-foreground: #fafafa;\n  --an-foreground-muted: #8c8c8c;\n  --an-foreground-subtle: #71717a;\n  --an-border-color: #2a2a2a;\n  --an-primary-color: #60a5fa;\n\n  --an-user-message-bg: #1a1a1a;\n  --an-user-message-text: #fafafa;\n\n  --an-input-background: #0a0a0a;\n  --an-input-border-color: #2a2a2a;\n  --an-input-color: #fafafa;\n  --an-input-placeholder-color: #71717a;\n  --an-input-focus-outline: transparent;\n  --an-context-padding: 12px;\n\n  --an-send-button-bg: #60a5fa;\n  /* Dark mode uses a lighter primary (#60a5fa) — pair it with black text so\n     labels like \"Approve\" / send-arrow have proper contrast instead of\n     low-contrast white-on-light-blue. */\n  --an-send-button-color: #0a0a0a;\n\n  --an-tool-background: #141414;\n  --an-tool-border-color: #2a2a2a;\n  --an-tool-color: #fafafa;\n  --an-tool-color-muted: #8c8c8c;\n\n  --an-code-background: #0a0a0a;\n  --an-code-color: #d4d4d4;\n\n  --an-diff-added-bg: rgba(34, 197, 94, 0.15);\n  --an-diff-added-border: rgba(34, 197, 94, 0.4);\n  --an-diff-added-text: #4ade80;\n  --an-diff-removed-bg: rgba(239, 68, 68, 0.15);\n  --an-diff-removed-border: rgba(239, 68, 68, 0.4);\n  --an-diff-removed-text: #f87171;\n}\n\n@theme inline {\n  --color-an-background: var(--an-background);\n  --color-an-background-secondary: var(--an-background-secondary);\n  --color-an-background-tertiary: var(--an-background-tertiary);\n  --color-an-foreground: var(--an-foreground);\n  --color-an-foreground-muted: var(--an-foreground-muted);\n  --color-an-foreground-subtle: var(--an-foreground-subtle);\n  --color-an-border-color: var(--an-border-color);\n  --color-an-primary-color: var(--an-primary-color);\n  --color-an-user-message-bg: var(--an-user-message-bg);\n  --color-an-user-message-text: var(--an-user-message-text);\n  --color-an-input-background: var(--an-input-background);\n  --color-an-input-border-color: var(--an-input-border-color);\n  --color-an-input-color: var(--an-input-color);\n  --color-an-input-placeholder-color: var(--an-input-placeholder-color);\n  --color-an-input-focus-outline: var(--an-input-focus-outline);\n  --color-an-send-button-bg: var(--an-send-button-bg);\n  --color-an-send-button-color: var(--an-send-button-color);\n  --color-an-tool-background: var(--an-tool-background);\n  --color-an-tool-border-color: var(--an-tool-border-color);\n  --color-an-tool-color: var(--an-tool-color);\n  --color-an-tool-color-muted: var(--an-tool-color-muted);\n  --color-an-code-background: var(--an-code-background);\n  --color-an-code-color: var(--an-code-color);\n  --color-an-diff-added-bg: var(--an-diff-added-bg);\n  --color-an-diff-added-border: var(--an-diff-added-border);\n  --color-an-diff-added-text: var(--an-diff-added-text);\n  --color-an-diff-removed-bg: var(--an-diff-removed-bg);\n  --color-an-diff-removed-border: var(--an-diff-removed-border);\n  --color-an-diff-removed-text: var(--an-diff-removed-text);\n  --radius-an-message: var(--an-message-border-radius);\n  --radius-an-message-inner: calc(\n    var(--an-message-border-radius) - var(--an-message-radius-inner-offset)\n  );\n  --radius-an-input-border-radius: var(--an-input-border-radius);\n  --radius-an-tool-border-radius: var(--an-tool-border-radius);\n  --spacing-an-context-padding: var(--an-context-padding);\n  --max-width-an: var(--an-max-width);\n  --spacing-an-user-message-x: 14px;\n  --spacing-an-user-message-y: 10px;\n}\n\n/* TextShimmer animation */\n@keyframes an-shimmer {\n  from {\n    background-position: 100% center;\n  }\n  to {\n    background-position: 0% center;\n  }\n}\n\n@keyframes an-blink {\n  50% {\n    opacity: 0;\n  }\n}\n\n@keyframes loading-dots {\n  0%,\n  100% {\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes an-ellipsis {\n  0% {\n    width: 0;\n  }\n  33% {\n    width: 0.33em;\n  }\n  66% {\n    width: 0.66em;\n  }\n  100% {\n    width: 1em;\n  }\n}\n\n.an-text-shimmer {\n  display: inline-block;\n  background-size: 250% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  color: transparent;\n  background-image: linear-gradient(\n    90deg,\n    var(--an-foreground-subtle, #a3a3a3) 0%,\n    var(--an-foreground-subtle, #a3a3a3) 40%,\n    var(--an-foreground-muted, #737373) 50%,\n    var(--an-foreground-subtle, #a3a3a3) 60%,\n    var(--an-foreground-subtle, #a3a3a3) 100%\n  );\n  background-repeat: no-repeat;\n}\n\n.an-text-shimmer--active {\n  animation: an-shimmer var(--an-shimmer-duration, 2s) linear infinite;\n}\n\n.an-ellipsis {\n  display: inline-block;\n  overflow: hidden;\n  width: 0;\n  vertical-align: bottom;\n  animation: an-ellipsis 1.2s steps(1, end) infinite;\n}\n\n.an-markdown pre code {\n  counter-reset: none !important;\n}\n\n.an-markdown pre code > span::before {\n  content: none !important;\n  display: none !important;\n}\n\n.an-markdown pre code > span {\n  padding-left: 0 !important;\n}\n\n/* Diff */\n.an-edit-diff,\n.an-edit-diff pre,\n.an-edit-diff code {\n  font-size: 12px;\n}\n\n.dark .an-edit-tool-card {\n  background-color: #000;\n}\n\n[data-theme=\"dark\"] .an-edit-tool-card {\n  background-color: #000;\n}\n\n.dark .an-edit-tool-card .an-edit-diff,\n.dark .an-edit-tool-card .an-edit-diff pre,\n.dark .an-edit-tool-card .an-edit-diff code {\n  background-color: #000 !important;\n}\n\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff,\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff pre,\n[data-theme=\"dark\"] .an-edit-tool-card .an-edit-diff code {\n  background-color: #000 !important;\n}\n\n/* Markdown Code Block */\n.an-markdown [data-streamdown=\"code-block\"] {\n  padding: 0;\n  border: 1px solid var(--an-border-color);\n  border-radius: var(--an-tool-border-radius);\n  background: transparent;\n  gap: 0;\n}\n\n.an-markdown [data-streamdown=\"code-block-header\"] {\n  padding: 0px 10px 0px 8px;\n  height: auto;\n  font-size: 12px;\n  height: 28px;\n  background: transparent;\n  border-bottom: 1px solid var(--color-an-tool-border-color);\n  background-color: var(--an-tool-background);\n  position: relative;\n}\n\n.an-markdown div:has(> [data-streamdown=\"code-block-actions\"]) {\n  position: absolute;\n  height: auto;\n  margin: 0;\n  top: 0;\n  right: 0;\n  height: 28px;\n  padding-right: 8px;\n}\n\n.an-markdown [data-streamdown=\"code-block-actions\"] {\n  background: transparent;\n  border: none;\n  backdrop-filter: none;\n  padding: 0;\n}\n\n.an-markdown [data-streamdown=\"code-block-actions\"] button > svg {\n  width: 14px;\n  height: 14px;\n}\n\n.an-markdown [data-streamdown=\"code-block-body\"] {\n  margin: 0;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  background: transparent;\n  border: transparent;\n  overflow-x: auto;\n}\n\n.an-markdown [data-streamdown=\"code-block\"] pre,\n.an-markdown [data-streamdown=\"code-block\"] code {\n  font-size: 12px;\n  background: transparent;\n}\n",
      "type": "registry:style",
      "target": "components/agent-elements/agent-ui.css"
    }
  ],
  "css": {
    "@import \"../components/agent-elements/agent-ui.css\"": ""
  }
}