diff --git a/studio/frontend/src/components/assistant-ui/tool-fallback.tsx b/studio/frontend/src/components/assistant-ui/tool-fallback.tsx index e407163045..f0d6262a45 100644 --- a/studio/frontend/src/components/assistant-ui/tool-fallback.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-fallback.tsx @@ -134,42 +134,42 @@ function ToolFallbackTrigger({ data-slot="tool-fallback-trigger-icon" className="aui-tool-fallback-trigger-icon size-4 shrink-0 animate-spin" /> + ) : ToolIcon ? ( + ) : ( - ToolIcon ? ( - - ) : ( - - ) + )} - - {label}: {toolName} + + {label}:{" "} + {toolName} {isRunning && ( - {label}: {toolName} + {label}:{" "} + {toolName} )} @@ -250,10 +250,7 @@ function ToolFallbackResult({ return (

Result:

@@ -315,9 +312,7 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({ status?.type === "incomplete" && status.reason === "cancelled"; return ( - + diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-code-execution.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-code-execution.tsx index 8141b8b2cc..b884c4d2d8 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-code-execution.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-code-execution.tsx @@ -3,9 +3,19 @@ "use client"; -import { type ToolCallMessagePartComponent, useAuiState } from "@assistant-ui/react"; -import { FileTextIcon, LoaderIcon, TerminalIcon } from "lucide-react"; -import { memo, useEffect, useState } from "react"; +import { copyToClipboard } from "@/lib/copy-to-clipboard"; +import { + type ToolCallMessagePartComponent, + useAuiState, +} from "@assistant-ui/react"; +import { + CheckIcon, + CopyIcon, + FileTextIcon, + LoaderIcon, + TerminalIcon, +} from "lucide-react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ToolFallbackContent, ToolFallbackRoot, @@ -36,6 +46,65 @@ interface CodeExecutionArgs { path?: string; } +const MAX_COMMAND_LABEL = 80; +const MAX_RESULT_DISPLAY = 10_000; +const COPY_RESET_MS = 2000; + +function truncateCommandLabel(text: string): string { + const normalized = text.replace(/\s+/g, " ").trim(); + if (normalized.length <= MAX_COMMAND_LABEL) { + return normalized; + } + const head = Math.ceil((MAX_COMMAND_LABEL - 3) * 0.65); + const tail = MAX_COMMAND_LABEL - head - 3; + return `${normalized.slice(0, head)}...${normalized.slice(-tail)}`; +} + +function truncateResult(text: string): string { + return text.length <= MAX_RESULT_DISPLAY + ? text + : `${text.slice(0, MAX_RESULT_DISPLAY)}\n... (truncated)`; +} + +function CopyBtn({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + const timer = useRef | null>(null); + + useEffect(() => { + return () => { + if (timer.current) { + clearTimeout(timer.current); + } + }; + }, []); + + const copy = useCallback(async () => { + if (await copyToClipboard(text)) { + setCopied(true); + if (timer.current) { + clearTimeout(timer.current); + } + timer.current = setTimeout(() => setCopied(false), COPY_RESET_MS); + } + }, [text]); + + return ( + + ); +} + const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({ args, result, @@ -47,6 +116,8 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({ const path = parsedArgs.path ?? ""; const isRunning = status?.type === "running"; + const commandLabel = command ? truncateCommandLabel(command) : ""; + let runningLabel: string; let completedLabel: string; let Icon = TerminalIcon; @@ -67,7 +138,7 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({ } } else { runningLabel = "Running command…"; - completedLabel = command ? `Ran \`${command}\`` : "Ran command"; + completedLabel = commandLabel ? `Ran \`${commandLabel}\`` : "Ran command"; } // Collapse the card once the model has resumed streaming prose after @@ -90,12 +161,19 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({ } }, [isRunning, hasText]); - const resultText = - typeof result === "string" - ? result - : result != null - ? JSON.stringify(result, null, 2) - : ""; + const resultText = useMemo( + () => + typeof result === "string" + ? result + : result != null + ? JSON.stringify(result, null, 2) + : "", + [result], + ); + const displayedResult = useMemo( + () => truncateResult(resultText), + [resultText], + ); return ( @@ -111,9 +189,14 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({ {runningLabel}
) : resultText ? ( -
-            {resultText}
-          
+
+
+ +
+
+              {displayedResult}
+            
+
) : null}