File size: 8,225 Bytes
67c7241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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>
  );
}