Spaces:
Running
Running
| import React from "react"; | |
| import { Terminal, CheckCircle, AlertTriangle, CircleDashed } from "lucide-react"; | |
| import { ToolViewProps } from "./types"; | |
| import { extractCommand, extractCommandOutput, extractExitCode, formatTimestamp, getToolTitle } from "./utils"; | |
| import { cn } from "@/lib/utils"; | |
| export function CommandToolView({ | |
| name = "execute-command", | |
| assistantContent, | |
| toolContent, | |
| assistantTimestamp, | |
| toolTimestamp, | |
| isSuccess = true, | |
| isStreaming = false | |
| }: ToolViewProps) { | |
| // Extract command with improved XML parsing | |
| const rawCommand = React.useMemo(() => { | |
| if (!assistantContent) return null; | |
| try { | |
| // Try to parse JSON content first | |
| const parsed = JSON.parse(assistantContent); | |
| if (parsed.content) { | |
| // Look for execute-command tag | |
| const commandMatch = parsed.content.match(/<execute-command[^>]*>([\s\S]*?)<\/execute-command>/); | |
| if (commandMatch) { | |
| return commandMatch[1].trim(); | |
| } | |
| } | |
| } catch (e) { | |
| // If JSON parsing fails, try direct XML extraction | |
| const commandMatch = assistantContent.match(/<execute-command[^>]*>([\s\S]*?)<\/execute-command>/); | |
| if (commandMatch) { | |
| return commandMatch[1].trim(); | |
| } | |
| } | |
| return null; | |
| }, [assistantContent]); | |
| // Clean the command by removing any leading/trailing whitespace and newlines | |
| const command = rawCommand | |
| ?.replace(/^suna@computer:~\$\s*/g, '') // Remove prompt prefix | |
| ?.replace(/\\n/g, '') // Remove escaped newlines | |
| ?.replace(/\n/g, '') // Remove actual newlines | |
| ?.trim(); // Clean up any remaining whitespace | |
| // Extract and clean the output with improved parsing | |
| const output = React.useMemo(() => { | |
| if (!toolContent) return null; | |
| try { | |
| // Try to parse JSON content first | |
| const parsed = JSON.parse(toolContent); | |
| if (parsed.content) { | |
| // Look for tool_result tag | |
| const toolResultMatch = parsed.content.match(/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/); | |
| if (toolResultMatch) { | |
| return toolResultMatch[1].trim(); | |
| } | |
| // Look for output field in a ToolResult pattern | |
| const outputMatch = parsed.content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/); | |
| if (outputMatch) { | |
| return outputMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"'); | |
| } | |
| // Try to parse as direct JSON | |
| try { | |
| const outputJson = JSON.parse(parsed.content); | |
| if (outputJson.output) { | |
| return outputJson.output; | |
| } | |
| } catch (e) { | |
| // If JSON parsing fails, use the content as-is | |
| return parsed.content; | |
| } | |
| } | |
| } catch (e) { | |
| // If JSON parsing fails, try direct XML extraction | |
| const toolResultMatch = toolContent.match(/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/); | |
| if (toolResultMatch) { | |
| return toolResultMatch[1].trim(); | |
| } | |
| const outputMatch = toolContent.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/); | |
| if (outputMatch) { | |
| return outputMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"'); | |
| } | |
| } | |
| return toolContent; | |
| }, [toolContent]); | |
| const exitCode = extractExitCode(toolContent); | |
| const toolTitle = getToolTitle(name); | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex-1 p-4 overflow-auto"> | |
| <div className="border border-zinc-200 dark:border-zinc-800 rounded-md overflow-hidden h-full flex flex-col"> | |
| <div className="flex items-center p-2 bg-zinc-100 dark:bg-zinc-900 justify-between border-b border-zinc-200 dark:border-zinc-800"> | |
| <div className="flex items-center"> | |
| <Terminal className="h-4 w-4 mr-2 text-zinc-600 dark:text-zinc-400" /> | |
| <span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">Terminal</span> | |
| </div> | |
| {exitCode !== null && !isStreaming && ( | |
| <span className={cn( | |
| "text-xs flex items-center", | |
| isSuccess ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400" | |
| )}> | |
| <span className="h-1.5 w-1.5 rounded-full mr-1.5 bg-current"></span> | |
| Exit: {exitCode} | |
| </span> | |
| )} | |
| </div> | |
| <div className="terminal-container flex-1 overflow-auto bg-black text-zinc-300 font-mono"> | |
| <div className="p-3 text-xs"> | |
| {command && output && !isStreaming && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-start"> | |
| <span className="text-emerald-400 shrink-0 mr-2">suna@computer:~$</span> | |
| <span className="text-zinc-300">{command}</span> | |
| </div> | |
| <div className="whitespace-pre-wrap break-words text-zinc-400 pl-0"> | |
| {output} | |
| </div> | |
| {isSuccess && <div className="text-emerald-400 mt-1">suna@computer:~$ _</div>} | |
| </div> | |
| )} | |
| {command && !output && !isStreaming && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-start"> | |
| <span className="text-emerald-400 shrink-0 mr-2">suna@computer:~$</span> | |
| <span className="text-zinc-300">{command}</span> | |
| </div> | |
| <div className="flex items-center h-4"> | |
| <div className="w-2 h-4 bg-zinc-500 animate-pulse"></div> | |
| </div> | |
| </div> | |
| )} | |
| {!command && !output && !isStreaming && ( | |
| <div className="flex items-start"> | |
| <span className="text-emerald-400 shrink-0 mr-2">suna@computer:~$</span> | |
| <span className="w-2 h-4 bg-zinc-500 animate-pulse"></span> | |
| </div> | |
| )} | |
| {isStreaming && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-start"> | |
| <span className="text-emerald-400 shrink-0 mr-2">suna@computer:~$</span> | |
| <span className="text-zinc-300">{command || 'running command...'}</span> | |
| </div> | |
| <div className="flex items-center gap-2 text-zinc-400"> | |
| <CircleDashed className="h-3 w-3 animate-spin text-blue-400" /> | |
| <span>Command execution in progress...</span> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Footer */} | |
| <div className="p-4 border-t border-zinc-200 dark:border-zinc-800"> | |
| <div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400"> | |
| {!isStreaming && ( | |
| <div className="flex items-center gap-2"> | |
| {isSuccess ? ( | |
| <CheckCircle className="h-3.5 w-3.5 text-emerald-500" /> | |
| ) : ( | |
| <AlertTriangle className="h-3.5 w-3.5 text-red-500" /> | |
| )} | |
| <span> | |
| {isSuccess | |
| ? `Command completed successfully${exitCode !== null ? ` (exit code: ${exitCode})` : ''}` | |
| : `Command failed${exitCode !== null ? ` with exit code ${exitCode}` : ''}`} | |
| </span> | |
| </div> | |
| )} | |
| {isStreaming && ( | |
| <div className="flex items-center gap-2"> | |
| <CircleDashed className="h-3.5 w-3.5 text-blue-500 animate-spin" /> | |
| <span>Executing command...</span> | |
| </div> | |
| )} | |
| <div className="text-xs"> | |
| {toolTimestamp && !isStreaming | |
| ? formatTimestamp(toolTimestamp) | |
| : assistantTimestamp | |
| ? formatTimestamp(assistantTimestamp) | |
| : ''} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |