"use client"; import { Ban, Braces, Check, ChevronDown, CircleCheck, CircleX, Copy, LoaderCircle, RotateCcw, SquareTerminal, Wrench, } from "lucide-react"; import { motion, useReducedMotion } from "motion/react"; import { type ReactNode, useCallback, useEffect, useId, useLayoutEffect, useRef, useState, } from "react"; import { AgentCode, type AgentCodeLanguage, } from "@/components/agents/agent-code"; import { ActionSwapRollText } from "@/components/motion/action-swap-roll"; import { AgentDisclosure } from "@/components/agents/agent-disclosure"; import { SPRING_PRESS, SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type ToolResultStatus = "running" | "success" | "error" | "cancelled"; export type ToolResultKind = "terminal" | "request" | "custom"; export interface ToolResultProps { tool: ReactNode; title: ReactNode; children: ReactNode; status?: ToolResultStatus; kind?: ToolResultKind; meta?: ReactNode; icon?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; collapseOnComplete?: boolean; maxHeight?: number; copyText?: string; onCopy?: () => void | Promise; onRetry?: () => void; className?: string; contentClassName?: string; } export interface ToolResultOutputProps { children: string; language?: AgentCodeLanguage; className?: string; } function getStatusLabel(status: ToolResultStatus) { if (status === "running") return "Running"; if (status === "success") return "Completed"; if (status === "error") return "Failed"; return "Cancelled"; } function getSwapKey(value: ReactNode, fallback: string) { return typeof value === "string" || typeof value === "number" ? String(value) : fallback; } function getStatusClass(status: ToolResultStatus) { if (status === "running") { return "text-blue-600 dark:text-blue-400"; } if (status === "success") { return "text-emerald-600 dark:text-emerald-400"; } if (status === "error") { return "text-rose-600 dark:text-rose-400"; } return "text-muted-foreground"; } function KindIcon({ kind }: { kind: ToolResultKind }) { if (kind === "terminal") return ; if (kind === "request") return ; return ; } function StatusIcon({ status, reduce, }: { status: ToolResultStatus; reduce: boolean; }) { if (status === "running") { return ; } if (status === "success") return ; if (status === "error") return ; return ; } function ToolResultAction({ label, onClick, children, }: { label: string; onClick: () => void; children: ReactNode; }) { const reduce = useReducedMotion() ?? false; return ( {children} ); } export function ToolResultOutput({ children, language = "bash", className, }: ToolResultOutputProps) { return ( ); } export function ToolResult({ tool, title, children, status = "running", kind = "custom", meta, icon, open, defaultOpen = true, onOpenChange, collapseOnComplete = true, maxHeight = 220, copyText, onCopy, onRetry, className, contentClassName, }: ToolResultProps) { const reduce = useReducedMotion() ?? false; const baseId = useId(); const triggerId = `${baseId}-trigger`; const contentId = `${baseId}-content`; const viewportRef = useRef(null); const previousStatus = useRef(status); const copyTimer = useRef(undefined); const [copied, setCopied] = useState(false); const [internalOpen, setInternalOpen] = useState(defaultOpen); const currentOpen = open ?? internalOpen; const running = status === "running"; const canCopy = Boolean(copyText || onCopy); const titleKey = getSwapKey(title, status); const metaKey = getSwapKey(meta, `${status}-meta`); const toolKey = getSwapKey(tool, `${status}-tool`); const statusLabel = getStatusLabel(status); const setOpen = useCallback( (next: boolean) => { if (open === undefined) setInternalOpen(next); onOpenChange?.(next); }, [onOpenChange, open], ); useEffect(() => { if (previousStatus.current !== "running" && status === "running") { setOpen(true); } if ( previousStatus.current === "running" && status !== "running" && collapseOnComplete ) { setOpen(false); } previousStatus.current = status; }, [collapseOnComplete, setOpen, status]); useEffect( () => () => { if (copyTimer.current) window.clearTimeout(copyTimer.current); }, [], ); useLayoutEffect(() => { const viewport = viewportRef.current; if (!viewport || !currentOpen || !running) return; const frame = requestAnimationFrame(() => { if (typeof viewport.scrollTo === "function") { viewport.scrollTo({ top: viewport.scrollHeight, behavior: reduce ? "auto" : "smooth", }); } else { viewport.scrollTop = viewport.scrollHeight; } }); return () => cancelAnimationFrame(frame); }); const handleCopy = useCallback(async () => { if (onCopy) await onCopy(); else if (copyText) await navigator.clipboard?.writeText(copyText); setCopied(true); if (copyTimer.current) window.clearTimeout(copyTimer.current); copyTimer.current = window.setTimeout(() => setCopied(false), 1600); }, [copyText, onCopy]); return (
{children}
{canCopy || onRetry ? (
{canCopy ? ( {copied ? ( ) : ( )} ) : null} {onRetry ? ( ) : null} {statusLabel}
) : null}
); }