{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-chat",
  "type": "registry:ui",
  "dependencies": [
    "@base-ui/react",
    "@tabler/icons-react",
    "ai",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://agent-elements.21st.dev/r/attachment-button.json",
    "https://agent-elements.21st.dev/r/bash-tool.json",
    "https://agent-elements.21st.dev/r/edit-tool.json",
    "https://agent-elements.21st.dev/r/error-message.json",
    "https://agent-elements.21st.dev/r/file-attachment.json",
    "https://agent-elements.21st.dev/r/generic-tool.json",
    "https://agent-elements.21st.dev/r/input-bar.json",
    "https://agent-elements.21st.dev/r/markdown.json",
    "https://agent-elements.21st.dev/r/mcp-tool.json",
    "https://agent-elements.21st.dev/r/message-list.json",
    "https://agent-elements.21st.dev/r/plan-tool.json",
    "https://agent-elements.21st.dev/r/question-tool.json",
    "https://agent-elements.21st.dev/r/search-tool.json",
    "https://agent-elements.21st.dev/r/send-button.json",
    "https://agent-elements.21st.dev/r/spiral-loader.json",
    "https://agent-elements.21st.dev/r/suggestions.json",
    "https://agent-elements.21st.dev/r/text-shimmer.json",
    "https://agent-elements.21st.dev/r/thinking-tool.json",
    "https://agent-elements.21st.dev/r/todo-tool.json",
    "https://agent-elements.21st.dev/r/tool-group.json",
    "https://agent-elements.21st.dev/r/user-message.json"
  ],
  "files": [
    {
      "path": "registry/agent-elements/components/agent-chat.tsx",
      "content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { MessageList } from \"./message-list\";\nimport { InputBar } from \"./input-bar\";\nimport { Suggestions, type SuggestionItem } from \"./input/suggestions\";\nimport { cn } from \"./utils/cn\";\nimport type { AgentChatProps } from \"./types\";\n\nexport function AgentChat({\n  messages,\n  onSend,\n  status,\n  onStop,\n  error,\n  classNames,\n  slots,\n  toolRenderers,\n  attachments,\n  showCopyToolbar,\n  initialScrollBehavior,\n  enableImagePreview,\n  suggestions,\n  emptyStatePosition = \"default\",\n  emptySuggestionsPlacement = \"input\",\n  emptySuggestionsPosition = \"top\",\n  questionTool,\n  className,\n  style,\n}: AgentChatProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [draft, setDraft] = useState(\"\");\n\n  const ResolvedInputBar = slots?.InputBar ?? InputBar;\n  const isEmpty = !error && messages.length === 0;\n  const isCenteredEmptyState = isEmpty && emptyStatePosition === \"center\";\n\n  const pendingQuestion = findPendingQuestion(messages, questionTool);\n  const suggestionConfig = resolveSuggestions(suggestions);\n  const showInputSuggestions =\n    emptySuggestionsPlacement === \"input\" ||\n    emptySuggestionsPlacement === \"both\";\n  const showEmptySuggestions =\n    isCenteredEmptyState &&\n    (emptySuggestionsPlacement === \"empty\" ||\n      emptySuggestionsPlacement === \"both\") &&\n    suggestionConfig.items.length > 0;\n\n  const handleEmptySuggestionSelect = (item: SuggestionItem) => {\n    setDraft(item.value ?? item.label);\n  };\n\n  const emptySuggestionsNode = showEmptySuggestions ? (\n    <Suggestions\n      items={suggestionConfig.items}\n      onSelect={handleEmptySuggestionSelect}\n      disabled={status === \"streaming\" || status === \"submitted\"}\n      className={cn(\n        \"w-full justify-center\",\n        emptySuggestionsPosition === \"top\" ? \"mb-3\" : \"mt-3\",\n        suggestionConfig.className,\n      )}\n      itemClassName={cn(\"h-8 rounded-md px-3\", suggestionConfig.itemClassName)}\n    />\n  ) : null;\n\n  const inputBarNode = (\n    <ResolvedInputBar\n      onSend={onSend}\n      status={status}\n      onStop={onStop}\n      value={draft}\n      onChange={setDraft}\n      placeholder=\"Send a message...\"\n      className={cn(classNames?.inputBar, isCenteredEmptyState && \"px-0 pb-0\")}\n      onAttach={attachments?.onAttach}\n      attachedImages={attachments?.images}\n      attachedFiles={attachments?.files}\n      onRemoveImage={attachments?.onRemoveImage}\n      onRemoveFile={attachments?.onRemoveFile}\n      onPaste={attachments?.onPaste}\n      isDragOver={attachments?.isDragOver}\n      suggestions={showInputSuggestions ? suggestions : []}\n      questionBar={\n        pendingQuestion\n          ? {\n              id: pendingQuestion.id,\n              questions: pendingQuestion.questions,\n              questionIndex: pendingQuestion.questionIndex,\n              totalQuestions: pendingQuestion.totalQuestions,\n              onPreviousQuestion: pendingQuestion.onPreviousQuestion,\n              onNextQuestion: pendingQuestion.onNextQuestion,\n              submitLabel: pendingQuestion.submitLabel,\n              skipLabel: pendingQuestion.skipLabel,\n              allowSkip: pendingQuestion.allowSkip,\n              onSubmit: (answer) => {\n                questionTool?.onAnswer?.({\n                  toolCallId: pendingQuestion.toolCallId,\n                  question:\n                    pendingQuestion.questions[\n                      pendingQuestion.questionIndex\n                        ? pendingQuestion.questionIndex - 1\n                        : 0\n                    ],\n                  answer,\n                });\n              },\n            }\n          : undefined\n      }\n    />\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"flex flex-col h-full min-h-0\",\n        classNames?.root,\n        className,\n      )}\n      style={style}\n    >\n      {isCenteredEmptyState ? (\n        <div className=\"flex-1 min-h-0 flex items-center justify-center px-4 py-4\">\n          <div className=\"w-full max-w-an\">\n            {emptySuggestionsPosition === \"top\" ? emptySuggestionsNode : null}\n            {inputBarNode}\n            {emptySuggestionsPosition === \"bottom\"\n              ? emptySuggestionsNode\n              : null}\n          </div>\n        </div>\n      ) : (\n        <MessageList\n          messages={\n            error\n              ? [\n                  ...messages,\n                  {\n                    id: \"agent-chat-error\",\n                    role: \"assistant\",\n                    parts: [\n                      {\n                        type: \"error\",\n                        title: \"Request failed\",\n                        message: error.message,\n                      },\n                    ],\n                  } as unknown as (typeof messages)[number],\n                ]\n              : messages\n          }\n          status={status}\n          classNames={classNames}\n          slots={slots}\n          toolRenderers={toolRenderers}\n          showCopyToolbar={showCopyToolbar}\n          initialScrollBehavior={initialScrollBehavior}\n          enableImagePreview={enableImagePreview}\n          suppressQuestionTool={Boolean(pendingQuestion)}\n        />\n      )}\n      {!isCenteredEmptyState ? inputBarNode : null}\n    </div>\n  );\n}\n\nfunction resolveSuggestions(suggestions: AgentChatProps[\"suggestions\"]) {\n  if (Array.isArray(suggestions)) {\n    return {\n      items: suggestions,\n      className: undefined,\n      itemClassName: undefined,\n    };\n  }\n  return {\n    items: suggestions?.items ?? [],\n    className: suggestions?.className,\n    itemClassName: suggestions?.itemClassName,\n  };\n}\n\nfunction findPendingQuestion(\n  messages: AgentChatProps[\"messages\"],\n  questionTool: AgentChatProps[\"questionTool\"],\n) {\n  for (let i = messages.length - 1; i >= 0; i -= 1) {\n    const message = messages[i];\n    if (message?.role !== \"assistant\") continue;\n    const parts = message.parts ?? [];\n    for (let p = parts.length - 1; p >= 0; p -= 1) {\n      const part = parts[p] as {\n        type?: string;\n        toolCallId?: string;\n        input?: {\n          questions?: import(\"./question/question-prompt\").QuestionConfig[];\n          question?: import(\"./question/question-prompt\").QuestionConfig;\n          questionIndex?: number;\n          totalQuestions?: number;\n          onPreviousQuestion?: () => void;\n          onNextQuestion?: () => void;\n          submitLabel?: string;\n          skipLabel?: string;\n          allowSkip?: boolean;\n        };\n        output?: {\n          answer?: import(\"./question/question-prompt\").QuestionAnswer;\n        };\n      };\n      if (part?.type !== \"tool-Question\") continue;\n      const input = part.input;\n      const questions = input?.questions ?? [];\n      const firstQuestion = questions[0] ?? input?.question;\n      if (!firstQuestion) continue;\n      if (part.output?.answer) return null;\n      return {\n        id: part.toolCallId ?? `question-${i}-${p}`,\n        toolCallId: part.toolCallId,\n        questions,\n        question: firstQuestion,\n        questionIndex: input?.questionIndex,\n        totalQuestions:\n          input?.totalQuestions ??\n          (questions.length > 0 ? questions.length : undefined),\n        onPreviousQuestion: input?.onPreviousQuestion,\n        onNextQuestion: input?.onNextQuestion,\n        submitLabel: questionTool?.submitLabel ?? input?.submitLabel,\n        skipLabel: questionTool?.skipLabel ?? input?.skipLabel,\n        allowSkip: questionTool?.allowSkip ?? input?.allowSkip,\n      };\n    }\n  }\n  return null;\n}\n\n// Legacy component alias kept for compatibility.\nexport const AnAgentChat = AgentChat;\n",
      "type": "registry:ui",
      "target": "components/agent-elements/agent-chat.tsx"
    },
    {
      "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/types.ts",
      "content": "import type React from \"react\";\nimport type { UIMessage, ChatStatus } from \"ai\";\nimport type {\n  QuestionAnswer,\n  QuestionConfig,\n} from \"./question/question-prompt\";\nimport type { SuggestionItem } from \"./input/suggestions\";\n\nexport type InputSuggestions =\n  | SuggestionItem[]\n  | {\n      items: SuggestionItem[];\n      className?: string;\n      itemClassName?: string;\n    };\n\n/** Theme JSON generated by the playground */\nexport type ChatTheme = {\n  theme: Record<string, string>;\n  light: Record<string, string>;\n  dark: Record<string, string>;\n};\n\n/** Per-element CSS class overrides */\nexport type ChatClassNames = {\n  root: string;\n  userMessage: string;\n  inputBar: string;\n};\n\n/** Props for createAgentChat() */\nexport type CreateAgentChatOptions = {\n  agent: string;\n  /** Provide either tokenUrl (simple) or getToken (full control) */\n  tokenUrl?: string;\n  getToken?: () => Promise<string>;\n  apiUrl?: string;\n  /**\n   * Sandbox ID — identifies the persistent sandbox environment.\n   *\n   * Each unique `sandboxId` maps to its own sandbox (isolated VM with persistent filesystem).\n   * Requests with the same `sandboxId` share the same sandbox — files, git repos,\n   * and session history persist across messages.\n   *\n   * If omitted, the relay creates a new sandbox per request.\n   *\n   * @example\n   * // Per-user sandbox\n   * createAgentChat({ agent: \"my-agent\", tokenUrl: \"/api/an/token\", sandboxId: `user-${userId}` })\n   *\n   * // Continue existing sandbox\n   * createAgentChat({ agent: \"my-agent\", tokenUrl: \"/api/an/token\", sandboxId: sandbox.id })\n   */\n  sandboxId?: string;\n  /**\n   * Thread ID — identifies a specific conversation thread within the sandbox.\n   * Requires `sandboxId` to be set. If omitted with `sandboxId`, creates a new thread.\n   */\n  threadId?: string;\n  onFinish?: () => void;\n  onError?: (error: Error) => void;\n};\n\n/** Props passed to custom tool renderer components */\nexport type CustomToolRendererProps = {\n  name: string;\n  input: Record<string, unknown>;\n  output: unknown | undefined;\n  status: \"pending\" | \"streaming\" | \"success\" | \"error\";\n};\n\n/** Component slot overrides */\nexport type ChatSlots = {\n  InputBar: React.ComponentType<{\n    onSend: (message: { role: \"user\"; content: string }) => void;\n    status: ChatStatus;\n    onStop: () => void;\n    [key: string]: unknown;\n  }>;\n  UserMessage: React.ComponentType<{\n    message: UIMessage;\n    className?: string;\n  }>;\n  ToolRenderer: React.ComponentType<{\n    part: {\n      type: string;\n      toolCallId?: string;\n      state?: string;\n      input?: unknown;\n      output?: unknown;\n      result?: unknown;\n    };\n    nestedTools?: {\n      type: string;\n      toolCallId?: string;\n      state?: string;\n      input?: unknown;\n      output?: unknown;\n      result?: unknown;\n    }[];\n    chatStatus?: string;\n    toolRenderers?: Record<\n      string,\n      React.ComponentType<CustomToolRendererProps>\n    >;\n  }>;\n};\n\n/** A model option for the model selector */\nexport type ModelOption = {\n  id: string;\n  name: string;\n  version?: string;\n};\n\n/** Props for the <AgentChat> drop-in component */\nexport type AgentChatProps = {\n  messages: UIMessage[];\n  onSend: (message: { role: \"user\"; content: string }) => void;\n  status: ChatStatus;\n  onStop: () => void;\n  error?: Error;\n\n  classNames?: Partial<ChatClassNames>;\n  slots?: Partial<ChatSlots>;\n  toolRenderers?: Record<string, React.ComponentType<CustomToolRendererProps>>;\n\n  /** Attachment configuration */\n  attachments?: {\n    onAttach?: () => void;\n    images?: { id: string; filename: string; url: string; size?: number }[];\n    files?: { id: string; filename: string; size?: number }[];\n    onRemoveImage?: (id: string) => void;\n    onRemoveFile?: (id: string) => void;\n    onPaste?: (e: React.ClipboardEvent) => void;\n    isDragOver?: boolean;\n  };\n\n  /** Show copy toolbar on text turns */\n  showCopyToolbar?: boolean;\n\n  /**\n   * Where to position the scroll container on initial mount.\n   * - \"bottom\" (default): classic chat behavior, pinned to the latest message.\n   * - \"top\": start from the top of the conversation — useful for static demos\n   *   or read-only transcripts where the user should read top-to-bottom.\n   */\n  initialScrollBehavior?: \"bottom\" | \"top\";\n\n  /**\n   * When true (default) clicking an attached image opens a fullscreen\n   * lightbox preview. Set to false to render images as plain thumbnails\n   * (no click handler, no portal). Applies to both staged input attachments\n   * and images inside user messages.\n   */\n  enableImagePreview?: boolean;\n\n  suggestions?: InputSuggestions;\n\n  emptyStatePosition?: \"default\" | \"center\";\n  emptySuggestionsPlacement?: \"input\" | \"empty\" | \"both\";\n  emptySuggestionsPosition?: \"top\" | \"bottom\";\n\n  questionTool?: {\n    submitLabel?: string;\n    skipLabel?: string;\n    allowSkip?: boolean;\n    onAnswer?: (payload: {\n      toolCallId?: string;\n      question: QuestionConfig;\n      answer: QuestionAnswer;\n    }) => void;\n  };\n\n  className?: string;\n  style?: React.CSSProperties;\n};\n\n// Legacy type aliases kept for compatibility.\nexport type AnTheme = ChatTheme;\nexport type AnClassNames = ChatClassNames;\nexport type AnSlots = ChatSlots;\nexport type CreateAnChatOptions = CreateAgentChatOptions;\nexport type AnModelOption = ModelOption;\nexport type AnAgentChatProps = AgentChatProps;\n",
      "type": "registry:lib",
      "target": "components/agent-elements/types.ts"
    },
    {
      "path": "registry/agent-elements/components/question/question-prompt.tsx",
      "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"../utils/cn\";\n\nexport type QuestionOption = {\n  id: string;\n  label: string;\n  description?: string;\n};\n\nexport type QuestionConfig = {\n  kind: \"single\" | \"multi\" | \"text\";\n  title: string;\n  description?: string;\n  options?: QuestionOption[];\n  allowCustom?: boolean;\n  customLabel?: string;\n  customPlaceholder?: string;\n  minSelections?: number;\n  maxSelections?: number;\n  placeholder?: string;\n};\n\nexport type QuestionAnswer = {\n  kind: \"single\" | \"multi\" | \"text\" | \"skip\";\n  selectedIds?: string[];\n  text?: string;\n};\n\nconst QUESTION_CUSTOM_ID = \"__custom__\";\n\nfunction optionBadge(idx: number) {\n  return String.fromCharCode(65 + idx);\n}\n\nexport type QuestionPromptProps = {\n  questions: QuestionConfig[];\n  questionIndex?: number;\n  totalQuestions?: number;\n  onPreviousQuestion?: () => void;\n  onNextQuestion?: () => void;\n  initialAnswer?: QuestionAnswer;\n  /** Label for the primary action on the LAST question (default \"Send\"). */\n  submitLabel?: string;\n  /** Label for the primary action when there are more questions ahead\n   *  (default \"Next\"). The host (e.g. QuestionTool) is expected to advance\n   *  to the next question after onSubmit fires. */\n  nextLabel?: string;\n  skipLabel?: string;\n  allowSkip?: boolean;\n  onSubmit: (answer: QuestionAnswer) => void;\n  onSkip?: () => void;\n  className?: string;\n};\n\nexport function QuestionPrompt({\n  questions,\n  questionIndex = 1,\n  totalQuestions,\n  onPreviousQuestion,\n  onNextQuestion,\n  submitLabel = \"Send\",\n  nextLabel = \"Next\",\n  skipLabel = \"Skip\",\n  allowSkip = true,\n  initialAnswer,\n  onSubmit,\n  onSkip,\n  className,\n}: QuestionPromptProps) {\n  const [selectedIds, setSelectedIds] = useState<string[]>([]);\n  const [customText, setCustomText] = useState(\"\");\n  const [textValue, setTextValue] = useState(\"\");\n  const resolvedTotal = totalQuestions ?? questions.length;\n  const clampedIndex = Math.max(1, Math.min(questionIndex, resolvedTotal));\n  const activeQuestion = questions[clampedIndex - 1];\n  const customEnabled = activeQuestion?.allowCustom ?? false;\n  const showNav =\n    resolvedTotal > 1 && (!!onPreviousQuestion || !!onNextQuestion);\n  const canGoPrev = clampedIndex > 1;\n  const canGoNext = clampedIndex < resolvedTotal;\n  const isLastQuestion = clampedIndex >= resolvedTotal;\n  const primaryLabel = isLastQuestion ? submitLabel : nextLabel;\n\n  useEffect(() => {\n    if (!initialAnswer || initialAnswer.kind === \"skip\") {\n      setSelectedIds([]);\n      setCustomText(\"\");\n      setTextValue(\"\");\n      return;\n    }\n\n    if (activeQuestion?.kind === \"text\") {\n      setSelectedIds([]);\n      setCustomText(\"\");\n      setTextValue(initialAnswer.text ?? \"\");\n      return;\n    }\n\n    const nextSelected = new Set(initialAnswer.selectedIds ?? []);\n    const nextCustomText = initialAnswer.text ?? \"\";\n    if (customEnabled && nextCustomText.trim().length > 0) {\n      nextSelected.add(QUESTION_CUSTOM_ID);\n    }\n    setSelectedIds(Array.from(nextSelected));\n    setCustomText(nextCustomText);\n    setTextValue(\"\");\n  }, [\n    activeQuestion?.kind,\n    clampedIndex,\n    customEnabled,\n    initialAnswer?.kind,\n    initialAnswer?.text,\n    initialAnswer?.selectedIds?.join(\"|\"),\n  ]);\n\n  const canSubmit = useMemo(() => {\n    if (activeQuestion?.kind === \"text\") return textValue.trim().length > 0;\n\n    const selectedNonCustom = selectedIds.filter(\n      (id) => id !== QUESTION_CUSTOM_ID,\n    ).length;\n    const hasCustomText = customText.trim().length > 0;\n    const total = selectedNonCustom + (hasCustomText ? 1 : 0);\n\n    if (activeQuestion?.kind === \"single\") {\n      return total === 1;\n    }\n\n    const min = activeQuestion?.minSelections ?? 1;\n    const max = activeQuestion?.maxSelections;\n    if (total < min) return false;\n    if (typeof max === \"number\" && total > max) return false;\n    return total > 0;\n  }, [\n    activeQuestion?.kind,\n    activeQuestion?.minSelections,\n    activeQuestion?.maxSelections,\n    selectedIds,\n    customText,\n    textValue,\n  ]);\n\n  const toggleMulti = (id: string) => {\n    setSelectedIds((prev) =>\n      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],\n    );\n  };\n\n  const handleSingleSelect = (id: string) => {\n    setSelectedIds([id]);\n  };\n\n  const handleCustomTextChange = (nextValue: string) => {\n    setCustomText(nextValue);\n    if (!activeQuestion) return;\n    if (activeQuestion.kind === \"single\") {\n      setSelectedIds(nextValue.trim().length > 0 ? [QUESTION_CUSTOM_ID] : []);\n      return;\n    }\n    setSelectedIds((prev) => {\n      const hasCustom = prev.includes(QUESTION_CUSTOM_ID);\n      if (nextValue.trim().length > 0 && !hasCustom) {\n        return [...prev, QUESTION_CUSTOM_ID];\n      }\n      if (nextValue.trim().length === 0 && hasCustom) {\n        return prev.filter((id) => id !== QUESTION_CUSTOM_ID);\n      }\n      return prev;\n    });\n  };\n\n  const handleSubmit = () => {\n    if (!canSubmit || !activeQuestion) return;\n    if (activeQuestion.kind === \"text\") {\n      onSubmit({ kind: \"text\", text: textValue.trim() });\n      return;\n    }\n\n    const selectedNonCustom = selectedIds.filter(\n      (id) => id !== QUESTION_CUSTOM_ID,\n    );\n    const answerText = customText.trim() || undefined;\n    onSubmit({\n      kind: activeQuestion.kind,\n      selectedIds: selectedNonCustom,\n      text: answerText || undefined,\n    });\n  };\n\n  const handleSkip = () => {\n    onSkip?.();\n    onSubmit({ kind: \"skip\" });\n  };\n\n  if (!activeQuestion) return null;\n\n  return (\n    <div className={cn(\"px-3 py-2 space-y-2 bg-background\", className)}>\n      <div\n        className=\"flex items-center justify-between gap-px\"\n        data-total-questions={resolvedTotal}\n      >\n        <div className=\"flex items-center gap-2 text-sm text-an-tool-color\">\n          <span className=\"h-5 min-w-5 px-1 rounded-[4px] inline-flex items-center justify-center text-sm font-medium text-an-tool-color-muted\">\n            {clampedIndex}\n          </span>\n          <span>{activeQuestion.title}</span>\n        </div>\n      </div>\n\n      {activeQuestion.kind !== \"text\" &&\n        (activeQuestion.options?.length ?? 0) > 0 && (\n          <div className=\"space-y-px\">\n            {activeQuestion.options!.map((option, idx) => {\n              const checked = selectedIds.includes(option.id);\n              return (\n                <button\n                  key={option.id}\n                  type=\"button\"\n                  onClick={() => {\n                    if (activeQuestion.kind === \"single\") {\n                      handleSingleSelect(option.id);\n                      if (customEnabled) setCustomText(\"\");\n                    } else {\n                      toggleMulti(option.id);\n                    }\n                  }}\n                  className=\"w-full text-left rounded-md px-2 py-1.5 flex items-center gap-2 hover:bg-an-background-secondary -mx-2\"\n                >\n                  <span\n                    className={cn(\n                      \"h-5 min-w-5 px-1 rounded-[4px] inline-flex items-center justify-center text-sm font-medium border\",\n                      checked\n                        ? \"bg-an-primary-color text-an-send-button-color border-an-primary-color\"\n                        : \"bg-transparent text-an-tool-color-muted border-border\",\n                    )}\n                  >\n                    {optionBadge(idx)}\n                  </span>\n                  <span className=\"text-sm text-an-tool-color\">\n                    {option.label}\n                    {option.description && (\n                      <span className=\"text-an-tool-color-muted\">\n                        {\" \"}\n                        {option.description}\n                      </span>\n                    )}\n                  </span>\n                </button>\n              );\n            })}\n\n            {customEnabled && (\n              <div className=\"pt-1 flex items-center gap-2\">\n                <span\n                  className={cn(\n                    \"h-5 min-w-5 px-1 rounded-[4px] inline-flex items-center justify-center text-sm font-medium border\",\n                    selectedIds.includes(QUESTION_CUSTOM_ID)\n                      ? \"bg-an-primary-color text-an-send-button-color border-an-primary-color\"\n                      : \"bg-transparent text-an-tool-color-muted border-border\",\n                  )}\n                >\n                  {optionBadge(activeQuestion.options!.length)}\n                </span>\n                <input\n                  value={customText}\n                  onChange={(event) =>\n                    handleCustomTextChange(event.target.value)\n                  }\n                  placeholder={\n                    activeQuestion.customPlaceholder ?? \"Type your answer\"\n                  }\n                  className=\"w-full h-7 rounded-md border border-border bg-background px-2 text-sm text-an-tool-color\"\n                />\n              </div>\n            )}\n          </div>\n        )}\n\n      {activeQuestion.kind === \"text\" && (\n        <textarea\n          value={textValue}\n          onChange={(event) => setTextValue(event.target.value)}\n          placeholder={activeQuestion.placeholder ?? \"Type your answer\"}\n          rows={3}\n          className=\"w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-an-tool-color resize-y\"\n        />\n      )}\n\n      <div\n        className={cn(\n          \"flex items-center gap-1.5\",\n          showNav ? \"justify-between\" : \"justify-end\",\n        )}\n      >\n        {showNav && (\n          <div className=\"flex items-center gap-1.5\">\n            {onPreviousQuestion && (\n              <button\n                type=\"button\"\n                onClick={onPreviousQuestion}\n                disabled={!canGoPrev}\n                className=\"h-6 px-2 rounded-[4px] text-sm text-muted-foreground hover:text-an-tool-color disabled:opacity-60\"\n              >\n                Previous\n              </button>\n            )}\n            {onNextQuestion && (\n              <button\n                type=\"button\"\n                onClick={onNextQuestion}\n                disabled={!canGoNext}\n                className=\"h-6 px-2 rounded-[4px] text-sm text-muted-foreground hover:text-an-tool-color disabled:opacity-60\"\n              >\n                Next\n              </button>\n            )}\n          </div>\n        )}\n        <div className=\"flex items-center justify-end gap-1.5\">\n          {allowSkip && (\n            <button\n              type=\"button\"\n              onClick={handleSkip}\n              className=\"h-6 px-2 rounded-[4px] text-sm text-muted-foreground hover:text-an-tool-color hover:bg-muted/50 active:scale-[0.98] transition-[background-color,color,transform] duration-150\"\n            >\n              {skipLabel}\n            </button>\n          )}\n          <button\n            type=\"button\"\n            onClick={handleSubmit}\n            disabled={!canSubmit}\n            className=\"h-6 px-2.5 rounded-[4px] text-sm font-medium bg-an-primary-color text-an-send-button-color hover:bg-an-primary-color/90 active:scale-[0.98] transition-[background-color,transform] duration-150 disabled:opacity-60 disabled:hover:bg-an-primary-color disabled:active:scale-100\"\n          >\n            {primaryLabel}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/question/question-prompt.tsx"
    },
    {
      "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/components/tools/tool-renderer.tsx",
      "content": "import React, { memo } from \"react\";\nimport { toolRegistry, parseMcpToolType } from \"./tool-registry\";\nimport { getToolStatus } from \"../utils/format-tool\";\nimport { GenericTool } from \"./generic-tool\";\nimport { BashTool } from \"./bash-tool\";\nimport { EditTool } from \"./edit-tool\";\nimport { TodoTool } from \"./todo-tool\";\nimport { PlanTool } from \"./plan-tool\";\nimport { ToolGroup } from \"./tool-group\";\nimport { McpTool, unwrapMcpOutput } from \"./mcp-tool\";\nimport { ThinkingTool } from \"./thinking-tool\";\nimport { SearchTool } from \"./search-tool\";\nimport { QuestionTool } from \"../question/question-tool\";\nimport type { CustomToolRendererProps } from \"../types\";\n\nexport type ToolRendererProps = {\n  part: any;\n  nestedTools?: any[];\n  chatStatus?: string;\n  toolRenderers?: Record<string, React.ComponentType<CustomToolRendererProps>>;\n};\n\nfunction deriveToolStatus(\n  part: any,\n  chatStatus?: string,\n): CustomToolRendererProps[\"status\"] {\n  if (part.state === \"input-streaming\") return \"streaming\";\n  if (part.state === \"output-available\") return \"success\";\n  if (part.state === \"output-error\") return \"error\";\n  const { isPending } = getToolStatus(part, chatStatus);\n  return isPending ? \"pending\" : \"success\";\n}\n\nexport const ToolRenderer = memo(function ToolRenderer({\n  part,\n  nestedTools,\n  chatStatus,\n  toolRenderers,\n}: ToolRendererProps) {\n  const partType = part.type as string;\n\n  // Specialized tool components with variant dispatch\n  switch (partType) {\n    case \"tool-Bash\":\n      return <BashTool part={part} />;\n    case \"tool-Edit\":\n    case \"tool-Write\":\n      return <EditTool part={part} />;\n    case \"tool-WebSearch\":\n    case \"tool-Grep\":\n    case \"tool-Glob\":\n      return <SearchTool part={part} />;\n    case \"tool-PlanWrite\":\n      return <PlanTool part={part} chatStatus={chatStatus} />;\n    case \"tool-TodoWrite\":\n      return <TodoTool part={part} chatStatus={chatStatus} />;\n    case \"tool-Question\":\n      return <QuestionTool part={part} chatStatus={chatStatus} />;\n    case \"tool-Task\":\n    case \"tool-Agent\":\n      const labelBase = part.type === \"tool-Agent\" ? \"Agent\" : \"Task\";\n      return (\n        <ToolGroup\n          part={part}\n          nestedTools={nestedTools}\n          chatStatus={chatStatus}\n          completeLabel={`${labelBase} completed`}\n          shimmerLabel={`Running ${labelBase.toLowerCase()}`}\n          interruptedLabel={`${labelBase} interrupted`}\n          defaultOpen={false}\n        />\n      );\n    case \"tool-Thinking\":\n      return <ThinkingTool part={part} />;\n  }\n\n  // MCP tools\n  const mcpInfo = parseMcpToolType(partType);\n  if (mcpInfo) {\n    // Custom renderer for user-defined tools\n    if (toolRenderers && mcpInfo.serverName === \"user-tools\") {\n      const CustomRenderer = toolRenderers[mcpInfo.toolName];\n      if (CustomRenderer) {\n        return (\n          <CustomRenderer\n            name={mcpInfo.toolName}\n            input={(part.input ?? {}) as Record<string, unknown>}\n            output={part.output ? unwrapMcpOutput(part.output) : undefined}\n            status={deriveToolStatus(part, chatStatus)}\n          />\n        );\n      }\n    }\n    return <McpTool part={part} mcpInfo={mcpInfo} chatStatus={chatStatus} />;\n  }\n\n  // Registry-based generic tools (Read, Grep, Glob, WebFetch, etc.)\n  const meta = toolRegistry[partType];\n  if (meta) {\n    const { isPending, isError } = getToolStatus(part, chatStatus);\n    return (\n      <GenericTool\n        title={meta.title(part)}\n        subtitle={meta.subtitle?.(part)}\n        isPending={isPending}\n        isError={isError}\n      />\n    );\n  }\n\n  // Fallback: show tool name\n  const toolName = partType.startsWith(\"tool-\") ? partType.slice(5) : partType;\n  const { isPending, isError } = getToolStatus(part, chatStatus);\n  return (\n    <GenericTool\n      title={isPending ? `Running ${toolName}` : toolName}\n      isPending={isPending}\n      isError={isError}\n    />\n  );\n});\n",
      "type": "registry:ui",
      "target": "components/agent-elements/tools/tool-renderer.tsx"
    },
    {
      "path": "registry/agent-elements/utils/tool-part-normalizer.ts",
      "content": "type AnyRecord = Record<string, any>;\n\nfunction isRecord(value: unknown): value is AnyRecord {\n  return typeof value === \"object\" && value !== null;\n}\n\nfunction parseStructuredJson(value: unknown): unknown {\n  if (typeof value !== \"string\") return value;\n  const trimmed = value.trim();\n  if (!trimmed) return value;\n\n  try {\n    const parsed = JSON.parse(trimmed);\n    return isRecord(parsed) || Array.isArray(parsed) ? parsed : value;\n  } catch {\n    return value;\n  }\n}\n\nexport function normalizeToolPart(part: unknown): unknown {\n  if (!isRecord(part)) return part;\n  if (typeof part.type !== \"string\" || !part.type.startsWith(\"tool-\"))\n    return part;\n\n  const normalizedInput = parseStructuredJson(part.input);\n  const normalizedOutput = parseStructuredJson(part.output);\n  const normalizedResult = parseStructuredJson(part.result);\n\n  const inputChanged = normalizedInput !== part.input;\n  const outputChanged = normalizedOutput !== part.output;\n  const resultChanged = normalizedResult !== part.result;\n\n  if (!inputChanged && !outputChanged && !resultChanged) {\n    return part;\n  }\n\n  const normalizedPart: AnyRecord = { ...part };\n  if (inputChanged) normalizedPart.input = normalizedInput;\n  if (outputChanged) normalizedPart.output = normalizedOutput;\n  if (resultChanged) normalizedPart.result = normalizedResult;\n  return normalizedPart;\n}\n\nexport function normalizeAssistantToolParts(parts: unknown[]): unknown[] {\n  let changed = false;\n  const normalizedParts = parts.map((part) => {\n    const normalizedPart = normalizeToolPart(part);\n    if (normalizedPart !== part) changed = true;\n    return normalizedPart;\n  });\n\n  return changed ? normalizedParts : parts;\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/tool-part-normalizer.ts"
    },
    {
      "path": "registry/agent-elements/components/input/input-typing.tsx",
      "content": "import { useState, useEffect, useRef } from \"react\";\n\nexport function useInputTyping(\n  text: string,\n  duration: number,\n  isActive: boolean,\n  onComplete: () => void,\n) {\n  const [visibleChars, setVisibleChars] = useState(0);\n  const [showImage, setShowImage] = useState(false);\n  const onCompleteRef = useRef(onComplete);\n  onCompleteRef.current = onComplete;\n\n  useEffect(() => {\n    if (!isActive) {\n      setVisibleChars(0);\n      setShowImage(false);\n      return;\n    }\n\n    const imageDelay = duration * 0.1;\n    const typingStart = duration * 0.15;\n    const typingDuration = duration * 0.7;\n    const charInterval = typingDuration / text.length;\n    const sendDelay = duration * 0.15;\n    const timers: ReturnType<typeof setTimeout>[] = [];\n\n    timers.push(setTimeout(() => setShowImage(true), imageDelay));\n    for (let i = 0; i < text.length; i++) {\n      timers.push(\n        setTimeout(\n          () => setVisibleChars(i + 1),\n          typingStart + charInterval * i,\n        ),\n      );\n    }\n    timers.push(\n      setTimeout(\n        () => onCompleteRef.current(),\n        typingStart + typingDuration + sendDelay,\n      ),\n    );\n\n    return () => timers.forEach(clearTimeout);\n  }, [isActive, text, duration]);\n\n  return { displayedText: text.slice(0, visibleChars), showImage };\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/input/input-typing.tsx"
    },
    {
      "path": "registry/agent-elements/components/image-lightbox.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  IconChevronLeft,\n  IconChevronRight,\n  IconX,\n} from \"@tabler/icons-react\";\nimport { cn } from \"./utils/cn\";\n\nexport type LightboxImage = {\n  /** Stable identifier — used for keys and to know which image is active. */\n  id: string;\n  /** Resolvable image URL (https / data: / blob:). */\n  url: string;\n  /** Optional filename used for the alt text. */\n  filename?: string;\n};\n\nexport type ImageLightboxProps = {\n  /** Whether the overlay is open. */\n  open: boolean;\n  /** Close handler — wired to overlay click, X button, and Esc key. */\n  onClose: () => void;\n  /** Full set of images for gallery navigation. */\n  images: LightboxImage[];\n  /** Index in `images` to start on. */\n  initialIndex?: number;\n};\n\n/**\n * Portal-based fullscreen image preview. Renders to `document.body` so it\n * escapes any clipping/transform/stacking context. Adapted from the\n * 21st-private-1 desktop chat — without copy/save (those are\n * desktop-API-specific).\n */\nexport function ImageLightbox({\n  open,\n  onClose,\n  images,\n  initialIndex = 0,\n}: ImageLightboxProps) {\n  const [currentIndex, setCurrentIndex] = useState(initialIndex);\n  const hasMultipleImages = images.length > 1;\n\n  // Sync the active index whenever the consumer re-opens with a new initial.\n  useEffect(() => {\n    if (open) setCurrentIndex(initialIndex);\n  }, [open, initialIndex]);\n\n  const goToPrevious = useCallback(\n    (event?: React.MouseEvent) => {\n      event?.stopPropagation();\n      setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1));\n    },\n    [images.length],\n  );\n\n  const goToNext = useCallback(\n    (event?: React.MouseEvent) => {\n      event?.stopPropagation();\n      setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0));\n    },\n    [images.length],\n  );\n\n  // Esc / arrow-key navigation. Capture phase so we beat any local handlers\n  // (e.g. an Editor that swallows Esc).\n  useEffect(() => {\n    if (!open) return;\n    const handleKeyDown = (event: KeyboardEvent) => {\n      switch (event.key) {\n        case \"Escape\":\n          event.preventDefault();\n          event.stopPropagation();\n          onClose();\n          break;\n        case \"ArrowLeft\":\n          if (hasMultipleImages) goToPrevious();\n          break;\n        case \"ArrowRight\":\n          if (hasMultipleImages) goToNext();\n          break;\n      }\n    };\n    window.addEventListener(\"keydown\", handleKeyDown, true);\n    return () =>\n      window.removeEventListener(\"keydown\", handleKeyDown, true);\n  }, [open, hasMultipleImages, onClose, goToPrevious, goToNext]);\n\n  // Lock body scroll while open so the page underneath doesn't move.\n  useEffect(() => {\n    if (!open) return;\n    const previousOverflow = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = previousOverflow;\n    };\n  }, [open]);\n\n  if (typeof document === \"undefined\") return null;\n  if (!open) return null;\n  const currentImage = images[currentIndex] ?? images[0];\n  if (!currentImage?.url) return null;\n\n  return createPortal(\n    <div\n      role=\"dialog\"\n      aria-modal=\"true\"\n      className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm\"\n      onClick={onClose}\n    >\n      <button\n        type=\"button\"\n        onClick={onClose}\n        aria-label=\"Close fullscreen (Esc)\"\n        className=\"absolute top-4 right-4 z-10 inline-flex size-9 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors\"\n      >\n        <IconX className=\"size-5\" />\n      </button>\n\n      {hasMultipleImages && (\n        <button\n          type=\"button\"\n          onClick={goToPrevious}\n          aria-label=\"Previous image (←)\"\n          className=\"absolute left-4 top-1/2 z-10 inline-flex size-10 -translate-y-1/2 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors\"\n        >\n          <IconChevronLeft className=\"size-6\" />\n        </button>\n      )}\n\n      <img\n        src={currentImage.url}\n        alt={currentImage.filename ?? \"Image preview\"}\n        className=\"max-w-[90vw] max-h-[85vh] object-contain select-none\"\n        onClick={(event) => event.stopPropagation()}\n        draggable={false}\n      />\n\n      {hasMultipleImages && (\n        <button\n          type=\"button\"\n          onClick={goToNext}\n          aria-label=\"Next image (→)\"\n          className=\"absolute right-4 top-1/2 z-10 inline-flex size-10 -translate-y-1/2 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors\"\n        >\n          <IconChevronRight className=\"size-6\" />\n        </button>\n      )}\n\n      {hasMultipleImages && (\n        <div className=\"absolute bottom-6 left-1/2 -translate-x-1/2 flex flex-col items-center gap-3\">\n          <div className=\"flex gap-2\">\n            {images.map((_, idx) => (\n              <button\n                key={idx}\n                type=\"button\"\n                onClick={(event) => {\n                  event.stopPropagation();\n                  setCurrentIndex(idx);\n                }}\n                aria-label={`Go to image ${idx + 1}`}\n                className={cn(\n                  \"size-2 rounded-full transition-all\",\n                  idx === currentIndex\n                    ? \"bg-white scale-125\"\n                    : \"bg-white/40 hover:bg-white/60\",\n                )}\n              />\n            ))}\n          </div>\n          <span className=\"text-white/70 text-sm\">\n            {currentIndex + 1} / {images.length}\n          </span>\n        </div>\n      )}\n    </div>,\n    document.body,\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/image-lightbox.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/components/spiral-loader-data.ts",
      "content": "export const spiralFastData = {\"nm\":\"spiral2\",\"h\":16,\"w\":16,\"meta\":{\"g\":\"@lottiefiles/toolkit-js 0.71.7\"},\"layers\":[{\"ty\":4,\"nm\":\"Vector 5821 copy Outlines\",\"sr\":1,\"st\":0,\"op\":30,\"ip\":0,\"ln\":\"18\",\"hasMask\":false,\"ao\":0,\"ks\":{\"a\":{\"a\":0,\"k\":[12.5,6.5,0]},\"s\":{\"a\":0,\"k\":[100,100,100]},\"p\":{\"a\":1,\"k\":[{\"o\":{\"x\":0.167,\"y\":0.167},\"i\":{\"x\":0.833,\"y\":0.833},\"s\":[12,8,0],\"t\":0},{\"s\":[4,8,0],\"t\":29}]},\"r\":{\"a\":0,\"k\":0},\"sa\":{\"a\":0,\"k\":0},\"o\":{\"a\":0,\"k\":24}},\"shapes\":[{\"ty\":\"gr\",\"nm\":\"Group 1\",\"it\":[{\"ty\":\"sh\",\"nm\":\"Path 1\",\"d\":1,\"ks\":{\"a\":0,\"k\":{\"c\":false,\"i\":[[0,0],[-0.289,3.296],[2.218,0],[-0.23,-2.627],[-4.452,0],[-0.289,3.296],[2.218,0],[-0.23,-2.627],[-4.452,0],[-0.289,3.296],[2.218,0],[-0.232,-2.627],[-4.443,0]],\"o\":[[4.452,0],[0.23,-2.627],[-2.218,0],[0.289,3.296],[4.452,0],[0.23,-2.627],[-2.218,0],[0.289,3.296],[4.452,0],[0.23,-2.627],[-2.218,0],[0.292,3.296],[0,0]],\"v\":[[-12,6],[-4.975,-1.012],[-8,-6],[-11.025,-1.012],[-4,6],[3.025,-1.012],[0,-6],[-3.025,-1.012],[4,6],[11.025,-1.012],[8,-6],[4.98,-1.012],[12,6]]}}},{\"ty\":\"st\",\"nm\":\"Stroke 1\",\"lc\":2,\"lj\":2,\"ml\":4,\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":1.4},\"c\":{\"a\":0,\"k\":[1,1,1]}},{\"ty\":\"tr\",\"a\":{\"a\":0,\"k\":[0,0]},\"s\":{\"a\":0,\"k\":[100,100]},\"p\":{\"a\":0,\"k\":[12.5,6.5]},\"r\":{\"a\":0,\"k\":0},\"sa\":{\"a\":0,\"k\":0},\"o\":{\"a\":0,\"k\":100}}]},{\"ty\":\"tm\",\"nm\":\"Trim Paths 1\",\"e\":{\"a\":1,\"k\":[{\"o\":{\"x\":0.341,\"y\":0.488},\"i\":{\"x\":0.269,\"y\":0.75},\"s\":[44],\"t\":0},{\"s\":[77],\"t\":29}]},\"o\":{\"a\":0,\"k\":0},\"s\":{\"a\":1,\"k\":[{\"o\":{\"x\":0.32,\"y\":0.154},\"i\":{\"x\":0.826,\"y\":0.579},\"s\":[23],\"t\":0},{\"s\":[57],\"t\":29}]},\"m\":1}],\"ind\":1}],\"v\":\"5.7.0\",\"fr\":60,\"op\":30,\"ip\":0,\"assets\":[]};\n\nexport const spiralSlowData = {\"nm\":\"spiral1\",\"h\":16,\"w\":16,\"meta\":{\"g\":\"@lottiefiles/toolkit-js 0.71.7\"},\"layers\":[{\"ty\":4,\"nm\":\"Vector 5821 copy Outlines\",\"sr\":1,\"st\":0,\"op\":60,\"ip\":0,\"ln\":\"18\",\"hasMask\":false,\"ao\":0,\"ks\":{\"a\":{\"a\":0,\"k\":[12.5,6.5,0]},\"s\":{\"a\":0,\"k\":[100,100,100]},\"p\":{\"a\":1,\"k\":[{\"o\":{\"x\":0.167,\"y\":0.167},\"i\":{\"x\":0.833,\"y\":0.833},\"s\":[12,8,0],\"t\":0},{\"s\":[4,8,0],\"t\":59}]},\"r\":{\"a\":0,\"k\":0},\"sa\":{\"a\":0,\"k\":0},\"o\":{\"a\":0,\"k\":24}},\"shapes\":[{\"ty\":\"gr\",\"nm\":\"Group 1\",\"it\":[{\"ty\":\"sh\",\"nm\":\"Path 1\",\"d\":1,\"ks\":{\"a\":0,\"k\":{\"c\":false,\"i\":[[0,0],[-0.289,3.296],[2.218,0],[-0.23,-2.627],[-4.452,0],[-0.289,3.296],[2.218,0],[-0.23,-2.627],[-4.452,0],[-0.289,3.296],[2.218,0],[-0.232,-2.627],[-4.443,0]],\"o\":[[4.452,0],[0.23,-2.627],[-2.218,0],[0.289,3.296],[4.452,0],[0.23,-2.627],[-2.218,0],[0.289,3.296],[4.452,0],[0.23,-2.627],[-2.218,0],[0.292,3.296],[0,0]],\"v\":[[-12,6],[-4.975,-1.012],[-8,-6],[-11.025,-1.012],[-4,6],[3.025,-1.012],[0,-6],[-3.025,-1.012],[4,6],[11.025,-1.012],[8,-6],[4.98,-1.012],[12,6]]}}},{\"ty\":\"st\",\"nm\":\"Stroke 1\",\"lc\":2,\"lj\":2,\"ml\":4,\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":1.4},\"c\":{\"a\":0,\"k\":[1,1,1]}},{\"ty\":\"tr\",\"a\":{\"a\":0,\"k\":[0,0]},\"s\":{\"a\":0,\"k\":[100,100]},\"p\":{\"a\":0,\"k\":[12.5,6.5]},\"r\":{\"a\":0,\"k\":0},\"sa\":{\"a\":0,\"k\":0},\"o\":{\"a\":0,\"k\":100}}]},{\"ty\":\"tm\",\"nm\":\"Trim Paths 1\",\"e\":{\"a\":1,\"k\":[{\"o\":{\"x\":0.341,\"y\":0.992},\"i\":{\"x\":0.269,\"y\":0.491},\"s\":[44],\"t\":0},{\"s\":[77],\"t\":59}]},\"o\":{\"a\":0,\"k\":0},\"s\":{\"a\":1,\"k\":[{\"o\":{\"x\":0.32,\"y\":0.313},\"i\":{\"x\":0.826,\"y\":0.143},\"s\":[23],\"t\":0},{\"s\":[57],\"t\":59}]},\"m\":1}],\"ind\":1}],\"v\":\"5.7.0\",\"fr\":60,\"op\":60,\"ip\":0,\"assets\":[]};\n",
      "type": "registry:lib",
      "target": "components/agent-elements/spiral-loader-data.ts"
    },
    {
      "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/utils/tool-adapters.ts",
      "content": "import type { TimelineStep, StepState } from \"../types/timeline\";\n\nfunction calculateDiffStatsFromPatch(\n  patches: Array<{ lines?: string[] }>,\n): string | undefined {\n  let addedLines = 0;\n  let removedLines = 0;\n\n  for (const patch of patches) {\n    if (!patch.lines) continue;\n    for (const line of patch.lines) {\n      if (line.startsWith(\"+\")) addedLines++;\n      else if (line.startsWith(\"-\")) removedLines++;\n    }\n  }\n\n  if (addedLines === 0 && removedLines === 0) return undefined;\n\n  const parts: string[] = [];\n  if (addedLines > 0) parts.push(`+${addedLines}`);\n  if (removedLines > 0) parts.push(`-${removedLines}`);\n  return parts.join(\" \");\n}\n\nfunction getDiffLinesFromPatch(\n  patches: Array<{ lines?: string[] }>,\n): { type: \"add\" | \"remove\" | \"context\"; content: string }[] {\n  const result: { type: \"add\" | \"remove\" | \"context\"; content: string }[] = [];\n\n  for (const patch of patches) {\n    if (!patch.lines) continue;\n    for (const line of patch.lines) {\n      if (line.startsWith(\"+\")) {\n        result.push({ type: \"add\", content: line.slice(1) });\n      } else if (line.startsWith(\"-\")) {\n        result.push({ type: \"remove\", content: line.slice(1) });\n      } else if (line.startsWith(\" \")) {\n        result.push({ type: \"context\", content: line.slice(1) });\n      }\n    }\n  }\n\n  return result;\n}\n\nexport function mapToolStateToStepState(\n  aiState: \"partial-call\" | \"call\" | \"result\",\n): StepState {\n  return aiState === \"result\" ? \"complete\" : \"animating\";\n}\n\nexport function mapToolNameToVariant(\n  toolName: string,\n): \"thinking\" | \"action\" | \"search\" | undefined {\n  const lower = toolName.toLowerCase();\n  if (lower === \"thinking\" || lower === \"reasoning\") return \"thinking\";\n  if (\n    lower === \"websearch\" ||\n    lower === \"web_search\" ||\n    lower === \"grep\" ||\n    lower === \"glob\" ||\n    lower === \"webfetch\" ||\n    lower === \"web_fetch\"\n  )\n    return \"search\";\n  return undefined;\n}\n\nfunction extractToolDetail(\n  toolName: string,\n  args: Record<string, any>,\n): string {\n  switch (toolName) {\n    case \"Bash\":\n      return args?.command ? String(args.command).slice(0, 80) : \"\";\n    case \"Edit\":\n    case \"Write\":\n    case \"Read\":\n      return args?.file_path\n        ? (String(args.file_path).split(\"/\").pop() ?? \"\")\n        : \"\";\n    case \"Grep\":\n      return args?.pattern ? String(args.pattern) : \"\";\n    case \"Glob\":\n      return args?.pattern ? String(args.pattern) : \"\";\n    case \"WebSearch\":\n    case \"web_search\":\n      return args?.query ? String(args.query) : \"\";\n    case \"WebFetch\":\n    case \"web_fetch\":\n      return args?.url ? String(args.url).slice(0, 60) : \"\";\n    default:\n      return \"\";\n  }\n}\n\nexport function mapToolInvocationToStep(\n  toolCallId: string,\n  toolInvocation: {\n    toolName: string;\n    args?: Record<string, any>;\n    state: \"partial-call\" | \"call\" | \"result\";\n    result?: any;\n  },\n): Extract<TimelineStep, { type: \"tool-call\" }> {\n  const { toolName, args = {}, result } = toolInvocation;\n  const displayToolName =\n    toolName === \"PlanWrite\"\n      ? \"Plan\"\n      : toolName === \"TodoWrite\"\n        ? \"Todo\"\n        : toolName;\n  const detail = extractToolDetail(toolName, args);\n\n  const step: Extract<TimelineStep, { type: \"tool-call\" }> = {\n    id: toolCallId,\n    type: \"tool-call\",\n    toolName: displayToolName,\n    toolDetail: detail,\n    duration: Number.MAX_SAFE_INTEGER,\n    toolVariant: mapToolNameToVariant(toolName),\n  };\n\n  if (toolName === \"Bash\") {\n    step.bashCommand = args?.command ? String(args.command) : undefined;\n    if (toolInvocation.state === \"result\" && result) {\n      if (typeof result === \"string\") {\n        step.bashOutput = result;\n        step.bashSuccess = true;\n      } else if (typeof result === \"object\") {\n        const stdout =\n          typeof result?.stdout === \"string\"\n            ? result.stdout\n            : typeof result?.output === \"string\"\n              ? result.output\n              : \"\";\n        const stderr = typeof result?.stderr === \"string\" ? result.stderr : \"\";\n        step.bashOutput = [stdout, stderr]\n          .filter(Boolean)\n          .join(stdout && stderr ? \"\\n\" : \"\");\n        const exitCode = result?.exitCode ?? result?.exit_code;\n        step.bashSuccess = exitCode === undefined ? true : exitCode === 0;\n      } else {\n        step.bashOutput = JSON.stringify(result);\n        step.bashSuccess = true;\n      }\n    }\n  }\n\n  if (toolName === \"Edit\" || toolName === \"Write\" || toolName === \"Read\") {\n    step.filePath = args?.file_path ? String(args.file_path) : undefined;\n  }\n\n  if (toolName === \"Write\") {\n    const content =\n      typeof result?.content === \"string\"\n        ? result.content\n        : typeof args?.content === \"string\"\n          ? args.content\n          : \"\";\n\n    if (content) {\n      const lines = content.split(\"\\n\");\n      step.diffStats = `+${lines.length}`;\n      step.diffLines = lines.map((line: string) => ({\n        type: \"add\",\n        content: line,\n      }));\n    }\n  }\n\n  if (toolName === \"Edit\" && Array.isArray(result?.structuredPatch)) {\n    step.diffStats = calculateDiffStatsFromPatch(result.structuredPatch);\n    step.diffLines = getDiffLinesFromPatch(result.structuredPatch);\n  }\n\n  if (\n    toolName === \"WebSearch\" ||\n    toolName === \"web_search\" ||\n    toolName === \"Grep\" ||\n    toolName === \"Glob\"\n  ) {\n    step.searchQuery =\n      (args?.query ?? args?.pattern)\n        ? String(args?.query ?? args?.pattern)\n        : undefined;\n    step.searchSource =\n      toolName === \"WebSearch\" || toolName === \"web_search\" ? \"web\" : \"code\";\n  }\n\n  if (\n    toolName.toLowerCase() === \"thinking\" ||\n    toolName.toLowerCase() === \"reasoning\"\n  ) {\n    step.thoughtContent =\n      typeof args?.thought === \"string\"\n        ? args.thought\n        : typeof result === \"string\"\n          ? result\n          : undefined;\n  }\n\n  return step;\n}\n",
      "type": "registry:lib",
      "target": "components/agent-elements/utils/tool-adapters.ts"
    },
    {
      "path": "registry/agent-elements/components/tools/tool-approval-footer.tsx",
      "content": "import { memo, useMemo, useState } from \"react\";\n\nexport type ToolApproval = {\n  approveLabel?: string;\n  rejectLabel?: string;\n  onApprove?: () => void;\n  onReject?: () => void;\n};\n\nexport type ToolApprovalFooterProps = ToolApproval & {\n  isPending?: boolean;\n};\n\nexport const ToolApprovalFooter = memo(function ToolApprovalFooter({\n  isPending,\n  approveLabel,\n  rejectLabel,\n  onApprove,\n  onReject,\n}: ToolApprovalFooterProps) {\n  const [decision, setDecision] = useState<\"approved\" | \"rejected\" | null>(\n    null,\n  );\n\n  const approveText =\n    decision === \"approved\" ? \"Approved\" : (approveLabel ?? \"Next\");\n  const rejectText =\n    decision === \"rejected\" ? \"Skipped\" : (rejectLabel ?? \"Skip\");\n\n  const handleApprove = () => {\n    if (decision) return;\n    setDecision(\"approved\");\n    onApprove?.();\n  };\n\n  const handleReject = () => {\n    if (decision) return;\n    setDecision(\"rejected\");\n    onReject?.();\n  };\n\n  const statusConfig = useMemo(() => {\n    if (decision === \"approved\") return { label: \"Waiting\", dots: true };\n    if (decision === \"rejected\") return { label: \"Canceled\", dots: false };\n    if (isPending) return { label: \"Starting\", dots: true };\n    // Default \"ready\" state — buttons themselves communicate the affordance,\n    // an extra \"Ready\" label just adds noise. Render an empty spacer so the\n    // buttons stay right-aligned via justify-between.\n    return null;\n  }, [decision, isPending]);\n\n  return (\n    <div className=\"flex items-center justify-between py-1 pl-3 pr-2 border-t border-border bg-an-tool-background\">\n      {statusConfig ? (\n        <span className=\"text-xs text-an-tool-color-muted\">\n          {statusConfig.label}\n          {statusConfig.dots && (\n            <span className=\"inline-flex\" aria-hidden=\"true\">\n              <span className=\"text-an-tool-color-muted animate-[loading-dots_1.4s_infinite_0.2s]\">\n                .\n              </span>\n              <span className=\"text-an-tool-color-muted animate-[loading-dots_1.4s_infinite_0.4s]\">\n                .\n              </span>\n              <span className=\"text-an-tool-color-muted animate-[loading-dots_1.4s_infinite_0.6s]\">\n                .\n              </span>\n            </span>\n          )}\n        </span>\n      ) : (\n        <span aria-hidden=\"true\" />\n      )}\n      <div className=\"flex gap-1\">\n        <button\n          type=\"button\"\n          onClick={handleReject}\n          disabled={Boolean(decision)}\n          className=\"h-5 px-1.5 rounded-[4px] text-xs text-muted-foreground hover:text-an-tool-color hover:bg-muted/50 active:scale-[0.98] transition-[background-color,color,transform] duration-150 disabled:opacity-60 disabled:hover:bg-transparent disabled:active:scale-100\"\n        >\n          {rejectText}\n        </button>\n        <button\n          type=\"button\"\n          onClick={handleApprove}\n          disabled={Boolean(decision)}\n          className=\"h-5 px-1.5 rounded-[4px] text-xs font-medium bg-an-primary-color text-an-send-button-color hover:bg-an-primary-color/90 active:scale-[0.98] transition-[background-color,transform] duration-150 disabled:opacity-60 disabled:hover:bg-an-primary-color disabled:active:scale-100\"\n        >\n          {approveText}\n        </button>\n      </div>\n    </div>\n  );\n});\n",
      "type": "registry:ui",
      "target": "components/agent-elements/tools/tool-approval-footer.tsx"
    },
    {
      "path": "registry/agent-elements/icons/file-ext-icon.tsx",
      "content": "export function FileExtIcon({\n  filename,\n  className,\n}: {\n  filename: string;\n  className?: string;\n}) {\n  const ext = filename.split(\".\").pop()?.toLowerCase() ?? \"\";\n  const cls = className ?? \"w-2.5 h-2.5 shrink-0\";\n\n  if (ext === \"ts\" || ext === \"tsx\") {\n    return (\n      <svg viewBox=\"0 0 32 32\" className={cls}>\n        <path\n          d=\"M23.827,8.243A4.424,4.424,0,0,1,26.05,9.524a5.853,5.853,0,0,1,.852,1.143c.011.045-1.534,1.083-2.471,1.662-.034.023-.169-.124-.322-.35a2.014,2.014,0,0,0-1.67-1c-1.077-.074-1.771.49-1.766,1.433a1.3,1.3,0,0,0,.153.666c.237.49.677.784,2.059,1.383,2.544,1.095,3.636,1.817,4.31,2.843a5.158,5.158,0,0,1,.416,4.333,4.764,4.764,0,0,1-3.932,2.815,10.9,10.9,0,0,1-2.708-.028,6.531,6.531,0,0,1-3.616-1.884,6.278,6.278,0,0,1-.926-1.371,2.655,2.655,0,0,1,.327-.208c.158-.09.756-.434,1.32-.761L19.1,19.6l.214.312a4.771,4.771,0,0,0,1.35,1.292,3.3,3.3,0,0,0,3.458-.175,1.545,1.545,0,0,0,.2-1.974c-.276-.395-.84-.727-2.443-1.422a8.8,8.8,0,0,1-3.349-2.055,4.687,4.687,0,0,1-.976-1.777,7.116,7.116,0,0,1-.062-2.268,4.332,4.332,0,0,1,3.644-3.374A9,9,0,0,1,23.827,8.243ZM15.484,9.726l.011,1.454h-4.63V24.328H7.6V11.183H2.97V9.755A13.986,13.986,0,0,1,3.01,8.289c.017-.023,2.832-.034,6.245-.028l6.211.017Z\"\n          fill=\"#007acc\"\n        />\n      </svg>\n    );\n  }\n  if (ext === \"js\" || ext === \"mjs\" || ext === \"cjs\" || ext === \"jsx\") {\n    return (\n      <svg viewBox=\"0 0 32 32\" className={cls}>\n        <rect x=\"2\" y=\"2\" width=\"28\" height=\"28\" rx=\"1.312\" fill=\"#f5de19\" />\n        <path\n          d=\"M20.809,23.875a2.866,2.866,0,0,0,2.6,1.6c1.09,0,1.787-.545,1.787-1.3,0-.9-.715-1.222-1.916-1.747l-.658-.282c-1.9-.809-3.16-1.822-3.16-3.964,0-1.973,1.5-3.476,3.853-3.476a3.889,3.889,0,0,1,3.742,2.107L25,18.128A1.789,1.789,0,0,0,23.311,17a1.145,1.145,0,0,0-1.259,1.128c0,.789.489,1.109,1.618,1.6l.658.282c2.236.959,3.5,1.936,3.5,4.133,0,2.369-1.861,3.667-4.36,3.667a5.055,5.055,0,0,1-4.795-2.691Zm-9.295.228c.413.733.789,1.353,1.693,1.353.864,0,1.41-.338,1.41-1.653V14.856h2.631v8.982c0,2.724-1.6,3.964-3.929,3.964a4.085,4.085,0,0,1-3.947-2.4Z\"\n          fill=\"#000\"\n        />\n      </svg>\n    );\n  }\n  if (ext === \"json\" || ext === \"jsonc\") {\n    return (\n      <svg viewBox=\"0 0 32 32\" className={cls}>\n        <path\n          d=\"M4.014,14.976a2.51,2.51,0,0,0,1.567-.518A2.377,2.377,0,0,0,6.386,13l.129-1.334A5.435,5.435,0,0,1,7.6,9.071,3.157,3.157,0,0,1,10.519,8h1.316v1.86H10.937a1.257,1.257,0,0,0-1.1.465,3.405,3.405,0,0,0-.357,1.585l-.127,1.244a3.283,3.283,0,0,1-.573,1.61A2.482,2.482,0,0,1,7.6,15.537a2.482,2.482,0,0,1,1.18.773A3.283,3.283,0,0,1,9.349,17.9l.127,1.244a3.405,3.405,0,0,0,.357,1.585,1.257,1.257,0,0,0,1.1.465h.9V23.06H10.519A3.157,3.157,0,0,1,7.6,21.981a5.435,5.435,0,0,1-1.082-2.595L6.386,18.05a2.377,2.377,0,0,0-.805-1.456A2.51,2.51,0,0,0,4.014,16.07Z\"\n          fill=\"#fbc02d\"\n        />\n        <path\n          d=\"M27.986,16.07a2.51,2.51,0,0,0-1.567.518,2.377,2.377,0,0,0-.805,1.456l-.129,1.334a5.435,5.435,0,0,1-1.082,2.595A3.157,3.157,0,0,1,21.481,23.06H20.165V21.2h.9a1.257,1.257,0,0,0,1.1-.465,3.405,3.405,0,0,0,.357-1.585l.127-1.244a3.283,3.283,0,0,1,.573-1.61,2.482,2.482,0,0,1,1.18-.773,2.482,2.482,0,0,1-1.18-.773,3.283,3.283,0,0,1-.573-1.61L22.527,11.9a3.405,3.405,0,0,0-.357-1.585,1.257,1.257,0,0,0-1.1-.465h-.9V8h1.316a3.157,3.157,0,0,1,2.924,1.071,5.435,5.435,0,0,1,1.082,2.595l.129,1.334A2.377,2.377,0,0,0,26.419,14.458a2.51,2.51,0,0,0,1.567.518Z\"\n          fill=\"#fbc02d\"\n        />\n      </svg>\n    );\n  }\n\n  return null;\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/icons/file-ext-icon.tsx"
    },
    {
      "path": "registry/agent-elements/icons.tsx",
      "content": "import React from \"react\";\n\ntype IconProps = React.SVGProps<SVGSVGElement> & {\n  className?: string;\n};\n\nexport function IconSpinner({ className, ...rest }: IconProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"16\"\n      height=\"16\"\n      fill=\"none\"\n      className={\n        className ?? \"animate-spin will-change-transform text-muted-foreground\"\n      }\n      {...rest}\n    >\n      <circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke=\"currentColor\"\n        strokeWidth=\"3\"\n        strokeLinecap=\"round\"\n        fill=\"none\"\n        opacity={0.2}\n      />\n      <path\n        d=\"M12 2C6.48 2 2 6.48 2 12\"\n        stroke=\"currentColor\"\n        strokeWidth=\"3\"\n        strokeLinecap=\"round\"\n        fill=\"none\"\n      />\n    </svg>\n  );\n}\n\nexport function CheckIcon(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      {...props}\n    >\n      <path\n        d=\"M5 12.75L10 19L19 5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function IconArrowRight(props: IconProps) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" width=\"24\" height=\"24\" {...props}>\n      <path\n        d=\"M14 6L20 12L14 18\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M19 12H4\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function IconDoubleChevronRight(props: IconProps) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" width=\"16\" height=\"16\" {...props}>\n      <path\n        d=\"M6 17L11 12L6 7M13 17L18 12L13 7\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/agent-elements/icons.tsx"
    },
    {
      "path": "registry/agent-elements/icons/source-icons.ts",
      "content": "export type SourceType =\n  | \"google\"\n  | \"booking\"\n  | \"tripadvisor\"\n  | \"expedia\"\n  | \"airbnb\"\n  | \"github\"\n  | \"stackoverflow\"\n  | \"arxiv\"\n  | \"scholar\"\n  | \"stripe\"\n  | \"zendesk\"\n  | \"web\";\n",
      "type": "registry:lib",
      "target": "components/agent-elements/icons/source-icons.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\"": ""
  }
}