"use client"; import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { AlertCircleIcon, CheckCircle2Icon, ChevronRightIcon, DownloadIcon, FileIcon, Loader2Icon, TerminalIcon, XCircleIcon, } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getBearerToken } from "@/lib/auth-utils"; import { BACKEND_URL } from "@/lib/env-config"; import { cn } from "@/lib/utils"; // ============================================================================ // Zod Schemas // ============================================================================ const ExecuteArgsSchema = z.object({ command: z.string(), timeout: z.number().nullish(), }); const ExecuteResultSchema = z.object({ result: z.string().nullish(), exit_code: z.number().nullish(), output: z.string().nullish(), error: z.string().nullish(), status: z.string().nullish(), thread_id: z.string().nullish(), }); // ============================================================================ // Types // ============================================================================ type ExecuteArgs = z.infer; type ExecuteResult = z.infer; interface SandboxFile { path: string; name: string; } interface ParsedOutput { exitCode: number | null; output: string; displayOutput: string; truncated: boolean; isError: boolean; files: SandboxFile[]; } // ============================================================================ // Helpers // ============================================================================ const SANDBOX_FILE_RE = /^SANDBOX_FILE:\s*(.+)$/gm; function extractSandboxFiles(text: string): SandboxFile[] { const files: SandboxFile[] = []; let match: RegExpExecArray | null; while ((match = SANDBOX_FILE_RE.exec(text)) !== null) { const filePath = match[1].trim(); if (filePath) { const name = filePath.includes("/") ? filePath.split("/").pop() || filePath : filePath; files.push({ path: filePath, name }); } } SANDBOX_FILE_RE.lastIndex = 0; return files; } function stripSandboxFileLines(text: string): string { return text .replace(/^SANDBOX_FILE:\s*.+$/gm, "") .replace(/\n{3,}/g, "\n\n") .trim(); } function parseExecuteResult(result: ExecuteResult): ParsedOutput { const raw = result.result || result.output || ""; if (result.error) { return { exitCode: null, output: result.error, displayOutput: result.error, truncated: false, isError: true, files: [], }; } if (result.exit_code !== undefined && result.exit_code !== null) { const files = extractSandboxFiles(raw); const displayOutput = stripSandboxFileLines(raw); return { exitCode: result.exit_code, output: raw, displayOutput, truncated: raw.includes("[Output was truncated"), isError: result.exit_code !== 0, files, }; } const exitMatch = raw.match(/^Exit code:\s*(\d+)/); if (exitMatch) { const exitCode = parseInt(exitMatch[1], 10); const outputMatch = raw.match(/\nOutput:\n([\s\S]*)/); const output = outputMatch ? outputMatch[1] : ""; const files = extractSandboxFiles(output); const displayOutput = stripSandboxFileLines(output); return { exitCode, output, displayOutput, truncated: raw.includes("[Output was truncated"), isError: exitCode !== 0, files, }; } if (raw.startsWith("Error:")) { return { exitCode: null, output: raw, displayOutput: raw, truncated: false, isError: true, files: [], }; } const files = extractSandboxFiles(raw); const displayOutput = stripSandboxFileLines(raw); return { exitCode: null, output: raw, displayOutput, truncated: false, isError: false, files }; } function truncateCommand(command: string, maxLen = 80): string { if (command.length <= maxLen) return command; return command.slice(0, maxLen) + "…"; } // ============================================================================ // Download helper // ============================================================================ async function downloadSandboxFile(threadId: string, filePath: string, fileName: string) { const token = getBearerToken(); const url = `${BACKEND_URL}/api/v1/threads/${threadId}/sandbox/download?path=${encodeURIComponent(filePath)}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token || ""}` }, }); if (!res.ok) { throw new Error(`Download failed: ${res.statusText}`); } const blob = await res.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(blobUrl); } // ============================================================================ // Sub-Components // ============================================================================ function ExecuteLoading({ command }: { command: string }) { return (
{truncateCommand(command)}
); } function ExecuteErrorState({ command, error }: { command: string; error: string }) { return (

Execution failed

$ {command}

{error}

); } function ExecuteCancelledState({ command }: { command: string }) { return (

$ {command}

); } function SandboxFileDownload({ file, threadId }: { file: SandboxFile; threadId: string }) { const [downloading, setDownloading] = useState(false); const [error, setError] = useState(null); const handleDownload = useCallback(async () => { setDownloading(true); setError(null); try { await downloadSandboxFile(threadId, file.path, file.name); } catch (e) { setError(e instanceof Error ? e.message : "Download failed"); } finally { setDownloading(false); } }, [threadId, file.path, file.name]); return ( ); } function ExecuteCompleted({ command, parsed, threadId, }: { command: string; parsed: ParsedOutput; threadId: string | null; }) { const [open, setOpen] = useState(false); const isLongCommand = command.length > 80 || command.includes("\n"); const hasTextContent = parsed.displayOutput.trim().length > 0 || isLongCommand; const hasFiles = parsed.files.length > 0 && !!threadId; const hasContent = hasTextContent || hasFiles; const exitBadge = useMemo(() => { if (parsed.exitCode === null) return null; const success = parsed.exitCode === 0; return ( {success ? : } {parsed.exitCode} ); }, [parsed.exitCode]); return (
{truncateCommand(command)} {hasFiles && !open && ( {parsed.files.length} )} {exitBadge}
{isLongCommand && (

Command

									{command}
								
)} {parsed.displayOutput.trim().length > 0 && (
{(isLongCommand || hasFiles) && (

Output

)}
									{parsed.displayOutput}
								
)} {parsed.truncated && (

Output was truncated due to size limits

)} {hasFiles && threadId && (

Files

{parsed.files.map((file) => ( ))}
)}
); } // ============================================================================ // Tool UI // ============================================================================ export const SandboxExecuteToolUI = ({ args, result, status }: ToolCallMessagePartProps) => { const command = args.command || "…"; if (status.type === "running" || status.type === "requires-action") { return ; } if (status.type === "incomplete") { if (status.reason === "cancelled") { return ; } if (status.reason === "error") { return ( ); } } if (!result) { return ; } if (result.error && !result.result && !result.output) { return ; } const parsed = parseExecuteResult(result); const threadId = result.thread_id || null; return ; }; export { ExecuteArgsSchema, ExecuteResultSchema, type ExecuteArgs, type ExecuteResult };