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

@ -118,4 +118,4 @@ export function ThreadSkeleton({
)}
</div>
);
}
}

View File

@ -6,6 +6,9 @@ import {
CheckCircle,
AlertTriangle,
CircleDashed,
ChevronDown,
ChevronUp,
Loader2
} from 'lucide-react';
import { ToolViewProps } from './types';
import {
@ -17,6 +20,12 @@ import {
import { ApiMessageType } from '@/components/thread/types';
import { safeJsonParse } from '@/components/thread/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({
name = 'browser-operation',
@ -84,7 +93,6 @@ export function BrowserToolView({
browserStateMessage.content,
{},
);
console.log('Browser state content: ', browserStateContent)
screenshotBase64 = browserStateContent?.screenshot_base64 || null;
}
}
@ -110,120 +118,180 @@ export function BrowserToolView({
<iframe
src={vncPreviewUrl}
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
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="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">
<MonitorPlay 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">
Browser Window
</span>
</div>
{url && (
<div className="text-xs font-mono text-zinc-500 dark:text-zinc-400 truncate max-w-[340px]">
{url}
</div>
)}
</div>
// Progress for loading state - only for visual feedback
const [progress, setProgress] = React.useState(100);
{/* Preview Logic */}
<div className="flex-1 flex items-stretch bg-black">
{isLastToolCall ? (
// Only show live sandbox or fallback to sandbox for the last tool call
isRunning && vncIframe ? (
// Use the memoized iframe for live preview
vncIframe
) : screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
<img
src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot"
className="max-w-full max-h-full object-contain"
/>
</div>
) : vncIframe ? (
// Use the memoized iframe
vncIframe
// 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 (
<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">
<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-2">
<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-5 w-5 text-purple-500 dark:text-purple-400" />
</div>
<div>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{toolTitle}
</CardTitle>
</div>
</div>
{!isRunning && (
<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" />
) : (
<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">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
<p className="text-sm font-medium">
Browser preview not available
</p>
{url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="mt-3 flex items-center text-blue-600 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 hover:underline"
>
Visit URL <ExternalLink className="h-3 w-3 ml-1" />
</a>
)}
<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 ? (
// Only show live sandbox or fallback to screenshot for the last tool call
isRunning && vncIframe ? (
<div className="flex flex-col items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
<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>
)
) : // For non-last tool calls, only show screenshot if available, otherwise show "No Browser State image found"
screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
</div>
) : screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
<img
src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot"
className="max-w-full max-h-full object-contain"
/>
</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">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
<p className="text-sm font-medium">
No Browser State image found
</p>
) : vncIframe ? (
// Use the memoized iframe
<div className="flex flex-col items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
{vncIframe}
</div>
)}
</div>
) : (
<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">
<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">
<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
</h3>
{url && (
<div className="mt-4">
<Button
variant="outline"
size="sm"
className="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-700 shadow-sm hover:shadow-md transition-shadow"
asChild
>
<a href={url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3.5 w-3.5 mr-2" />
Visit URL
</a>
</Button>
</div>
)}
</div>
)
) : // For non-last tool calls, only show screenshot if available, otherwise show "No Browser State"
screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
<img
src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot"
className="max-w-full max-h-full object-contain"
/>
</div>
) : (
<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">
<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">
<MonitorPlay 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 Browser State Available
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
Browser state image not found for this action
</p>
</div>
)}
</div>
</div>
</CardContent>
{/* 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">
<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">
{!isRunning && (
<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
? `${operation} completed successfully`
: `${operation} failed`}
</span>
</div>
<Badge className="h-6 py-0.5">
<Globe className="h-3 w-3" />
{operation}
</Badge>
)}
{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>
{url && (
<span className="text-xs truncate max-w-[200px] hidden sm:inline-block">
{url}
</span>
)}
<div className="text-xs">
{toolTimestamp && !isRunning
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{toolTimestamp && !isRunning
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
</div>
</div>
</Card>
);
}
}

View File

@ -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(
/<execute-command[^>]*>([\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(
/<execute-command[^>]*>([\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(
/<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;
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(
/<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, '"');
}
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 (
<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>
<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">
<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-2">
<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-5 w-5 text-purple-500 dark:text-purple-400" />
</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>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{toolTitle}
</CardTitle>
</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">
<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 text-emerald-500" />
<CheckCircle className="h-3.5 w-3.5 mr-1" />
) : (
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
)}
<span>
{isSuccess
? `Command completed successfully${exitCode !== null ? ` (exit code: ${exitCode})` : ''}`
: `Command failed${exitCode !== null ? ` with exit code ${exitCode}` : ''}`}
</span>
</div>
{isSuccess ? 'Command executed successfully' : 'Command failed'}
</Badge>
)}
</div>
</CardHeader>
{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>
<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 className="text-xs">
{toolTimestamp && !isStreaming
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</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"
)}
>
Exit code: {exitCode}
</Badge>
)}
</h3>
{hasMoreLines && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowFullOutput(!showFullOutput)}
className="h-7 text-xs flex items-center gap-1"
>
{showFullOutput ? (
<>
<ChevronUp className="h-3 w-3" />
Show less
</>
) : (
<>
<ChevronDown className="h-3 w-3" />
Show full output
</>
)}
</Button>
)}
</div>
<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>
)}
{!output && !isStreaming && (
<div className="bg-black rounded-lg overflow-hidden border border-zinc-700/20 shadow-md p-6 flex items-center justify-center">
<div className="text-center">
<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>
</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="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
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
</div>
</div>
</Card>
);
}

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 { 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 (
<div className="flex flex-col h-full p-4">
<div className="text-xs text-zinc-500 dark:text-zinc-400">
No port exposure information available
</div>
</div>
);
}
// 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 (
<div className="flex flex-col h-full">
<div className="flex-1 p-4 overflow-auto">
{/* Assistant Content */}
{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>
)}
<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">
<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-2">
<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">
<Computer className="h-5 w-5 text-emerald-500 dark:text-emerald-400" />
</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="text-xs font-medium text-zinc-800 dark:text-zinc-300">
Port
</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">
{portNumber}
</div>
</div>
<div>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
Port Exposure
</CardTitle>
</div>
</div>
)}
{/* Tool Result */}
{toolResult && (
<div className="space-y-1.5 mt-4">
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
{isStreaming ? 'Processing' : 'Exposed URL'}
</div>
{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',
)}
>
{isStreaming ? (
<div className="flex items-center gap-2 text-xs font-medium text-blue-700 dark:text-blue-400">
<span>Exposing port {portNumber}...</span>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2">
<ExternalLink className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
<a
href={toolResult.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline break-all"
>
{toolResult.url}
</a>
</div>
<div className="flex items-center gap-2">
<div className="text-xs text-zinc-600 dark:text-zinc-400">
Port
</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>
</div>
<div className="text-xs text-zinc-600 dark:text-zinc-400">
{toolResult.message}
</div>
<div className="text-xs text-amber-600 dark:text-amber-400 italic">
Note: This URL might only be temporarily available and could
expire after some time.
</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">
<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 text-emerald-500" />
<CheckCircle className="h-3.5 w-3.5 mr-1" />
) : (
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
)}
<span>
{isSuccess
? 'Port exposed successfully'
: 'Failed to expose port'}
</span>
</div>
{isSuccess ? 'Port exposed successfully' : 'Failed to expose port'}
</Badge>
)}
</div>
</CardHeader>
{isStreaming && (
<div className="flex items-center gap-2">
<span>Exposing port...</span>
<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-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 className="text-xs">
{toolTimestamp && !isStreaming
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
) : (
<ScrollArea className="h-full w-full">
<div className="p-4 py-0 my-4 space-y-6">
{/* 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
href={toolResult.url}
target="_blank"
rel="noopener noreferrer"
className="text-md font-medium text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-2 mb-3"
>
{toolResult.url}
<ExternalLink className="flex-shrink-0 h-3.5 w-3.5" />
</a>
</div>
</div>
<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 className="text-sm text-zinc-600 dark:text-zinc-400">
{toolResult.message}
</div>
<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">
<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>
)}
{/* 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>
<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>
</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>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{isSuccess ? (
<span className="flex items-center gap-1.5">
<CheckCircle className="h-3 w-3 text-emerald-500" />
{isStreaming ? 'Exposing...' : 'Port exposed successfully'}
</span>
) : (
<span className="flex items-center gap-1.5">
<AlertTriangle className="h-3 w-3 text-red-500" />
Failed to expose port
</span>
)}
</div>
</div>
</div>
</Card>
);
}

View File

@ -432,39 +432,37 @@ export function FileOperationToolView({
</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">
<div className="flex items-center gap-3">
<div className={cn("p-2 rounded-lg", config.bgColor)}>
<Icon className={cn("h-5 w-5", config.color)} />
<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={cn("p-2 rounded-lg", config.bgColor)}>
<Icon className={cn("h-5 w-5", config.color)} />
</div>
<div>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{processedFilePath || 'Unknown file path'}
</CardTitle>
</div>
</div>
<div>
<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'}
</CardDescription>
<div className="flex items-center gap-2">
{!isStreaming ? (
<Badge variant="secondary" className={cn("px-2 py-1 transition-colors", config.badgeColor)}>
{isSuccess ? (
<CheckCircle className="mr-1.5 h-3.5 w-3.5" />
) : (
<AlertTriangle className="mr-1.5 h-3.5 w-3.5" />
)}
{isSuccess ? config.successMessage : `Failed to ${operation}`}
</Badge>
) : (
<Badge variant="secondary" className="bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300 px-2 py-1">
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
{config.progressMessage}
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2">
{!isStreaming ? (
<Badge variant="secondary" className={cn("px-2 py-1 transition-colors", config.badgeColor)}>
{isSuccess ? (
<CheckCircle className="mr-1.5 h-3.5 w-3.5" />
) : (
<AlertTriangle className="mr-1.5 h-3.5 w-3.5" />
)}
{isSuccess ? config.successMessage : `Failed to ${operation}`}
</Badge>
) : (
<Badge variant="secondary" className="bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300 px-2 py-1">
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
{config.progressMessage}
</Badge>
)}
</div>
</CardHeader>
<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>;
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,