"use client"; import { useState, useEffect, useRef, useSyncExternalStore } from "react"; import { getEnvStatus } from "./actions/browse"; import type { EnvStatus } from "./actions/browse"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, } from "@/components/ui/resizable"; import { ALLOWED_URLS } from "@/lib/constants"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Loader2, Monitor, CircleX, Sun, Moon, Check } from "lucide-react"; const MOBILE_QUERY = "(max-width: 767px)"; const subscribe = (cb: () => void) => { const mql = window.matchMedia(MOBILE_QUERY); mql.addEventListener("change", cb); return () => mql.removeEventListener("change", cb); }; const getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches; const getServerSnapshot = () => false; function useIsMobile() { return useSyncExternalStore(subscribe, getMobileSnapshot, getServerSnapshot); } function useTheme() { const [theme, setThemeState] = useState<"light" | "dark">("light"); useEffect(() => { const stored = localStorage.getItem("theme"); const initial = stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light"; setThemeState(initial); document.documentElement.classList.toggle("dark", initial === "dark"); }, []); const toggle = () => { const next = theme === "dark" ? "light" : "dark"; setThemeState(next); document.documentElement.classList.toggle("dark", next === "dark"); localStorage.setItem("theme", next); }; return { theme, toggle }; } type Action = "screenshot" | "snapshot"; type StepInfo = { step: string; status: "running" | "done" | "error"; elapsed?: number; }; type BrowseResult = { ok: boolean; screenshot?: string; snapshot?: string; title?: string; error?: string; }; function formatError(raw: string): string { let cleaned = raw.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); const match = cleaned.match(/(?:error|Error)[:\s]*(.{1,200})/); if (match) cleaned = match[1].trim(); if (cleaned.length > 300) cleaned = cleaned.slice(0, 300) + "..."; return cleaned || raw.slice(0, 300); } function SegmentedControl({ value, onChange, options, }: { value: T; onChange: (v: T) => void; options: { value: T; label: string }[]; }) { return (
{options.map((opt) => ( ))}
); } function StepIndicator({ step }: { step: StepInfo }) { return (
{step.status === "running" ? ( ) : step.status === "done" ? ( ) : ( )}
{step.step} {step.elapsed != null && step.status !== "running" && ( {(step.elapsed / 1000).toFixed(1)}s )}
); } function ErrorDisplay({ error }: { error: string }) { const isHtml = /<[a-z][\s\S]*>/i.test(error); const message = isHtml ? formatError(error) : error; const showRaw = isHtml && error.length > 100; return (
Request failed {message} {showRaw && (
Show raw response
            {error}
          
)}
); } async function streamBrowse( url: string, action: Action, onStep: (step: StepInfo) => void, ): Promise { const res = await fetch("/api/browse", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, action }), }); if (!res.ok) { const body = await res.json().catch(() => null); return { ok: false, error: body?.error || `HTTP ${res.status}` }; } const reader = res.body?.getReader(); if (!reader) { return { ok: false, error: "No response stream" }; } const decoder = new TextDecoder(); let buffer = ""; let result: BrowseResult = { ok: false, error: "No result received" }; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop() || ""; for (const part of parts) { const eventMatch = part.match(/^event: (\w+)\ndata: ([\s\S]+)$/); if (!eventMatch) continue; const [, event, data] = eventMatch; try { const parsed = JSON.parse(data); if (event === "step") { onStep(parsed as StepInfo); } else if (event === "result") { result = parsed as BrowseResult; } } catch { // skip malformed events } } } return result; } export default function Home() { const isMobile = useIsMobile(); const { theme, toggle: toggleTheme } = useTheme(); const [url, setUrl] = useState(ALLOWED_URLS[0]); const [loading, setLoading] = useState(false); const [action, setAction] = useState("screenshot"); const [result, setResult] = useState(null); const [steps, setSteps] = useState([]); const [envStatus, setEnvStatus] = useState(null); const stepsEndRef = useRef(null); useEffect(() => { getEnvStatus().then(setEnvStatus); }, []); function clearResults() { setResult(null); setSteps([]); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setResult(null); setSteps([]); try { const browseResult = await streamBrowse(url, action, (step) => { setSteps((prev) => { const existing = prev.findIndex((s) => s.step === step.step); if (existing >= 0) { const updated = [...prev]; updated[existing] = step; return updated; } return [...prev, step]; }); }); setResult(browseResult); } catch (err) { const message = err instanceof Error ? err.message : String(err); setResult({ ok: false, error: message }); } finally { setLoading(false); } } useEffect(() => { stepsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [steps]); const controlsForm = (
value={action} onChange={(v) => { setAction(v); clearResults(); }} options={[ { value: "screenshot", label: "Screenshot" }, { value: "snapshot", label: "Snapshot" }, ]} />

{action === "screenshot" ? "Captures a full-page PNG image" : "Returns the accessibility tree"}

{envStatus && !envStatus.sandbox.hasSnapshot && ( Sandbox snapshot not configured Without a sandbox snapshot, the VM installs agent-browser + Chromium on every request (~30s). Create one with{" "} npx tsx scripts/create-snapshot.ts {" "} and set{" "} AGENT_BROWSER_SNAPSHOT_ID {" "} for sub-second startup. )}
); const showSteps = loading || (steps.length > 0 && !result); const hasResult = result && !loading; const resultContent = showSteps ? (
{steps.map((s, i) => ( ))}
) : hasResult ? (
{result.ok && result.screenshot && (

{result.title}

screenshot
{result.title}
Show steps ({steps.length})
{steps.map((s, i) => ( ))}
)} {result.ok && result.snapshot && (

{result.title}

snapshot
            {result.snapshot}
          
Show steps ({steps.length})
{steps.map((s, i) => ( ))}
)} {!result.ok && (
{steps.length > 0 && (
{steps.map((s, i) => ( ))}
)}
)}
) : (

No result yet

Pick a URL and click Run

); return (
agent-browser
Deploy with Vercel
{isMobile ? (
{controlsForm}
{resultContent}
) : (
{resultContent}
)}
); }