Truncate long code execution tool output (#5708)
This commit is contained in:
parent
e9cf735f1b
commit
df2d31fea8
2 changed files with 120 additions and 42 deletions
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue