import { memo, useMemo, useState, type ReactNode } from "react" import type { AdaptedContentPart } from "@/lib/adapters/ai-elements-adapter" import type { AgentToolCall } from "@/lib/types" import { tryParseJson, extractJsonField } from "./content-parts-renderer" import { MessageResponse } from "@/components/ai-elements/message" import { Shimmer } from "@/components/ai-elements/shimmer" import { getStatusBadge } from "@/components/ai-elements/tool" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { cn } from "@/lib/utils" import { ChevronDownIcon, ChevronRightIcon, CompassIcon, MapIcon, TerminalIcon, WrenchIcon, } from "lucide-react" import { useTranslations } from "next-intl" // ── helpers ──────────────────────────────────────────────────────────── const ICON_CLASS = "size-4 text-muted-foreground" function getAgentIcon(subagentType: string | null) { const t = subagentType?.toLowerCase() ?? "" if (t.includes("explore")) return if (t.includes("plan")) return if (t.includes("bash")) return return } function getAccentColor(subagentType: string | null): string { const t = subagentType?.toLowerCase() ?? "" if (t.includes("explore")) return "border-l-blue-500/50 dark:border-l-blue-400/40" if (t.includes("plan")) return "border-l-amber-500/50 dark:border-l-amber-400/40" if (t.includes("bash")) return "border-l-green-500/50 dark:border-l-green-400/40" return "border-l-purple-500/50 dark:border-l-purple-400/40" } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms` const sec = ms / 1000 if (sec < 60) return `${sec.toFixed(1)}s` return `${(sec / 60).toFixed(1)}m` } /** Convert AgentToolCall[] to AdaptedContentPart[] for reuse with ToolCallPart */ function adaptToolCalls( calls: AgentToolCall[], parentId: string ): AdaptedContentPart[] { return calls.map( (call, i): Extract => ({ type: "tool-call", toolCallId: `${parentId}-sub-${i}`, toolName: call.tool_name, input: call.input_preview ?? null, state: call.is_error ? "output-error" : "output-available", output: call.output_preview ?? null, errorText: call.is_error ? (call.output_preview ?? undefined) : undefined, }) ) } // ── main component ──────────────────────────────────────────────────── export const AgentToolCallPart = memo(function AgentToolCallPart({ part, renderToolCall, }: { part: Extract /** Render a single tool-call part — injected by the parent to avoid * circular imports (content-parts-renderer → agent-tool-call → renderer). */ renderToolCall: ( part: Extract, key: string ) => ReactNode }) { const t = useTranslations("Folder.chat.contentParts") const tTool = useTranslations("Folder.chat.tool") const isRunning = part.state === "input-available" || part.state === "input-streaming" const isError = part.state === "output-error" const [bodyOpen, setBodyOpen] = useState(isRunning || isError) const [promptOpen, setPromptOpen] = useState(false) const parsed = useMemo( () => (part.input ? tryParseJson(part.input) : null), [part.input] ) const subagentType = useMemo( () => (parsed?.subagent_type as string | undefined) ?? (part.input ? extractJsonField(part.input, "subagent_type") : null), [parsed, part.input] ) const description = useMemo( () => (parsed?.description as string | undefined) ?? (part.input ? extractJsonField(part.input, "description") : null), [parsed, part.input] ) const prompt = useMemo( () => (parsed?.prompt as string | undefined) ?? (part.input ? extractJsonField(part.input, "prompt") : null), [parsed, part.input] ) const model = useMemo( () => (parsed?.model as string | undefined) ?? (part.input ? extractJsonField(part.input, "model") : null), [parsed, part.input] ) const icon = useMemo(() => getAgentIcon(subagentType), [subagentType]) const accentColor = useMemo( () => getAccentColor(subagentType), [subagentType] ) const title = useMemo(() => { const prefix = subagentType ?? "Agent" return description ? `${prefix}: ${description}` : prefix }, [subagentType, description]) const statusLabel = part.state === "input-available" ? tTool("status.inputAvailable") : part.state === "input-streaming" ? tTool("status.inputStreaming") : part.state === "output-available" ? tTool("status.outputAvailable") : tTool("status.outputError") const agentStats = part.agentStats ?? null const adaptedToolCalls = useMemo( () => adaptToolCalls(agentStats?.tool_calls ?? [], part.toolCallId), [agentStats?.tool_calls, part.toolCallId] ) const durationSuffix = useMemo(() => { if (!agentStats?.total_duration_ms) return null return formatDuration(agentStats.total_duration_ms) }, [agentStats]) return (
{/* Header — clickable to toggle body */}
{icon} {title} {!bodyOpen && durationSuffix && ( {durationSuffix} )}
{getStatusBadge(part.state, statusLabel)}
{/* Collapsible body */}
{/* Model + duration summary */} {(model || durationSuffix) && (
{model && ( {t("agentModelLabel")}:{" "} {model} )} {durationSuffix && {durationSuffix}}
)} {/* Collapsible prompt */} {prompt && ( {t("agentPromptLabel")}
{prompt}
)} {/* Subagent tool calls — rendered with the same ToolCallPart as the outer conversation for consistent appearance */} {adaptedToolCalls.length > 0 && (
{adaptedToolCalls.map((tc, i) => renderToolCall( tc as Extract, `subagent-tc-${i}` ) )}
)} {/* Running indicator */} {isRunning && !part.output && ( Running... )} {/* Error output */} {isError && part.errorText && (
                  {part.errorText}
                
)} {/* Final output */} {part.output && !isError && (
{part.output}
)}
) })