-
+
+
{!isRunning && (
-
- {isSuccess ? (
-
- ) : (
-
- )}
-
- {isSuccess
- ? `${operation} completed successfully`
- : `${operation} failed`}
-
-
+
+
+ {operation}
+
)}
-
- {isRunning && (
-
-
- Executing browser action...
-
+ {url && (
+
+ {url}
+
)}
-
-
- {toolTimestamp && !isRunning
- ? formatTimestamp(toolTimestamp)
- : assistantTimestamp
- ? formatTimestamp(assistantTimestamp)
- : ''}
-
+
+
+
+ {toolTimestamp && !isRunning
+ ? formatTimestamp(toolTimestamp)
+ : assistantTimestamp
+ ? formatTimestamp(assistantTimestamp)
+ : ''}
-
+
);
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/CommandToolView.tsx b/frontend/src/components/thread/tool-views/CommandToolView.tsx
index 23f9a021..4f2b8617 100644
--- a/frontend/src/components/thread/tool-views/CommandToolView.tsx
+++ b/frontend/src/components/thread/tool-views/CommandToolView.tsx
@@ -1,19 +1,30 @@
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import {
Terminal,
CheckCircle,
AlertTriangle,
CircleDashed,
+ ExternalLink,
+ Code,
+ Clock,
+ ChevronDown,
+ ChevronUp,
+ Loader2,
+ ArrowRight
} from 'lucide-react';
import { ToolViewProps } from './types';
import {
- extractCommand,
- extractCommandOutput,
extractExitCode,
formatTimestamp,
getToolTitle,
} from './utils';
import { cn } from '@/lib/utils';
+import { useTheme } from 'next-themes';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Progress } from '@/components/ui/progress';
+import { ScrollArea } from "@/components/ui/scroll-area";
export function CommandToolView({
name = 'execute-command',
@@ -24,15 +35,17 @@ export function CommandToolView({
isSuccess = true,
isStreaming = false,
}: ToolViewProps) {
- // Extract command with improved XML parsing
+ const { resolvedTheme } = useTheme();
+ const isDarkTheme = resolvedTheme === 'dark';
+ const [progress, setProgress] = useState(0);
+ const [showFullOutput, setShowFullOutput] = useState(false);
+
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(
/
]*>([\s\S]*?)<\/execute-command>/,
);
@@ -41,7 +54,6 @@ export function CommandToolView({
}
}
} catch (e) {
- // If JSON parsing fails, try direct XML extraction
const commandMatch = assistantContent.match(
/]*>([\s\S]*?)<\/execute-command>/,
);
@@ -53,198 +65,286 @@ export function CommandToolView({
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
+ ?.replace(/^suna@computer:~\$\s*/g, '')
+ ?.replace(/\\n/g, '')
+ ?.replace(/\n/g, '')
+ ?.trim();
- // Extract and clean the output with improved parsing
const output = React.useMemo(() => {
if (!toolContent) return null;
+ let extractedOutput = '';
+ let success = true;
+
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(
- /\s*([\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;
+ if (typeof toolContent === 'string') {
+ if (toolContent.includes('ToolResult')) {
+ const successMatch = toolContent.match(/success=(true|false)/i);
+ success = successMatch ? successMatch[1].toLowerCase() === 'true' : true;
+
+ //@ts-expect-error IGNORE
+ const outputMatch = toolContent.match(/output=['"](.*)['"]/s);
+ if (outputMatch && outputMatch[1]) {
+ extractedOutput = outputMatch[1]
+ .replace(/\\n/g, '\n')
+ .replace(/\\"/g, '"')
+ .replace(/\\t/g, '\t')
+ .replace(/\\'/g, "'");
+ } else {
+ extractedOutput = toolContent;
+ }
+ } else {
+ try {
+ const parsed = JSON.parse(toolContent);
+ if (parsed.output) {
+ extractedOutput = parsed.output;
+ success = parsed.success !== false;
+ } else if (parsed.content) {
+ extractedOutput = parsed.content;
+ } else {
+ extractedOutput = JSON.stringify(parsed, null, 2);
+ }
+ } catch (e) {
+ extractedOutput = toolContent;
}
- } catch (e) {
- // If JSON parsing fails, use the content as-is
- return parsed.content;
}
+ } else {
+ extractedOutput = String(toolContent);
}
} catch (e) {
- // If JSON parsing fails, try direct XML extraction
- const toolResultMatch = toolContent.match(
- /\s*([\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, '"');
- }
+ extractedOutput = String(toolContent);
+ console.error('Error parsing tool content:', e);
}
- return toolContent;
+ return extractedOutput;
}, [toolContent]);
- const exitCode = extractExitCode(toolContent);
+ const exitCode = extractExitCode(output);
const toolTitle = getToolTitle(name);
+
+ // Simulate progress when streaming
+ useEffect(() => {
+ if (isStreaming) {
+ const timer = setInterval(() => {
+ setProgress((prevProgress) => {
+ if (prevProgress >= 95) {
+ clearInterval(timer);
+ return prevProgress;
+ }
+ return prevProgress + 5;
+ });
+ }, 300);
+ return () => clearInterval(timer);
+ } else {
+ setProgress(100);
+ }
+ }, [isStreaming]);
+
+ // Format and handle the output for display
+ const formattedOutput = React.useMemo(() => {
+ if (!output) return [];
+
+ // Replace JSON string escaped newlines with actual newlines
+ let processedOutput = output
+ .replace(/\\n/g, '\n')
+ .replace(/\\t/g, '\t')
+ .replace(/\\"/g, '"')
+ .replace(/\\'/g, "'");
+
+ // Split by real newlines for line-by-line display
+ return processedOutput.split('\n');
+ }, [output]);
+
+ // Only show a preview if there are many lines
+ const hasMoreLines = formattedOutput.length > 10;
+ const previewLines = formattedOutput.slice(0, 10);
+ const linesToShow = showFullOutput ? formattedOutput : previewLines;
return (
-
-
-
-
-
-
-
- Terminal
-
+
+
+
+
+
+
- {exitCode !== null && !isStreaming && (
-
-
- Exit: {exitCode}
-
- )}
-
-
-
-
- {command && output && !isStreaming && (
-
-
-
- suna@computer:~$
-
- {command}
-
-
-
- {output}
-
-
- {isSuccess && (
-
- suna@computer:~$ _
-
- )}
-
- )}
-
- {command && !output && !isStreaming && (
-
-
-
- suna@computer:~$
-
- {command}
-
-
-
- )}
-
- {!command && !output && !isStreaming && (
-
-
- suna@computer:~$
-
-
-
- )}
-
- {isStreaming && (
-
-
-
- suna@computer:~$
-
-
- {command || 'running command...'}
-
-
-
-
- Command execution in progress...
-
-
- )}
+
+
+ {toolTitle}
+
-
-
-
- {/* Footer */}
-
-
+
{!isStreaming && (
-
+
{isSuccess ? (
-
+
) : (
-
+
)}
-
- {isSuccess
- ? `Command completed successfully${exitCode !== null ? ` (exit code: ${exitCode})` : ''}`
- : `Command failed${exitCode !== null ? ` with exit code ${exitCode}` : ''}`}
-
-
+ {isSuccess ? 'Command executed successfully' : 'Command failed'}
+
)}
+
+
- {isStreaming && (
-
-
-
Executing command...
+
+ {isStreaming ? (
+
+
+
+
+
+
+ Executing command
+
+
+ {command || 'Processing command...'}
+
+
+
{progress}%
- )}
-
-
- {toolTimestamp && !isStreaming
- ? formatTimestamp(toolTimestamp)
- : assistantTimestamp
- ? formatTimestamp(assistantTimestamp)
- : ''}
+ ) : command ? (
+
+
+
+
+
+ Command
+
+
+ $
+ {command}
+
+
+
+ {output && (
+
+
+
+
+ Output
+ {exitCode !== null && (
+
+ Exit code: {exitCode}
+
+ )}
+
+ {hasMoreLines && (
+
setShowFullOutput(!showFullOutput)}
+ className="h-7 text-xs flex items-center gap-1"
+ >
+ {showFullOutput ? (
+ <>
+
+ Show less
+ >
+ ) : (
+ <>
+
+ Show full output
+ >
+ )}
+
+ )}
+
+
+
+
+
Terminal output
+ {exitCode !== null && exitCode !== 0 && (
+
+
+ Error
+
+ )}
+
+
+
+ {linesToShow.map((line, index) => (
+
+ {line || ' '}
+
+ ))}
+ {!showFullOutput && hasMoreLines && (
+
+ + {formattedOutput.length - 10} more lines
+
+ )}
+
+
+
+
+ )}
+
+ {!output && !isStreaming && (
+
+ )}
+
+
+ ) : (
+
+
+
+
+
+ No Command Found
+
+
+ No command was detected. Please provide a valid command to execute.
+
+
+ )}
+
+
+
+
+ {!isStreaming && command && (
+
+
+ Command
+
+ )}
+
+
+
+
+ {toolTimestamp && !isStreaming
+ ? formatTimestamp(toolTimestamp)
+ : assistantTimestamp
+ ? formatTimestamp(assistantTimestamp)
+ : ''}
-
+
);
}
diff --git a/frontend/src/components/thread/tool-views/ExposePortToolView.tsx b/frontend/src/components/thread/tool-views/ExposePortToolView.tsx
index f7633814..ede5301d 100644
--- a/frontend/src/components/thread/tool-views/ExposePortToolView.tsx
+++ b/frontend/src/components/thread/tool-views/ExposePortToolView.tsx
@@ -1,9 +1,22 @@
-import React from 'react';
+import React, { useMemo, useState, useEffect } from 'react';
+import {
+ ExternalLink,
+ CheckCircle,
+ AlertTriangle,
+ Globe,
+ Loader2,
+ Link2,
+ Computer
+} from 'lucide-react';
import { ToolViewProps } from './types';
import { formatTimestamp } from './utils';
-import { ExternalLink, CheckCircle, AlertTriangle } from 'lucide-react';
-import { Markdown } from '@/components/ui/markdown';
import { cn } from '@/lib/utils';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { Button } from '@/components/ui/button';
export function ExposePortToolView({
name = 'expose-port',
@@ -14,18 +27,10 @@ export function ExposePortToolView({
assistantTimestamp,
toolTimestamp,
}: ToolViewProps) {
- console.log('ExposePortToolView:', {
- name,
- assistantContent,
- toolContent,
- isSuccess,
- isStreaming,
- assistantTimestamp,
- toolTimestamp,
- });
+ const [progress, setProgress] = useState(0);
// Parse the assistant content
- const parsedAssistantContent = React.useMemo(() => {
+ const parsedAssistantContent = useMemo(() => {
if (!assistantContent) return null;
try {
const parsed = JSON.parse(assistantContent);
@@ -37,7 +42,7 @@ export function ExposePortToolView({
}, [assistantContent]);
// Parse the tool result
- const toolResult = React.useMemo(() => {
+ const toolResult = useMemo(() => {
if (!toolContent) return null;
try {
// First parse the outer JSON
@@ -56,7 +61,7 @@ export function ExposePortToolView({
}, [toolContent]);
// Extract port number from assistant content
- const portNumber = React.useMemo(() => {
+ const portNumber = useMemo(() => {
if (!parsedAssistantContent) return null;
try {
const match = parsedAssistantContent.match(
@@ -69,141 +74,219 @@ export function ExposePortToolView({
}
}, [parsedAssistantContent]);
- // If we have no content to show, render a placeholder
- if (!portNumber && !toolResult && !isStreaming) {
- return (
-
-
- No port exposure information available
-
-
- );
- }
+ // Simulate progress when streaming
+ useEffect(() => {
+ if (isStreaming) {
+ const timer = setInterval(() => {
+ setProgress((prevProgress) => {
+ if (prevProgress >= 95) {
+ clearInterval(timer);
+ return prevProgress;
+ }
+ return prevProgress + 5;
+ });
+ }, 300);
+ return () => clearInterval(timer);
+ } else {
+ setProgress(100);
+ }
+ }, [isStreaming]);
return (
-
-
- {/* Assistant Content */}
- {portNumber && !isStreaming && (
-
-
-
- Port to Expose
-
- {assistantTimestamp && (
-
- {formatTimestamp(assistantTimestamp)}
-
- )}
+
+
+
+
+
+
-
-
-
- Port
-
-
- {portNumber}
-
-
+
+
+ Port Exposure
+
- )}
-
- {/* Tool Result */}
- {toolResult && (
-
-
-
- {isStreaming ? 'Processing' : 'Exposed URL'}
-
- {toolTimestamp && !isStreaming && (
-
- {formatTimestamp(toolTimestamp)}
-
- )}
-
-
- {isStreaming ? (
-
- Exposing port {portNumber}...
-
- ) : (
-
-
-
-
- Port
-
-
- {toolResult.port}
-
-
-
- {toolResult.message}
-
-
- Note: This URL might only be temporarily available and could
- expire after some time.
-
-
- )}
-
-
- )}
-
-
- {/* Footer */}
-
-
+
{!isStreaming && (
-
+
{isSuccess ? (
-
+
) : (
-
+
)}
-
- {isSuccess
- ? 'Port exposed successfully'
- : 'Failed to expose port'}
-
-
+ {isSuccess ? 'Port exposed successfully' : 'Failed to expose port'}
+
)}
+
+
- {isStreaming && (
-
-
Exposing port...
+
+ {isStreaming ? (
+
+
+
+
+
+
+ Exposing port
+
+
+
+ {portNumber}
+
+
+
+
{progress}%
- )}
-
-
- {toolTimestamp && !isStreaming
- ? formatTimestamp(toolTimestamp)
- : assistantTimestamp
- ? formatTimestamp(assistantTimestamp)
- : ''}
+ ) : (
+
+
+ {/* Port Information Section */}
+ {portNumber && (
+
+
+
+ Port to Expose
+
+
+
Port Number
+
+ {portNumber}
+
+
+
+ {assistantTimestamp && formatTimestamp(assistantTimestamp)}
+
+
+ )}
+
+ {/* Exposed URL Section */}
+ {toolResult && (
+
+
+
+
+
+
+
+ Port Details
+
+
+
+ Port: {toolResult.port}
+
+
+
+
+
+ {toolResult.message}
+
+
+
+
+
This URL might only be temporarily available and could expire after some time.
+
+
+
+
+
+
+
+
+
+
+
+ Open in Browser
+
+
+
+
+ Open the exposed URL in a new tab
+
+
+
+
+
+ {toolTimestamp && formatTimestamp(toolTimestamp)}
+
+
+
+ )}
+
+ {/* Empty State */}
+ {!portNumber && !toolResult && !isStreaming && (
+
+
+
+
+
+ No Port Information
+
+
+ No port exposure information is available yet. Use the expose-port command to share a local port.
+
+
+ )}
+
+
+ )}
+
+
+
+
+ {!isStreaming && toolResult && (
+
+
+ Port {portNumber}
+
+ )}
+
+
+
+ {isSuccess ? (
+
+
+ {isStreaming ? 'Exposing...' : 'Port exposed successfully'}
+
+ ) : (
+
+
+ Failed to expose port
+
+ )}
-
+
);
}
diff --git a/frontend/src/components/thread/tool-views/FileOperationToolView.tsx b/frontend/src/components/thread/tool-views/FileOperationToolView.tsx
index ced222ca..5e51ca82 100644
--- a/frontend/src/components/thread/tool-views/FileOperationToolView.tsx
+++ b/frontend/src/components/thread/tool-views/FileOperationToolView.tsx
@@ -432,39 +432,37 @@ export function FileOperationToolView({
) : (
<>
-
-
-
-
+
+
+
+
+
+
+
+
+ {processedFilePath || 'Unknown file path'}
+
+
-
-
- {operation === 'create' ? 'Create File' :
- operation === 'rewrite' ? 'Update File' : 'Delete File'}
-
-
- {processedFilePath || 'Unknown file path'}
-
+
+
+ {!isStreaming ? (
+
+ {isSuccess ? (
+
+ ) : (
+
+ )}
+ {isSuccess ? config.successMessage : `Failed to ${operation}`}
+
+ ) : (
+
+
+ {config.progressMessage}
+
+ )}
-
-
- {!isStreaming ? (
-
- {isSuccess ? (
-
- ) : (
-
- )}
- {isSuccess ? config.successMessage : `Failed to ${operation}`}
-
- ) : (
-
-
- {config.progressMessage}
-
- )}
-
diff --git a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx
index a6d3f61f..58c41f12 100644
--- a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx
+++ b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx
@@ -16,12 +16,21 @@ export type ToolViewComponent = React.ComponentType;
type ToolViewRegistryType = Record;
const defaultRegistry: ToolViewRegistryType = {
- 'browser-navigate': BrowserToolView,
- 'browser-click': BrowserToolView,
- 'browser-extract': BrowserToolView,
- 'browser-fill': BrowserToolView,
+ 'browser-navigate-to': BrowserToolView,
+ 'browser-go-back': BrowserToolView,
'browser-wait': BrowserToolView,
- 'browser-screenshot': BrowserToolView,
+ 'browser-click-element': BrowserToolView,
+ 'browser-input-text': BrowserToolView,
+ 'browser-send-keys': BrowserToolView,
+ 'browser-switch-tab': BrowserToolView,
+ 'browser-close-tab': BrowserToolView,
+ 'browser-scroll-down': BrowserToolView,
+ 'browser-scroll-up': BrowserToolView,
+ 'browser-scroll-to-text': BrowserToolView,
+ 'browser-get-dropdown-options': BrowserToolView,
+ 'browser-select-dropdown-option': BrowserToolView,
+ 'browser-drag-drop': BrowserToolView,
+ 'browser-click-coordinates': BrowserToolView,
'execute-command': CommandToolView,