chore(ui): tool-panel ui revamp

This commit is contained in:
Soumyadas15 2025-05-19 22:00:33 +05:30
parent 6fc03aba04
commit 67604ea95a
6 changed files with 695 additions and 437 deletions

View File

@ -6,6 +6,9 @@ import {
CheckCircle, CheckCircle,
AlertTriangle, AlertTriangle,
CircleDashed, CircleDashed,
ChevronDown,
ChevronUp,
Loader2
} from 'lucide-react'; } from 'lucide-react';
import { ToolViewProps } from './types'; import { ToolViewProps } from './types';
import { import {
@ -17,6 +20,12 @@ import {
import { ApiMessageType } from '@/components/thread/types'; import { ApiMessageType } from '@/components/thread/types';
import { safeJsonParse } from '@/components/thread/utils'; import { safeJsonParse } from '@/components/thread/utils';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
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";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
export function BrowserToolView({ export function BrowserToolView({
name = 'browser-operation', name = 'browser-operation',
@ -84,7 +93,6 @@ export function BrowserToolView({
browserStateMessage.content, browserStateMessage.content,
{}, {},
); );
console.log('Browser state content: ', browserStateContent)
screenshotBase64 = browserStateContent?.screenshot_base64 || null; screenshotBase64 = browserStateContent?.screenshot_base64 || null;
} }
} }
@ -110,38 +118,95 @@ export function BrowserToolView({
<iframe <iframe
src={vncPreviewUrl} src={vncPreviewUrl}
title="Browser preview" title="Browser preview"
className="w-full h-full border-0 flex-1" className="w-full h-full border-0 min-h-[600px]"
style={{ width: '100%', height: '100%', minHeight: '600px' }}
/> />
); );
}, [vncPreviewUrl]); // Only recreate if the URL changes }, [vncPreviewUrl]); // Only recreate if the URL changes
// Progress for loading state - only for visual feedback
const [progress, setProgress] = React.useState(100);
// Simulate progress when running
React.useEffect(() => {
if (isRunning) {
setProgress(0);
const timer = setInterval(() => {
setProgress((prevProgress) => {
if (prevProgress >= 95) {
clearInterval(timer);
return prevProgress;
}
return prevProgress + 2;
});
}, 500);
return () => clearInterval(timer);
} else {
setProgress(100);
}
}, [isRunning]);
return ( return (
<div className="flex flex-col h-full"> <Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-white dark:bg-zinc-950">
<div className="flex-1 p-4 overflow-auto"> <CardHeader className="h-13 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
<div className="border border-zinc-200 dark:border-zinc-800 rounded-md overflow-hidden h-full flex flex-col"> <div className="flex flex-row items-center justify-between">
<div className="bg-zinc-100 dark:bg-zinc-900 p-2 flex items-center justify-between border-b border-zinc-200 dark:border-zinc-800"> <div className="flex items-center gap-2">
<div className="flex items-center"> <div className="p-2 rounded-lg bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20">
<MonitorPlay className="h-4 w-4 mr-2 text-zinc-600 dark:text-zinc-400" /> <MonitorPlay className="h-5 w-5 text-purple-500 dark:text-purple-400" />
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">
Browser Window
</span>
</div> </div>
{url && ( <div>
<div className="text-xs font-mono text-zinc-500 dark:text-zinc-400 truncate max-w-[340px]"> <CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{url} {toolTitle}
</CardTitle>
</div> </div>
)}
</div> </div>
{/* Preview Logic */} {!isRunning && (
<div className="flex-1 flex items-stretch bg-black"> <Badge
variant="secondary"
className={
isSuccess
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
}
>
{isSuccess ? (
<CheckCircle className="h-3.5 w-3.5 mr-1" />
) : (
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
)}
{isSuccess ? 'Browser action completed' : 'Browser action failed'}
</Badge>
)}
{isRunning && (
<Badge className="bg-gradient-to-b from-blue-200 to-blue-100 text-blue-700 dark:from-blue-800/50 dark:to-blue-900/60 dark:text-blue-300">
<CircleDashed className="h-3.5 w-3.5 mr-1 animate-spin" />
Executing browser action
</Badge>
)}
</div>
</CardHeader>
<CardContent className="p-0 flex-1 overflow-hidden relative" style={{ height: 'calc(100vh - 150px)', minHeight: '600px' }}>
{/* Browser View Logic - Preserving the core functionality */}
<div className="flex-1 flex h-full items-stretch bg-black">
{isLastToolCall ? ( {isLastToolCall ? (
// Only show live sandbox or fallback to sandbox for the last tool call // Only show live sandbox or fallback to screenshot for the last tool call
isRunning && vncIframe ? ( isRunning && vncIframe ? (
// Use the memoized iframe for live preview <div className="flex flex-col items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
vncIframe <div className="relative w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
{vncIframe}
<div className="absolute top-4 right-4 z-10">
<Badge className="bg-blue-500/90 text-white border-none shadow-lg animate-pulse">
<CircleDashed className="h-3 w-3 mr-1.5 animate-spin" />
{operation} in progress
</Badge>
</div>
</div>
</div>
) : screenshotBase64 ? ( ) : screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto"> <div className="flex items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
<img <img
src={`data:image/jpeg;base64,${screenshotBase64}`} src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot" alt="Browser Screenshot"
@ -150,26 +215,35 @@ export function BrowserToolView({
</div> </div>
) : vncIframe ? ( ) : vncIframe ? (
// Use the memoized iframe // Use the memoized iframe
vncIframe <div className="flex flex-col items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
{vncIframe}
</div>
) : ( ) : (
<div className="p-8 flex flex-col items-center justify-center w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-400"> <div className="p-8 flex flex-col items-center justify-center w-full bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900 text-zinc-700 dark:text-zinc-400">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" /> <div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60">
<p className="text-sm font-medium"> <MonitorPlay className="h-10 w-10 text-purple-400 dark:text-purple-600" />
</div>
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
Browser preview not available Browser preview not available
</p> </h3>
{url && ( {url && (
<a <div className="mt-4">
href={url} <Button
target="_blank" variant="outline"
rel="noopener noreferrer" size="sm"
className="mt-3 flex items-center text-blue-600 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 hover:underline" className="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-700 shadow-sm hover:shadow-md transition-shadow"
asChild
> >
Visit URL <ExternalLink className="h-3 w-3 ml-1" /> <a href={url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3.5 w-3.5 mr-2" />
Visit URL
</a> </a>
</Button>
</div>
)} )}
</div> </div>
) )
) : // For non-last tool calls, only show screenshot if available, otherwise show "No Browser State image found" ) : // For non-last tool calls, only show screenshot if available, otherwise show "No Browser State"
screenshotBase64 ? ( screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto"> <div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
<img <img
@ -179,43 +253,38 @@ export function BrowserToolView({
/> />
</div> </div>
) : ( ) : (
<div className="p-8 flex flex-col items-center justify-center w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-400"> <div className="p-8 h-full flex flex-col items-center justify-center w-full bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900 text-zinc-700 dark:text-zinc-400">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" /> <div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-zinc-100 to-zinc-50 shadow-inner dark:from-zinc-800/40 dark:to-zinc-900/60">
<p className="text-sm font-medium"> <MonitorPlay className="h-10 w-10 text-zinc-400 dark:text-zinc-600" />
No Browser State image found </div>
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
No Browser State Available
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
Browser state image not found for this action
</p> </p>
</div> </div>
)} )}
</div> </div>
</div> </CardContent>
</div>
{/* Footer */} {/* Footer */}
<div className="p-4 border-t border-zinc-200 dark:border-zinc-800"> <div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400"> <div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
{!isRunning && ( {!isRunning && (
<div className="flex items-center gap-2"> <Badge className="h-6 py-0.5">
{isSuccess ? ( <Globe className="h-3 w-3" />
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" /> {operation}
) : ( </Badge>
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
)} )}
<span> {url && (
{isSuccess <span className="text-xs truncate max-w-[200px] hidden sm:inline-block">
? `${operation} completed successfully` {url}
: `${operation} failed`}
</span> </span>
</div>
)} )}
{isRunning && (
<div className="flex items-center gap-2">
<CircleDashed className="h-3.5 w-3.5 text-blue-500 animate-spin" />
<span>Executing browser action...</span>
</div> </div>
)}
<div className="text-xs"> <div className="text-xs text-zinc-500 dark:text-zinc-400">
{toolTimestamp && !isRunning {toolTimestamp && !isRunning
? formatTimestamp(toolTimestamp) ? formatTimestamp(toolTimestamp)
: assistantTimestamp : assistantTimestamp
@ -223,7 +292,6 @@ export function BrowserToolView({
: ''} : ''}
</div> </div>
</div> </div>
</div> </Card>
</div>
); );
} }

View File

@ -1,19 +1,30 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { import {
Terminal, Terminal,
CheckCircle, CheckCircle,
AlertTriangle, AlertTriangle,
CircleDashed, CircleDashed,
ExternalLink,
Code,
Clock,
ChevronDown,
ChevronUp,
Loader2,
ArrowRight
} from 'lucide-react'; } from 'lucide-react';
import { ToolViewProps } from './types'; import { ToolViewProps } from './types';
import { import {
extractCommand,
extractCommandOutput,
extractExitCode, extractExitCode,
formatTimestamp, formatTimestamp,
getToolTitle, getToolTitle,
} from './utils'; } from './utils';
import { cn } from '@/lib/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({ export function CommandToolView({
name = 'execute-command', name = 'execute-command',
@ -24,15 +35,17 @@ export function CommandToolView({
isSuccess = true, isSuccess = true,
isStreaming = false, isStreaming = false,
}: ToolViewProps) { }: 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(() => { const rawCommand = React.useMemo(() => {
if (!assistantContent) return null; if (!assistantContent) return null;
try { try {
// Try to parse JSON content first
const parsed = JSON.parse(assistantContent); const parsed = JSON.parse(assistantContent);
if (parsed.content) { if (parsed.content) {
// Look for execute-command tag
const commandMatch = parsed.content.match( const commandMatch = parsed.content.match(
/<execute-command[^>]*>([\s\S]*?)<\/execute-command>/, /<execute-command[^>]*>([\s\S]*?)<\/execute-command>/,
); );
@ -41,7 +54,6 @@ export function CommandToolView({
} }
} }
} catch (e) { } catch (e) {
// If JSON parsing fails, try direct XML extraction
const commandMatch = assistantContent.match( const commandMatch = assistantContent.match(
/<execute-command[^>]*>([\s\S]*?)<\/execute-command>/, /<execute-command[^>]*>([\s\S]*?)<\/execute-command>/,
); );
@ -53,190 +65,279 @@ export function CommandToolView({
return null; return null;
}, [assistantContent]); }, [assistantContent]);
// Clean the command by removing any leading/trailing whitespace and newlines
const command = rawCommand const command = rawCommand
?.replace(/^suna@computer:~\$\s*/g, '') // Remove prompt prefix ?.replace(/^suna@computer:~\$\s*/g, '')
?.replace(/\\n/g, '') // Remove escaped newlines ?.replace(/\\n/g, '')
?.replace(/\n/g, '') // Remove actual newlines ?.replace(/\n/g, '')
?.trim(); // Clean up any remaining whitespace ?.trim();
// Extract and clean the output with improved parsing
const output = React.useMemo(() => { const output = React.useMemo(() => {
if (!toolContent) return null; if (!toolContent) return null;
let extractedOutput = '';
let success = true;
try {
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 { try {
// Try to parse JSON content first
const parsed = JSON.parse(toolContent); const parsed = JSON.parse(toolContent);
if (parsed.content) { if (parsed.output) {
// Look for tool_result tag extractedOutput = parsed.output;
const toolResultMatch = parsed.content.match( success = parsed.success !== false;
/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/, } else if (parsed.content) {
); extractedOutput = parsed.content;
if (toolResultMatch) { } else {
return toolResultMatch[1].trim(); extractedOutput = JSON.stringify(parsed, null, 2);
}
// 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) { } catch (e) {
// If JSON parsing fails, use the content as-is extractedOutput = toolContent;
return parsed.content;
} }
} }
} else {
extractedOutput = String(toolContent);
}
} catch (e) { } catch (e) {
// If JSON parsing fails, try direct XML extraction extractedOutput = String(toolContent);
const toolResultMatch = toolContent.match( console.error('Error parsing tool content:', e);
/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/,
);
if (toolResultMatch) {
return toolResultMatch[1].trim();
} }
const outputMatch = toolContent.match( return extractedOutput;
/ToolResult\(.*?output='([\s\S]*?)'.*?\)/,
);
if (outputMatch) {
return outputMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
}
}
return toolContent;
}, [toolContent]); }, [toolContent]);
const exitCode = extractExitCode(toolContent); const exitCode = extractExitCode(output);
const toolTitle = getToolTitle(name); 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 ( return (
<div className="flex flex-col h-full"> <Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-white dark:bg-zinc-950">
<div className="flex-1 p-4 overflow-auto"> <CardHeader className="h-13 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
<div className="border border-zinc-200 dark:border-zinc-800 rounded-md overflow-hidden h-full flex flex-col"> <div className="flex flex-row items-center justify-between">
<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 gap-2">
<div className="flex items-center"> <div className="p-2 rounded-lg bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20">
<Terminal className="h-4 w-4 mr-2 text-zinc-600 dark:text-zinc-400" /> <Terminal className="h-5 w-5 text-purple-500 dark:text-purple-400" />
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">
Terminal
</span>
</div> </div>
{exitCode !== null && !isStreaming && ( <div>
<span <CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
className={cn( {toolTitle}
'text-xs flex items-center', </CardTitle>
</div>
</div>
{!isStreaming && (
<Badge
variant="secondary"
className={
isSuccess isSuccess
? 'text-emerald-600 dark:text-emerald-400' ? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
: 'text-red-600 dark:text-red-400', : "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
}
>
{isSuccess ? (
<CheckCircle className="h-3.5 w-3.5 mr-1" />
) : (
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
)}
{isSuccess ? 'Command executed successfully' : 'Command failed'}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
{isStreaming ? (
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
<div className="text-center w-full max-w-xs">
<div className="w-16 h-16 rounded-full mx-auto mb-6 flex items-center justify-center bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20">
<Loader2 className="h-8 w-8 animate-spin text-purple-500 dark:text-purple-400" />
</div>
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2">
Executing command
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
<span className="font-mono text-xs break-all">{command || 'Processing command...'}</span>
</p>
<Progress value={progress} className="w-full h-2" />
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-2">{progress}%</p>
</div>
</div>
) : command ? (
<ScrollArea className="h-full w-full">
<div className="p-4">
<div className="mb-4 bg-zinc-100 dark:bg-zinc-800/80 rounded-lg overflow-hidden shadow-sm border border-zinc-200 dark:border-zinc-800">
<div className="bg-zinc-200 dark:bg-zinc-800 px-4 py-2 flex items-center gap-2">
<Code className="h-4 w-4 text-zinc-600 dark:text-zinc-400" />
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Command</span>
</div>
<div className="p-4 font-mono text-sm text-zinc-700 dark:text-zinc-300 flex gap-2">
<span className="text-purple-500 dark:text-purple-400 select-none">$</span>
<code className="flex-1 break-all">{command}</code>
</div>
</div>
{output && (
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-zinc-700 dark:text-zinc-300 flex items-center">
<ArrowRight className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
Output
{exitCode !== null && (
<Badge
className={cn(
"ml-2",
exitCode === 0
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
)} )}
> >
<span className="h-1.5 w-1.5 rounded-full mr-1.5 bg-current"></span> Exit code: {exitCode}
Exit: {exitCode} </Badge>
</span>
)} )}
</div> </h3>
{hasMoreLines && (
<div className="terminal-container flex-1 overflow-auto bg-black text-zinc-300 font-mono"> <Button
<div className="p-3 text-xs"> variant="ghost"
{command && output && !isStreaming && ( size="sm"
<div className="space-y-2"> onClick={() => setShowFullOutput(!showFullOutput)}
<div className="flex items-start"> className="h-7 text-xs flex items-center gap-1"
<span className="text-emerald-400 shrink-0 mr-2"> >
suna@computer:~$ {showFullOutput ? (
</span> <>
<span className="text-zinc-300">{command}</span> <ChevronUp className="h-3 w-3" />
</div> Show less
</>
<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" /> <>
<ChevronDown className="h-3 w-3" />
Show full output
</>
)} )}
<span> </Button>
{isSuccess )}
? `Command completed successfully${exitCode !== null ? ` (exit code: ${exitCode})` : ''}` </div>
: `Command failed${exitCode !== null ? ` with exit code ${exitCode}` : ''}`}
</span> <div className="bg-zinc-100 dark:bg-neutral-900 rounded-lg overflow-hidden border border-zinc-00/20">
<div className="bg-zinc-300 dark:bg-neutral-800 px-3 py-1 flex items-center justify-between border-b border-zinc-300/50 dark:border-zinc-700/50">
<span className="text-xs text-zinc-600 dark:text-zinc-400">Terminal output</span>
{exitCode !== null && exitCode !== 0 && (
<Badge variant="outline" className="text-xs h-5 border-red-700/30 text-red-400">
<AlertTriangle className="h-3 w-3 mr-1" />
Error
</Badge>
)}
</div>
<div className="p-4 max-h-96 overflow-auto scrollbar-hide">
<pre className="text-xs text-zinc-600 dark:text-zinc-300 font-mono whitespace-pre-wrap break-all overflow-visible">
{linesToShow.map((line, index) => (
<div
key={index}
className={cn(
"py-0.5 bg-transparent",
)}
>
{line || ' '}
</div>
))}
{!showFullOutput && hasMoreLines && (
<div className="text-zinc-500 mt-2 border-t border-zinc-700/30 pt-2">
+ {formattedOutput.length - 10} more lines
</div>
)}
</pre>
</div>
</div>
</div> </div>
)} )}
{isStreaming && ( {!output && !isStreaming && (
<div className="flex items-center gap-2"> <div className="bg-black rounded-lg overflow-hidden border border-zinc-700/20 shadow-md p-6 flex items-center justify-center">
<CircleDashed className="h-3.5 w-3.5 text-blue-500 animate-spin" /> <div className="text-center">
<span>Executing command...</span> <CircleDashed className="h-8 w-8 text-zinc-500 mx-auto mb-2" />
<p className="text-zinc-400 text-sm">No output received</p>
</div>
</div> </div>
)} )}
</div>
</ScrollArea>
) : (
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-zinc-100 to-zinc-50 shadow-inner dark:from-zinc-800/40 dark:to-zinc-900/60">
<Terminal className="h-10 w-10 text-zinc-400 dark:text-zinc-600" />
</div>
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
No Command Found
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 text-center max-w-md">
No command was detected. Please provide a valid command to execute.
</p>
</div>
)}
</CardContent>
<div className="text-xs"> <div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
{!isStreaming && command && (
<Badge variant="outline" className="h-6 py-0.5 bg-zinc-50 dark:bg-zinc-900">
<Terminal className="h-3 w-3 mr-1" />
Command
</Badge>
)}
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-2">
<Clock className="h-3.5 w-3.5" />
{toolTimestamp && !isStreaming {toolTimestamp && !isStreaming
? formatTimestamp(toolTimestamp) ? formatTimestamp(toolTimestamp)
: assistantTimestamp : assistantTimestamp
@ -244,7 +345,6 @@ export function CommandToolView({
: ''} : ''}
</div> </div>
</div> </div>
</div> </Card>
</div>
); );
} }

View File

@ -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 { ToolViewProps } from './types';
import { formatTimestamp } from './utils'; import { formatTimestamp } from './utils';
import { ExternalLink, CheckCircle, AlertTriangle } from 'lucide-react';
import { Markdown } from '@/components/ui/markdown';
import { cn } from '@/lib/utils'; 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({ export function ExposePortToolView({
name = 'expose-port', name = 'expose-port',
@ -14,18 +27,10 @@ export function ExposePortToolView({
assistantTimestamp, assistantTimestamp,
toolTimestamp, toolTimestamp,
}: ToolViewProps) { }: ToolViewProps) {
console.log('ExposePortToolView:', { const [progress, setProgress] = useState(0);
name,
assistantContent,
toolContent,
isSuccess,
isStreaming,
assistantTimestamp,
toolTimestamp,
});
// Parse the assistant content // Parse the assistant content
const parsedAssistantContent = React.useMemo(() => { const parsedAssistantContent = useMemo(() => {
if (!assistantContent) return null; if (!assistantContent) return null;
try { try {
const parsed = JSON.parse(assistantContent); const parsed = JSON.parse(assistantContent);
@ -37,7 +42,7 @@ export function ExposePortToolView({
}, [assistantContent]); }, [assistantContent]);
// Parse the tool result // Parse the tool result
const toolResult = React.useMemo(() => { const toolResult = useMemo(() => {
if (!toolContent) return null; if (!toolContent) return null;
try { try {
// First parse the outer JSON // First parse the outer JSON
@ -56,7 +61,7 @@ export function ExposePortToolView({
}, [toolContent]); }, [toolContent]);
// Extract port number from assistant content // Extract port number from assistant content
const portNumber = React.useMemo(() => { const portNumber = useMemo(() => {
if (!parsedAssistantContent) return null; if (!parsedAssistantContent) return null;
try { try {
const match = parsedAssistantContent.match( const match = parsedAssistantContent.match(
@ -69,141 +74,219 @@ export function ExposePortToolView({
} }
}, [parsedAssistantContent]); }, [parsedAssistantContent]);
// If we have no content to show, render a placeholder // Simulate progress when streaming
if (!portNumber && !toolResult && !isStreaming) { useEffect(() => {
return ( if (isStreaming) {
<div className="flex flex-col h-full p-4"> const timer = setInterval(() => {
<div className="text-xs text-zinc-500 dark:text-zinc-400"> setProgress((prevProgress) => {
No port exposure information available if (prevProgress >= 95) {
</div> clearInterval(timer);
</div> return prevProgress;
);
} }
return prevProgress + 5;
});
}, 300);
return () => clearInterval(timer);
} else {
setProgress(100);
}
}, [isStreaming]);
return ( return (
<div className="flex flex-col h-full"> <Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-white dark:bg-zinc-950">
<div className="flex-1 p-4 overflow-auto"> <CardHeader className="h-13 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
{/* Assistant Content */} <div className="flex flex-row items-center justify-between">
{portNumber && !isStreaming && (
<div className="space-y-1.5">
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
Port to Expose
</div>
{assistantTimestamp && (
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{formatTimestamp(assistantTimestamp)}
</div>
)}
</div>
<div className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 p-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-300"> <div className="p-2 rounded-lg bg-gradient-to-b from-emerald-100 to-emerald-50 shadow-inner dark:from-emerald-800/40 dark:to-emerald-900/60 dark:shadow-emerald-950/20">
Port <Computer className="h-5 w-5 text-emerald-500 dark:text-emerald-400" />
</div> </div>
<div className="px-2 py-1 rounded-md bg-zinc-100 dark:bg-zinc-800 text-xs font-mono text-zinc-800 dark:text-zinc-300"> <div>
{portNumber} <CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
Port Exposure
</CardTitle>
</div> </div>
</div> </div>
</div>
</div>
)}
{/* Tool Result */} {!isStreaming && (
{toolResult && ( <Badge
<div className="space-y-1.5 mt-4"> variant="secondary"
<div className="flex justify-between items-center"> className={
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400"> isSuccess
{isStreaming ? 'Processing' : 'Exposed URL'} ? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
</div> : "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
{toolTimestamp && !isStreaming && ( }
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{formatTimestamp(toolTimestamp)}
</div>
)}
</div>
<div
className={cn(
'rounded-md border p-3',
isStreaming
? 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/10'
: isSuccess
? 'border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900'
: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/10',
)}
> >
{isSuccess ? (
<CheckCircle className="h-3.5 w-3.5 mr-1" />
) : (
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
)}
{isSuccess ? 'Port exposed successfully' : 'Failed to expose port'}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
{isStreaming ? ( {isStreaming ? (
<div className="flex items-center gap-2 text-xs font-medium text-blue-700 dark:text-blue-400"> <div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
<span>Exposing port {portNumber}...</span> <div className="text-center w-full max-w-xs">
<div className="w-16 h-16 rounded-full mx-auto mb-6 flex items-center justify-center bg-gradient-to-b from-emerald-100 to-emerald-50 shadow-inner dark:from-emerald-800/40 dark:to-emerald-900/60 dark:shadow-emerald-950/20">
<Loader2 className="h-8 w-8 animate-spin text-emerald-500 dark:text-emerald-400" />
</div>
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2">
Exposing port
</h3>
<div className="mb-4">
<Badge className="bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-300 text-xs font-mono px-3 py-1 rounded-md">
{portNumber}
</Badge>
</div>
<Progress value={progress} className="w-full h-2" />
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-2">{progress}%</p>
</div>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <ScrollArea className="h-full w-full">
<div className="flex items-center gap-2"> <div className="p-4 py-0 my-4 space-y-6">
<ExternalLink className="h-4 w-4 text-zinc-500 dark:text-zinc-400" /> {/* Port Information Section */}
{portNumber && (
<div className="bg-zinc-50/70 dark:bg-zinc-900/50 p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 shadow-sm">
<h3 className="text-sm font-medium text-zinc-800 dark:text-zinc-300 mb-3 flex items-center">
<Computer className="h-4 w-4 mr-2 opacity-70" />
Port to Expose
</h3>
<div className="flex items-center gap-2 bg-white dark:bg-zinc-800 p-3 rounded-md border border-zinc-200 dark:border-zinc-700 mb-1">
<div className="text-xs text-zinc-500 dark:text-zinc-400">Port Number</div>
<Badge className="bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 border-none font-mono text-xs px-3 py-1">
{portNumber}
</Badge>
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-2">
{assistantTimestamp && formatTimestamp(assistantTimestamp)}
</div>
</div>
)}
{/* Exposed URL Section */}
{toolResult && (
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-sm overflow-hidden">
<div className="p-4">
<div className="flex items-start gap-3 mb-3">
<div className="p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900/30">
<Link2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-zinc-800 dark:text-zinc-200 mb-2">
Exposed URL
</h3>
<a <a
href={toolResult.url} href={toolResult.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline break-all" className="text-md font-medium text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-2 mb-3"
> >
{toolResult.url} {toolResult.url}
<ExternalLink className="flex-shrink-0 h-3.5 w-3.5" />
</a> </a>
</div> </div>
<div className="flex items-center gap-2">
<div className="text-xs text-zinc-600 dark:text-zinc-400">
Port
</div> </div>
<div className="px-2 py-1 rounded-md bg-zinc-100 dark:bg-zinc-800 text-xs font-mono text-zinc-800 dark:text-zinc-300">
{toolResult.port} <div className="space-y-3">
<div className="flex flex-col gap-1.5">
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
Port Details
</div>
<div className="flex gap-2 flex-wrap">
<Badge variant="outline" className="bg-zinc-50 dark:bg-zinc-800 font-mono">
Port: {toolResult.port}
</Badge>
</div> </div>
</div> </div>
<div className="text-xs text-zinc-600 dark:text-zinc-400">
<div className="text-sm text-zinc-600 dark:text-zinc-400">
{toolResult.message} {toolResult.message}
</div> </div>
<div className="text-xs text-amber-600 dark:text-amber-400 italic">
Note: This URL might only be temporarily available and could <div className="text-xs bg-amber-50 dark:bg-amber-950/30 border border-amber-100 dark:border-amber-900/50 rounded-md p-3 text-amber-600 dark:text-amber-400 flex items-start gap-2">
expire after some time. <AlertTriangle className="h-4 w-4 flex-shrink-0 mt-0.5" />
<span>This URL might only be temporarily available and could expire after some time.</span>
</div>
</div>
</div>
<div className="bg-zinc-50 dark:bg-zinc-800/50 border-t border-zinc-200 dark:border-zinc-800 p-3 flex justify-between items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-7 text-xs bg-white dark:bg-zinc-900"
asChild
>
<a href={toolResult.url} target="_blank" rel="noopener noreferrer">
<Globe className="h-3 w-3 mr-1.5" />
Open in Browser
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open the exposed URL in a new tab</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{toolTimestamp && formatTimestamp(toolTimestamp)}
</div>
</div> </div>
</div> </div>
)} )}
{/* Empty State */}
{!portNumber && !toolResult && !isStreaming && (
<div className="flex flex-col items-center justify-center py-12 px-6">
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-zinc-100 to-zinc-50 shadow-inner dark:from-zinc-800/40 dark:to-zinc-900/60">
<Computer className="h-10 w-10 text-zinc-400 dark:text-zinc-600" />
</div> </div>
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
No Port Information
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 text-center max-w-md">
No port exposure information is available yet. Use the expose-port command to share a local port.
</p>
</div> </div>
)} )}
</div> </div>
</ScrollArea>
)}
</CardContent>
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
{!isStreaming && toolResult && (
<Badge className="h-6 py-0.5 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 border-none">
<Computer className="h-3 w-3 mr-1" />
Port {portNumber}
</Badge>
)}
</div>
{/* Footer */} <div className="text-xs text-zinc-500 dark:text-zinc-400">
<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 ? ( {isSuccess ? (
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" /> <span className="flex items-center gap-1.5">
) : ( <CheckCircle className="h-3 w-3 text-emerald-500" />
<AlertTriangle className="h-3.5 w-3.5 text-red-500" /> {isStreaming ? 'Exposing...' : 'Port exposed successfully'}
)} </span>
<span> ) : (
{isSuccess <span className="flex items-center gap-1.5">
? 'Port exposed successfully' <AlertTriangle className="h-3 w-3 text-red-500" />
: 'Failed to expose port'} Failed to expose port
</span> </span>
</div>
)} )}
{isStreaming && (
<div className="flex items-center gap-2">
<span>Exposing port...</span>
</div>
)}
<div className="text-xs">
{toolTimestamp && !isStreaming
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
</div>
</div> </div>
</div> </div>
</Card>
); );
} }

View File

@ -432,19 +432,16 @@ export function FileOperationToolView({
</Tabs> </Tabs>
) : ( ) : (
<> <>
<CardHeader className="bg-gradient-to-r from-zinc-50/80 to-zinc-100/80 dark:from-zinc-900/80 dark:to-zinc-800/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-800 p-2 py-1 flex-row items-center justify-between space-y-0"> <CardHeader className="h-13 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
<div className="flex flex-row items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={cn("p-2 rounded-lg", config.bgColor)}> <div className={cn("p-2 rounded-lg", config.bgColor)}>
<Icon className={cn("h-5 w-5", config.color)} /> <Icon className={cn("h-5 w-5", config.color)} />
</div> </div>
<div> <div>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100"> <CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{operation === 'create' ? 'Create File' :
operation === 'rewrite' ? 'Update File' : 'Delete File'}
</CardTitle>
<CardDescription className="text-sm text-zinc-500 dark:text-zinc-400 font-mono">
{processedFilePath || 'Unknown file path'} {processedFilePath || 'Unknown file path'}
</CardDescription> </CardTitle>
</div> </div>
</div> </div>
@ -465,6 +462,7 @@ export function FileOperationToolView({
</Badge> </Badge>
)} )}
</div> </div>
</div>
</CardHeader> </CardHeader>
<CardContent className="p-0 flex-1 overflow-hidden relative"> <CardContent className="p-0 flex-1 overflow-hidden relative">

View File

@ -16,12 +16,21 @@ export type ToolViewComponent = React.ComponentType<ToolViewProps>;
type ToolViewRegistryType = Record<string, ToolViewComponent>; type ToolViewRegistryType = Record<string, ToolViewComponent>;
const defaultRegistry: ToolViewRegistryType = { const defaultRegistry: ToolViewRegistryType = {
'browser-navigate': BrowserToolView, 'browser-navigate-to': BrowserToolView,
'browser-click': BrowserToolView, 'browser-go-back': BrowserToolView,
'browser-extract': BrowserToolView,
'browser-fill': BrowserToolView,
'browser-wait': 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, 'execute-command': CommandToolView,