Truncate long code execution tool output (#5708)

This commit is contained in:
Wasim Yousef Said 2026-05-22 16:45:58 +02:00 committed by GitHub
commit df2d31fea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 120 additions and 42 deletions

View file

@ -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
data-slot="tool-fallback-trigger-icon"
className={cn(
"aui-tool-fallback-trigger-icon size-4 shrink-0",
isCancelled && "text-muted-foreground",
)}
/>
) : (
ToolIcon ? (
<ToolIcon
data-slot="tool-fallback-trigger-icon"
className={cn(
"aui-tool-fallback-trigger-icon size-4 shrink-0",
isCancelled && "text-muted-foreground",
)}
/>
) : (
<StatusIcon
data-slot="tool-fallback-trigger-icon"
className={cn(
"aui-tool-fallback-trigger-icon size-4 shrink-0",
isCancelled && "text-muted-foreground",
)}
/>
)
<StatusIcon
data-slot="tool-fallback-trigger-icon"
className={cn(
"aui-tool-fallback-trigger-icon size-4 shrink-0",
isCancelled && "text-muted-foreground",
)}
/>
)}
<span
data-slot="tool-fallback-trigger-label"
className={cn(
"aui-tool-fallback-trigger-label-wrapper relative inline-block grow text-left leading-none text-muted-foreground",
"aui-tool-fallback-trigger-label-wrapper relative min-w-0 grow text-left leading-none text-muted-foreground",
isCancelled && "text-muted-foreground line-through",
)}
>
<span>
{label}: <span className="font-medium text-foreground/85">{toolName}</span>
<span className="block truncate">
{label}:{" "}
<span className="font-medium text-foreground/85">{toolName}</span>
</span>
{isRunning && (
<span
aria-hidden={true}
data-slot="tool-fallback-trigger-shimmer"
className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 block truncate motion-reduce:animate-none"
>
{label}: <span className="font-medium text-foreground/85">{toolName}</span>
{label}:{" "}
<span className="font-medium text-foreground/85">{toolName}</span>
</span>
)}
</span>
@ -250,10 +250,7 @@ function ToolFallbackResult({
return (
<div
data-slot="tool-fallback-result"
className={cn(
"aui-tool-fallback-result pt-2",
className,
)}
className={cn("aui-tool-fallback-result pt-2", className)}
{...props}
>
<p className="aui-tool-fallback-result-header font-semibold">Result:</p>
@ -315,9 +312,7 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
status?.type === "incomplete" && status.reason === "cancelled";
return (
<ToolFallbackRoot
className={cn(isCancelled && "bg-muted/30")}
>
<ToolFallbackRoot className={cn(isCancelled && "bg-muted/30")}>
<ToolFallbackTrigger toolName={toolName} status={status} />
<ToolFallbackContent>
<ToolFallbackError status={status} />

View file

@ -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<ReturnType<typeof setTimeout> | 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 (
<button
type="button"
onClick={copy}
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="Copy to clipboard"
>
{copied ? (
<CheckIcon className="size-3" />
) : (
<CopyIcon className="size-3" />
)}
{copied ? "Copied" : "Copy"}
</button>
);
}
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 (
<ToolFallbackRoot open={open} onOpenChange={setOpen}>
@ -111,9 +189,14 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({
<span>{runningLabel}</span>
</div>
) : resultText ? (
<pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-2 text-xs">
{resultText}
</pre>
<div>
<div className="flex justify-end">
<CopyBtn text={resultText} />
</div>
<pre className="mt-1 max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-2 text-xs">
{displayedResult}
</pre>
</div>
) : null}
</ToolFallbackContent>
</ToolFallbackRoot>