mirror of https://github.com/kortix-ai/suna.git
chore(ui): tool-panel ui revamp
This commit is contained in:
parent
6fc03aba04
commit
67604ea95a
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue