import React, { useMemo } from 'react'; import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle, CircleDashed, } from 'lucide-react'; import { ToolViewProps } from './types'; import { extractBrowserUrl, extractBrowserOperation, formatTimestamp, getToolTitle, } from './utils'; import { ApiMessageType } from '@/components/thread/types'; import { safeJsonParse } from '@/components/thread/utils'; import { cn } from '@/lib/utils'; export function BrowserToolView({ name = 'browser-operation', assistantContent, toolContent, assistantTimestamp, toolTimestamp, isSuccess = true, isStreaming = false, project, agentStatus = 'idle', messages = [], currentIndex = 0, totalCalls = 1, }: ToolViewProps) { const url = extractBrowserUrl(assistantContent); const operation = extractBrowserOperation(name); const toolTitle = getToolTitle(name); // --- message_id Extraction Logic --- let browserStateMessageId: string | undefined; try { // 1. Parse the top-level JSON const topLevelParsed = safeJsonParse<{ content?: string }>(toolContent, {}); const innerContentString = topLevelParsed?.content; if (innerContentString && typeof innerContentString === 'string') { // 2. Extract the output='...' string using regex const outputMatch = innerContentString.match(/\boutput='(.*?)'(?=\s*\))/); const outputString = outputMatch ? outputMatch[1] : null; if (outputString) { // 3. Unescape the JSON string (basic unescaping for \n and \") const unescapedOutput = outputString .replace(/\\n/g, '\n') .replace(/\\"/g, '"'); // 4. Parse the unescaped JSON to get message_id const finalParsedOutput = safeJsonParse<{ message_id?: string }>( unescapedOutput, {}, ); browserStateMessageId = finalParsedOutput?.message_id; } } } catch (error) { console.error( '[BrowserToolView] Error parsing tool content for message_id:', error, ); } // Find the browser_state message and extract the screenshot let screenshotBase64: string | null = null; let latestBrowserState: any = null; let latestTimestamp = 0; if (messages.length > 0) { // Find the latest browser_state message by comparing timestamps messages.forEach((msg) => { if ((msg.type as string) === 'browser_state') { try { const content = safeJsonParse<{ timestamp?: number }>(msg.content, {}); const timestamp = content?.timestamp || 0; if (timestamp > latestTimestamp) { latestTimestamp = timestamp; latestBrowserState = content; } } catch (error) { console.error('[BrowserToolView] Error parsing browser state:', error); } } }); // Use the latest browser state if (latestBrowserState) { screenshotBase64 = latestBrowserState.screenshot_base64 || null; console.log('Latest browser state:', latestBrowserState); } } // Check if we have a VNC preview URL from the project const vncPreviewUrl = project?.sandbox?.vnc_preview ? `${project.sandbox.vnc_preview}/vnc_lite.html?password=${project?.sandbox?.pass}&autoconnect=true&scale=local&width=1024&height=768` : undefined; const isRunning = isStreaming || agentStatus === 'running'; const isLastToolCall = currentIndex === totalCalls - 1; // Memoize the VNC iframe to prevent reconnections on re-renders const vncIframe = useMemo(() => { if (!vncPreviewUrl) return null; console.log( '[BrowserToolView] Creating memoized VNC iframe with URL:', vncPreviewUrl, ); return ( ); }, [vncPreviewUrl]); // Only recreate if the URL changes return (
No Browser State image found