import { createEffect, createSignal, For, Match, on, onCleanup, onMount, Show, splitProps, Switch, type JSX, } from "solid-js" import { animate, type AnimationPlaybackControls, tunableSpringValue, COLLAPSIBLE_SPRING } from "./motion" import { Collapsible } from "./collapsible" import { TextShimmer } from "./text-shimmer" import { hold } from "./tool-utils" export type TriggerTitle = { title: string titleClass?: string subtitle?: string subtitleClass?: string args?: string[] argsClass?: string action?: JSX.Element } const isTriggerTitle = (val: any): val is TriggerTitle => { return ( typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node)) ) } interface ToolCallPanelBaseProps { icon: string trigger: TriggerTitle | JSX.Element children?: JSX.Element status?: string animate?: boolean hideDetails?: boolean defaultOpen?: boolean forceOpen?: boolean defer?: boolean locked?: boolean watchDetails?: boolean springContent?: boolean onSubtitleClick?: () => void } function ToolCallTriggerBody(props: { trigger: TriggerTitle | JSX.Element pending: boolean onSubtitleClick?: () => void arrow?: boolean }) { return (
{(trigger) => (
{ if (!props.onSubtitleClick) return e.stopPropagation() props.onSubtitleClick() }} > {trigger().subtitle} {(arg) => ( {arg} )}
{trigger().action}
)}
{props.trigger as JSX.Element}
) } function ToolCallPanel(props: ToolCallPanelBaseProps) { const [open, setOpen] = createSignal(props.defaultOpen ?? false) const [ready, setReady] = createSignal(open()) const pendingRaw = () => props.status === "pending" || props.status === "running" const pending = hold(pendingRaw, 1000) const watchDetails = () => props.watchDetails !== false let frame: number | undefined const cancel = () => { if (frame === undefined) return cancelAnimationFrame(frame) frame = undefined } onCleanup(cancel) createEffect(() => { if (props.forceOpen) setOpen(true) }) createEffect( on( open, (value) => { if (!props.defer || props.springContent) return if (!value) { cancel() setReady(false) return } cancel() frame = requestAnimationFrame(() => { frame = undefined if (!open()) return setReady(true) }) }, { defer: true }, ), ) // Animated content height — single springValue drives all height changes let contentRef: HTMLDivElement | undefined let bodyRef: HTMLDivElement | undefined let fadeAnim: AnimationPlaybackControls | undefined let observer: ResizeObserver | undefined let resizeFrame: number | undefined const initialOpen = open() const heightSpring = tunableSpringValue(0, COLLAPSIBLE_SPRING) const read = () => Math.max(0, Math.ceil(bodyRef?.getBoundingClientRect().height ?? 0)) const doOpen = () => { if (!contentRef || !bodyRef) return contentRef.style.display = "" // Ensure fade starts from 0 if content was hidden (first open or after close cleared styles) if (bodyRef.style.opacity === "") { bodyRef.style.opacity = "0" bodyRef.style.filter = "blur(2px)" } const next = read() fadeAnim?.stop() fadeAnim = animate(bodyRef, { opacity: 1, filter: "blur(0px)" }, COLLAPSIBLE_SPRING) fadeAnim.finished.then(() => { if (!bodyRef) return bodyRef.style.opacity = "" bodyRef.style.filter = "" }) heightSpring.set(next) } const doClose = () => { if (!contentRef || !bodyRef) return fadeAnim?.stop() fadeAnim = animate(bodyRef, { opacity: 0, filter: "blur(2px)" }, COLLAPSIBLE_SPRING) fadeAnim.finished.then(() => { if (!contentRef || open()) return contentRef.style.display = "none" }) heightSpring.set(0) } const grow = () => { if (!contentRef || !open()) return const next = read() if (Math.abs(next - heightSpring.get()) < 1) return heightSpring.set(next) } onMount(() => { if (!props.springContent || props.animate === false || !contentRef || !bodyRef) return const offChange = heightSpring.on("change", (v) => { if (!contentRef) return contentRef.style.height = `${Math.max(0, Math.ceil(v))}px` }) onCleanup(() => { offChange() }) if (watchDetails()) { observer = new ResizeObserver(() => { if (resizeFrame !== undefined) return resizeFrame = requestAnimationFrame(() => { resizeFrame = undefined grow() }) }) observer.observe(bodyRef) } if (!open()) return if (contentRef.style.display !== "none") { const next = read() heightSpring.jump(next) contentRef.style.height = `${next}px` return } let mountFrame: number | undefined = requestAnimationFrame(() => { mountFrame = undefined if (!open()) return doOpen() }) onCleanup(() => { if (mountFrame !== undefined) cancelAnimationFrame(mountFrame) }) }) createEffect( on( open, (isOpen) => { if (!props.springContent || props.animate === false || !contentRef) return if (isOpen) doOpen() else doClose() }, { defer: true }, ), ) onCleanup(() => { if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame) observer?.disconnect() fadeAnim?.stop() heightSpring.destroy() }) const handleOpenChange = (value: boolean) => { if (pending()) return if (props.locked && !value) return setOpen(value) } return (
{props.children}
{props.children}
) } function label(input: Record | undefined) { const keys = ["description", "query", "url", "filePath", "path", "pattern", "name"] return keys.map((key) => input?.[key]).find((value): value is string => typeof value === "string" && value.length > 0) } function args(input: Record | undefined) { if (!input) return [] const skip = new Set(["description", "query", "url", "filePath", "path", "pattern", "name"]) return Object.entries(input) .filter(([key]) => !skip.has(key)) .flatMap(([key, value]) => { if (typeof value === "string") return [`${key}=${value}`] if (typeof value === "number") return [`${key}=${value}`] if (typeof value === "boolean") return [`${key}=${value}`] return [] }) .slice(0, 3) } export interface ToolCallRowProps { variant: "row" icon: string trigger: TriggerTitle | JSX.Element status?: string animate?: boolean onSubtitleClick?: () => void open?: boolean showArrow?: boolean onOpenChange?: (value: boolean) => void } export interface ToolCallPanelProps extends Omit { variant: "panel" } export type ToolCallProps = ToolCallRowProps | ToolCallPanelProps function ToolCallRoot(props: ToolCallProps) { const pending = () => props.status === "pending" || props.status === "running" if (props.variant === "row") { return (
} > {(onOpenChange) => ( )}
) } const [, rest] = splitProps(props, ["variant"]) return } export const ToolCall = ToolCallRoot export function GenericTool(props: { tool: string status?: string hideDetails?: boolean input?: Record }) { return ( ) }