"use client" import { forwardRef, useMemo, useState } from "react" import { cn } from "../../utils/cn" import { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon } from "../icons-v2-generated" import { ToolType } from "../platform" import { ToolIcon } from "../tool-icon" import { ExpandChevron } from "./expand-chevron" import { useCollapsible } from "./hooks/use-collapsible" import { ArgRow, ResultBlock } from "./tool-call-blocks" import { COMMAND_BODY_ARG_KEYS } from "./utils/tool-call-helpers" import type { ToolExecutionDisplayProps } from "./types" const COMMAND_BODY_KEYS = new Set(COMMAND_BODY_ARG_KEYS) const ToolExecutionDisplay = forwardRef( ({ className, message, assistantType, variant = "admin", ...props }, ref) => { const [expanded, setExpanded] = useState(false) const { innerRef, containerStyle } = useCollapsible({ expanded }) const isClient = assistantType === "fae" // Render audience (consumer-declared, NOT `assistantType`). The end-client // Fae app (`'client'`) gets a static, non-expandable row — just the // explanation + status; the command/args/result are admin-only detail the // end user doesn't need. Every admin surface keeps the expandable accordion. const isClientView = variant === "client" const isExecuting = message.type === "EXECUTING_TOOL" const isExecuted = message.type === "EXECUTED_TOOL" const integratedToolType = (message.integratedToolType as ToolType) || ("OPENFRAME" as ToolType) // Header text depends on the render audience (the consumer-declared // `variant`, NOT `assistantType`: a ticket's Fae client tab on the admin // dashboard is still `assistantType === 'fae'`). The end-client Fae app // (`variant === 'client'`) reads the human-readable explanation (what/why); // every admin surface reads the concise BE-provided title. Each falls back // to the other when its preferred field is absent (older backends / // EXECUTED rows). const previewText = variant === "client" ? message.toolExplanation?.trim() : message.toolTitle?.trim() const argEntries = useMemo>(() => { if (!message.parameters || typeof message.parameters !== "object") return [] const entries = Object.entries(message.parameters).filter( ([, v]) => v !== null && v !== undefined && v !== "", ) // The end client (Fae app) never sees the raw command/query/script body — // only the secondary args. Every admin surface DOES: surface the command // body first (as a labeled `command:` row), then the rest of the args. if (variant === "client") { return entries.filter(([k]) => !COMMAND_BODY_KEYS.has(k)) } const commandEntries = entries.filter(([k]) => COMMAND_BODY_KEYS.has(k)) const otherEntries = entries.filter(([k]) => !COMMAND_BODY_KEYS.has(k)) return [...commandEntries, ...otherEntries] }, [message.parameters, variant]) const hasResult = isExecuted && typeof message.result === "string" && message.result.length > 0 const hasBody = argEntries.length > 0 || hasResult || isExecuting const renderStatusIcon = () => { if (isExecuting) return if (isExecuted && message.success === true) return if (isExecuted && message.success === false) return return null } const headerContent = ( <> {!isClient && (
)}
{previewText}
{renderStatusIcon()}
{!isClientView && (
)} ) return (
{isClientView ? ( // Client (Fae end-user): static, non-expandable row — no chevron, no // body. Just the explanation + status; command/args/result are hidden.
{headerContent}
) : ( <>
{hasBody && (
{argEntries.map(([key, value]) => ( ))} {hasResult && ( 0 ? "mt-[var(--spacing-system-xsf)]" : undefined} /> )} {isExecuting && (
0 && "mt-[var(--spacing-system-xsf)]", )} > Result:
)}
)}
)}
) }, ) ToolExecutionDisplay.displayName = "ToolExecutionDisplay" export { ToolExecutionDisplay }