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 ? (
-