import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Bot, Clock3, Download, Expand, ExternalLink, Loader2, MonitorPlay, MousePointer2, RefreshCw, Settings, Square, Trash2, X, } from 'lucide-react'; import { cn } from '../../../lib/utils'; import { Badge, Button } from '../../../shared/view/ui'; import { authenticatedFetch } from '../../../utils/api'; import type { SettingsMainTab } from '../../settings/types/types'; const BROWSER_USE_GUIDE_URL = 'https://cloudcli.ai/docs/browser-use'; const BROWSER_USE_CACHE_TTL_MS = 30_000; type BrowserUseStatus = { enabled: boolean; available: boolean; backend: 'playwright' | 'camoufox-vnc'; browserBackend: 'playwright' | 'camoufox-vnc'; playwrightInstalled: boolean; chromiumInstalled: boolean; installInProgress: boolean; sessionCount: number; message: string; }; type BrowserUseSession = { id: string; status: 'ready' | 'stopped' | 'unavailable'; url: string | null; title: string | null; screenshotDataUrl: string | null; createdAt: string; updatedAt: string; lastAction: string | null; message: string | null; backend?: 'playwright' | 'camoufox-vnc'; viewerUrl?: string | null; viewerEmbedUrl?: string | null; createdBy: 'agent'; profileName: string | null; viewport: { width: number; height: number; } | null; cursor: { x: number; y: number; actor: 'agent'; } | null; }; type BrowserUsePanelProps = { isVisible: boolean; projectId?: string | null; onShowSettings?: (tab?: SettingsMainTab) => void; }; type BrowserUsePanelCacheEntry = { status: BrowserUseStatus | null; sessions: BrowserUseSession[]; selectedSessionId: string | null; updatedAt: number; }; const browserUsePanelCache = new Map(); async function readJson(response: Response): Promise { const text = await response.text(); let data: any = {}; if (text) { try { data = JSON.parse(text); } catch { throw new Error(response.ok ? 'Received an invalid Browser response.' : `Browser request failed (${response.status}).`); } } if (!response.ok || data.success === false) { throw new Error(data.error || data.details || `Request failed (${response.status})`); } return data as T; } async function fetchBrowserPanelData() { const [statusResponse, sessionsResponse] = await Promise.all([ authenticatedFetch('/api/browser-use/status'), authenticatedFetch('/api/browser-use/sessions'), ]); const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse); const sessionsData = await readJson<{ data: { sessions: BrowserUseSession[] } }>(sessionsResponse); return { status: statusData.data, sessions: [...sessionsData.data.sessions].sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)), }; } function formatRelativeTime(value: string | null): string { if (!value) return 'Never'; const timestamp = Date.parse(value); if (!Number.isFinite(timestamp)) return 'Unknown'; const elapsedSeconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000)); if (elapsedSeconds < 10) return 'Just now'; if (elapsedSeconds < 60) return `${elapsedSeconds}s ago`; const elapsedMinutes = Math.round(elapsedSeconds / 60); if (elapsedMinutes < 60) return `${elapsedMinutes}m ago`; const elapsedHours = Math.round(elapsedMinutes / 60); if (elapsedHours < 24) return `${elapsedHours}h ago`; return `${Math.round(elapsedHours / 24)}d ago`; } function getDomain(url: string | null): string { if (!url) return 'No page loaded'; try { return new URL(url).hostname; } catch { return url; } } function formatAction(action: string | null): string { if (!action) return 'Waiting'; return action.replace(/_/g, ' ').replace(/:/g, ': '); } function getStatusTone(status: BrowserUseSession['status']): string { if (status === 'ready') { return 'border-primary/30 bg-primary/5 text-foreground'; } if (status === 'stopped') { return 'border-border bg-muted text-muted-foreground'; } return 'border-border bg-background text-muted-foreground'; } function getRuntimeTone(status: BrowserUseStatus | null, installing: boolean): string { if (!status?.enabled) return 'border-border bg-muted text-muted-foreground'; if (status.available) return 'border-primary/30 bg-primary/5 text-foreground'; if (status.installInProgress || installing) return 'border-primary/30 bg-primary/5 text-foreground'; return 'border-border bg-background text-muted-foreground'; } function getStatusDot(status: BrowserUseSession['status']): string { if (status === 'ready') return 'bg-primary'; if (status === 'stopped') return 'bg-muted-foreground/50'; return 'bg-border'; } function getEngineLabel(backend?: BrowserUseStatus['backend'] | BrowserUseSession['backend']): string { return backend === 'camoufox-vnc' ? 'Visible browser' : 'Playwright'; } const PROMPTS = [ 'Use Browser to inspect the checkout flow and report any broken UI states.', 'Open with Browser, interact with the page, and summarize what changed after each step.', ]; function getBrowserUseCacheKey(projectId?: string | null): string { return projectId ? `browser-use:project:${projectId}` : 'browser-use:global'; } function getFreshCacheEntry(cacheKey: string): BrowserUsePanelCacheEntry | null { const entry = browserUsePanelCache.get(cacheKey); if (!entry || Date.now() - entry.updatedAt > BROWSER_USE_CACHE_TTL_MS) { return null; } return entry; } export default function BrowserUsePanel({ isVisible, projectId, onShowSettings }: BrowserUsePanelProps) { const cacheKey = getBrowserUseCacheKey(projectId); const initialCacheEntry = getFreshCacheEntry(cacheKey); const [status, setStatus] = useState(() => initialCacheEntry?.status ?? null); const [sessions, setSessions] = useState(() => initialCacheEntry?.sessions ?? []); const [selectedSessionId, setSelectedSessionId] = useState(() => ( initialCacheEntry?.selectedSessionId || initialCacheEntry?.sessions[0]?.id || null )); const [hasLoadedOnce, setHasLoadedOnce] = useState(Boolean(initialCacheEntry)); const [isRefreshing, setIsRefreshing] = useState(false); const [isBusy, setIsBusy] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [error, setError] = useState(null); const activeLoadIdRef = useRef(0); const selectedSession = useMemo( () => sessions.find((session) => session.id === selectedSessionId) || sessions[0] || null, [selectedSessionId, sessions], ); const activeSessions = sessions.filter((session) => session.status === 'ready'); const isInitialLoading = isRefreshing && !hasLoadedOnce && sessions.length === 0; const isBackgroundRefreshing = isRefreshing && !isInitialLoading; const needsBrowserBinaries = Boolean(status?.enabled && !status.available); const runtimeLabel = isInitialLoading ? 'Loading' : !status?.enabled ? 'Disabled' : status.available ? 'Ready' : status.installInProgress || isInstalling ? 'Installing' : 'Setup required'; const cursorStyle = selectedSession?.cursor && selectedSession.viewport ? { left: `${(selectedSession.cursor.x / selectedSession.viewport.width) * 100}%`, top: `${(selectedSession.cursor.y / selectedSession.viewport.height) * 100}%`, } : null; const refresh = useCallback(async () => { const loadId = activeLoadIdRef.current + 1; activeLoadIdRef.current = loadId; setIsRefreshing(true); try { let nextData: Awaited>; try { nextData = await fetchBrowserPanelData(); } catch (error) { if (loadId !== activeLoadIdRef.current) { return; } await new Promise((resolve) => setTimeout(resolve, 350)); nextData = await fetchBrowserPanelData(); } if (activeLoadIdRef.current !== loadId) { return; } const nextSessions = nextData.sessions; setStatus(nextData.status); setSessions(nextSessions); setHasLoadedOnce(true); let nextSelectedSessionId: string | null = null; setSelectedSessionId((current) => { nextSelectedSessionId = current && nextSessions.some((session) => session.id === current) ? current : nextSessions[0]?.id || null; return nextSelectedSessionId; }); browserUsePanelCache.set(cacheKey, { status: nextData.status, sessions: nextSessions, selectedSessionId: nextSelectedSessionId, updatedAt: Date.now(), }); setError(null); } catch (err) { if (activeLoadIdRef.current !== loadId) { return; } setHasLoadedOnce(true); setError(err instanceof Error ? err.message : 'Failed to load Browser'); } finally { if (activeLoadIdRef.current === loadId) { setIsRefreshing(false); } } }, [cacheKey]); useEffect(() => { const cachedEntry = browserUsePanelCache.get(cacheKey); if (!cachedEntry) return; browserUsePanelCache.set(cacheKey, { ...cachedEntry, selectedSessionId, }); }, [cacheKey, selectedSessionId]); useEffect(() => { const cachedEntry = getFreshCacheEntry(cacheKey); setStatus(cachedEntry?.status ?? null); setSessions(cachedEntry?.sessions ?? []); setSelectedSessionId(cachedEntry?.selectedSessionId || cachedEntry?.sessions[0]?.id || null); setHasLoadedOnce(Boolean(cachedEntry)); setError(null); activeLoadIdRef.current += 1; }, [cacheKey]); useEffect(() => { if (!isVisible) return; void refresh(); }, [isVisible, refresh]); const runAction = useCallback(async (action: () => Promise) => { setIsBusy(true); setError(null); try { await action(); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : 'Browser action failed'); } finally { setIsBusy(false); } }, [refresh]); const stopSession = () => runAction(async () => { if (!selectedSession) return; const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/stop`, { method: 'POST' }); await readJson(response); }); const deleteSession = () => runAction(async () => { if (!selectedSession) return; const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}`, { method: 'DELETE' }); await readJson(response); setIsFullscreen(false); }); const installBrowserBinaries = () => runAction(async () => { setIsInstalling(true); try { const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' }); await readJson(response); } finally { setIsInstalling(false); } }); const renderSessionItem = (session: BrowserUseSession) => { const isSelected = selectedSession?.id === session.id; return ( ); }; const renderEmptyState = () => (
{status?.enabled ? 'No browser sessions yet' : 'Browser is disabled'}

{status?.enabled ? 'When an agent opens a browser, you can watch the latest screenshot, take control in a new tab, or end the running session.' : 'Enable Browser to let agents open websites, test flows, capture screenshots, and debug UI from a real page.'}

Read the Browser guide
{needsBrowserBinaries && (
Runtime setup required

{status?.message}

)}
{PROMPTS.map((prompt) => (
Prompt

{prompt}

))}
); const renderLoadingState = () => (
Loading browser sessions...
); const renderBrowserSurface = (fullscreen = false) => (
{selectedSession?.screenshotDataUrl ? (
Browser session screenshot {cursorStyle && (
)} {selectedSession?.viewerEmbedUrl && selectedSession.status === 'ready' && ( )}
) : (
{selectedSession?.message || 'Waiting for screenshot'}

The next agent browser snapshot will render here.

)}
); return (

Browser

{runtimeLabel} {getEngineLabel(status?.backend)}

Watch and manage browser sessions agents use to test real websites.

{isBackgroundRefreshing && (
Refreshing sessions...
)}
{onShowSettings && ( )}
{error && (
{error}
)} {sessions.length > 0 && (
{sessions.map((session) => ( ))}
)}
{activeSessions.length} active / {sessions.length} total
Updated {formatRelativeTime(selectedSession?.updatedAt || null)}
{sessions.length === 0 ? ( isInitialLoading ? renderLoadingState() : renderEmptyState() ) : (
{selectedSession?.status || 'empty'}
{selectedSession?.title || getDomain(selectedSession?.url || null)}
{selectedSession?.url || 'No page loaded'}
{getEngineLabel(selectedSession?.backend || status?.backend)} Profile: {selectedSession?.profileName || 'Temporary'} Updated {formatRelativeTime(selectedSession?.updatedAt || null)}
{formatAction(selectedSession?.lastAction || null)}
{selectedSession?.viewerUrl && selectedSession.status === 'ready' && ( )}
{renderBrowserSurface()}
)}
{isFullscreen && selectedSession && (
{selectedSession.title || selectedSession.url || 'Browser session'}
{renderBrowserSurface(true)}
)}
); }