import { Clipboard, Cloud, Download, HelpCircle, MessageSquare, PauseCircle, PlayCircle, Plus, RefreshCw, Send, Shield, Terminal, Zap } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { SandboxDaemonError, createSandboxDaemonClient, type SandboxDaemonClient, type AgentInfo, type AgentModeInfo, type PermissionRequest, type QuestionRequest, type SessionInfo, type UniversalEvent, type UniversalMessage, type UniversalMessagePart } from "sandbox-agent"; type RequestLog = { id: number; method: string; url: string; body?: string; status?: number; time: string; curl: string; error?: string; }; type DebugTab = "log" | "events" | "approvals" | "agents"; const defaultAgents = ["claude", "codex", "opencode", "amp"]; const formatJson = (value: unknown) => { if (value === null || value === undefined) return ""; if (typeof value === "string") return value; try { return JSON.stringify(value, null, 2); } catch { return String(value); } }; const escapeSingleQuotes = (value: string) => value.replace(/'/g, `'\\''`); const buildCurl = (method: string, url: string, body?: string, token?: string) => { const headers: string[] = []; if (token) { headers.push(`-H 'Authorization: Bearer ${escapeSingleQuotes(token)}'`); } if (body) { headers.push(`-H 'Content-Type: application/json'`); } const data = body ? `-d '${escapeSingleQuotes(body)}'` : ""; return `curl -X ${method} ${headers.join(" ")} ${data} '${escapeSingleQuotes(url)}'` .replace(/\s+/g, " ") .trim(); }; const getEventType = (event: UniversalEvent) => { if ("message" in event.data) return "message"; if ("started" in event.data) return "started"; if ("error" in event.data) return "error"; if ("questionAsked" in event.data) return "question"; if ("permissionAsked" in event.data) return "permission"; return "event"; }; const formatTime = (value: string) => { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleTimeString(); }; export default function App() { const [endpoint, setEndpoint] = useState("http://localhost:2468"); const [token, setToken] = useState(""); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [connectError, setConnectError] = useState(null); const [agents, setAgents] = useState([]); const [modesByAgent, setModesByAgent] = useState>({}); const [sessions, setSessions] = useState([]); const [agentId, setAgentId] = useState("claude"); const [agentMode, setAgentMode] = useState(""); const [permissionMode, setPermissionMode] = useState("default"); const [model, setModel] = useState(""); const [variant, setVariant] = useState(""); const [sessionId, setSessionId] = useState(""); const [sessionError, setSessionError] = useState(null); const [message, setMessage] = useState(""); const [events, setEvents] = useState([]); const [offset, setOffset] = useState(0); const offsetRef = useRef(0); const [polling, setPolling] = useState(false); const pollTimerRef = useRef(null); const [streamMode, setStreamMode] = useState<"poll" | "sse">("poll"); const [eventError, setEventError] = useState(null); const [questionSelections, setQuestionSelections] = useState>({}); const [questionStatus, setQuestionStatus] = useState>({}); const [permissionStatus, setPermissionStatus] = useState>({}); const [requestLog, setRequestLog] = useState([]); const logIdRef = useRef(1); const [copiedLogId, setCopiedLogId] = useState(null); const [debugTab, setDebugTab] = useState("events"); const messagesEndRef = useRef(null); const clientRef = useRef(null); const sseAbortRef = useRef(null); const logRequest = useCallback((entry: RequestLog) => { setRequestLog((prev) => { const next = [entry, ...prev]; return next.slice(0, 200); }); }, []); const createClient = useCallback(() => { const fetchWithLog: typeof fetch = async (input, init) => { const method = init?.method ?? "GET"; const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const bodyText = typeof init?.body === "string" ? init.body : undefined; const curl = buildCurl(method, url, bodyText, token); const logId = logIdRef.current++; const entry: RequestLog = { id: logId, method, url, body: bodyText, time: new Date().toLocaleTimeString(), curl }; let logged = false; try { const response = await fetch(input, init); logRequest({ ...entry, status: response.status }); logged = true; return response; } catch (error) { const message = error instanceof Error ? error.message : "Request failed"; if (!logged) { logRequest({ ...entry, status: 0, error: message }); } throw error; } }; const client = createSandboxDaemonClient({ baseUrl: endpoint, token: token || undefined, fetch: fetchWithLog }); clientRef.current = client; return client; }, [endpoint, token, logRequest]); const getClient = useCallback((): SandboxDaemonClient => { if (!clientRef.current) { throw new Error("Not connected"); } return clientRef.current; }, []); const getErrorMessage = (error: unknown, fallback: string) => { if (error instanceof SandboxDaemonError) { return error.problem?.detail ?? error.problem?.title ?? error.message; } return error instanceof Error ? error.message : fallback; }; const connect = async () => { setConnecting(true); setConnectError(null); try { const client = createClient(); await client.getHealth(); setConnected(true); await refreshAgents(); await fetchSessions(); } catch (error) { const message = getErrorMessage(error, "Unable to connect"); setConnectError(message); setConnected(false); clientRef.current = null; } finally { setConnecting(false); } }; const disconnect = () => { setConnected(false); clientRef.current = null; setSessionError(null); setEvents([]); setOffset(0); offsetRef.current = 0; setEventError(null); stopPolling(); stopSse(); }; const refreshAgents = async () => { try { const data = await getClient().listAgents(); const agentList = data.agents ?? []; setAgents(agentList); // Auto-load modes for installed agents for (const agent of agentList) { if (agent.installed) { loadModes(agent.id); } } } catch (error) { setConnectError(getErrorMessage(error, "Unable to refresh agents")); } }; const fetchSessions = async () => { try { const data = await getClient().listSessions(); const sessionList = data.sessions ?? []; setSessions(sessionList); } catch { // Silently fail - sessions list is supplementary } }; const installAgent = async (targetId: string, reinstall: boolean) => { try { await getClient().installAgent(targetId, { reinstall }); await refreshAgents(); } catch (error) { setConnectError(getErrorMessage(error, "Install failed")); } }; const loadModes = async (targetId: string) => { try { const data = await getClient().getAgentModes(targetId); const modes = data.modes ?? []; setModesByAgent((prev) => ({ ...prev, [targetId]: modes })); } catch { // Silently fail - modes are optional } }; const sendMessage = async () => { if (!message.trim()) return; setSessionError(null); try { await getClient().postMessage(sessionId, { message }); setMessage(""); // Auto-start polling if not already if (!polling) { if (streamMode === "poll") { startPolling(); } else { startSse(); } } } catch (error) { setSessionError(getErrorMessage(error, "Unable to send message")); } }; const createSession = async () => { setSessionError(null); try { const body: { agent: string; agentMode?: string; permissionMode?: string; model?: string; variant?: string; } = { agent: agentId }; if (agentMode) body.agentMode = agentMode; if (permissionMode) body.permissionMode = permissionMode; if (model) body.model = model; if (variant) body.variant = variant; await getClient().createSession(sessionId, body); await fetchSessions(); } catch (error) { setSessionError(getErrorMessage(error, "Unable to create session")); } }; const selectSession = (session: SessionInfo) => { setSessionId(session.sessionId); setAgentId(session.agent); setAgentMode(session.agentMode); setPermissionMode(session.permissionMode); setModel(session.model ?? ""); setVariant(session.variant ?? ""); // Reset events and offset when switching sessions setEvents([]); setOffset(0); offsetRef.current = 0; setSessionError(null); }; const createNewSession = async () => { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; let id = "session-"; for (let i = 0; i < 8; i++) { id += chars[Math.floor(Math.random() * chars.length)]; } setSessionId(id); setEvents([]); setOffset(0); offsetRef.current = 0; setSessionError(null); // Create the session try { const body: { agent: string; agentMode?: string; permissionMode?: string; model?: string; variant?: string; } = { agent: agentId }; if (agentMode) body.agentMode = agentMode; if (permissionMode) body.permissionMode = permissionMode; if (model) body.model = model; if (variant) body.variant = variant; await getClient().createSession(id, body); await fetchSessions(); } catch (error) { setSessionError(getErrorMessage(error, "Unable to create session")); } }; const appendEvents = useCallback((incoming: UniversalEvent[]) => { if (!incoming.length) return; setEvents((prev) => [...prev, ...incoming]); const lastId = incoming[incoming.length - 1]?.id ?? offsetRef.current; offsetRef.current = lastId; setOffset(lastId); }, []); const fetchEvents = useCallback(async () => { if (!sessionId) return; try { const response = await getClient().getEvents(sessionId, { offset: offsetRef.current, limit: 200 }); const newEvents = response.events ?? []; appendEvents(newEvents); setEventError(null); } catch (error) { setEventError(getErrorMessage(error, "Unable to fetch events")); } }, [appendEvents, getClient, sessionId]); const startPolling = () => { stopSse(); if (pollTimerRef.current) return; setPolling(true); fetchEvents(); pollTimerRef.current = window.setInterval(fetchEvents, 2500); }; const stopPolling = () => { if (pollTimerRef.current) { window.clearInterval(pollTimerRef.current); pollTimerRef.current = null; } setPolling(false); }; const startSse = () => { stopPolling(); if (sseAbortRef.current) return; if (!sessionId) { setEventError("Select or create a session first."); return; } setEventError(null); setPolling(true); const controller = new AbortController(); sseAbortRef.current = controller; const start = async () => { try { for await (const event of getClient().streamEvents( sessionId, { offset: offsetRef.current }, controller.signal )) { appendEvents([event]); } } catch (error) { if (controller.signal.aborted) { return; } setEventError(getErrorMessage(error, "SSE connection error. Falling back to polling.")); stopSse(); startPolling(); } finally { if (sseAbortRef.current === controller) { sseAbortRef.current = null; setPolling(false); } } }; void start(); }; const stopSse = () => { if (sseAbortRef.current) { sseAbortRef.current.abort(); sseAbortRef.current = null; } setPolling(false); }; const resetEvents = () => { setEvents([]); setOffset(0); offsetRef.current = 0; }; const handleCopy = async (entry: RequestLog) => { try { await navigator.clipboard.writeText(entry.curl); setCopiedLogId(entry.id); window.setTimeout(() => setCopiedLogId(null), 1500); } catch { setCopiedLogId(null); } }; const toggleQuestionOption = ( requestId: string, questionIndex: number, optionLabel: string, multiSelect: boolean ) => { setQuestionSelections((prev) => { const next = { ...prev }; const currentAnswers = next[requestId] ? [...next[requestId]] : []; const selections = currentAnswers[questionIndex] ? [...currentAnswers[questionIndex]] : []; if (multiSelect) { if (selections.includes(optionLabel)) { currentAnswers[questionIndex] = selections.filter((label) => label !== optionLabel); } else { currentAnswers[questionIndex] = [...selections, optionLabel]; } } else { currentAnswers[questionIndex] = [optionLabel]; } next[requestId] = currentAnswers; return next; }); }; const answerQuestion = async (request: QuestionRequest) => { const answers = questionSelections[request.id] ?? []; try { await getClient().replyQuestion(sessionId, request.id, { answers }); setQuestionStatus((prev) => ({ ...prev, [request.id]: "replied" })); } catch (error) { setEventError(getErrorMessage(error, "Unable to reply")); } }; const rejectQuestion = async (requestId: string) => { try { await getClient().rejectQuestion(sessionId, requestId); setQuestionStatus((prev) => ({ ...prev, [requestId]: "rejected" })); } catch (error) { setEventError(getErrorMessage(error, "Unable to reject")); } }; const replyPermission = async (requestId: string, reply: "once" | "always" | "reject") => { try { await getClient().replyPermission(sessionId, requestId, { reply }); setPermissionStatus((prev) => ({ ...prev, [requestId]: "replied" })); } catch (error) { setEventError(getErrorMessage(error, "Unable to reply")); } }; const questionRequests = useMemo(() => { return events .filter((event) => "questionAsked" in event.data) .map((event) => (event.data as { questionAsked: QuestionRequest }).questionAsked) .filter((request) => !questionStatus[request.id]); }, [events, questionStatus]); const permissionRequests = useMemo(() => { return events .filter((event) => "permissionAsked" in event.data) .map((event) => (event.data as { permissionAsked: PermissionRequest }).permissionAsked) .filter((request) => !permissionStatus[request.id]); }, [events, permissionStatus]); const transcriptMessages = useMemo(() => { return events .filter((event): event is UniversalEvent & { data: { message: UniversalMessage } } => "message" in event.data) .map((event) => { const msg = event.data.message; const parts = "parts" in msg ? msg.parts : []; const content = parts .filter((part: UniversalMessagePart) => part.type === "text" && part.text) .map((part: UniversalMessagePart) => part.text) .join("\n"); return { id: event.id, role: "role" in msg ? msg.role : "assistant", content, timestamp: event.timestamp }; }) .filter((msg) => msg.content); }, [events]); useEffect(() => { return () => { stopPolling(); stopSse(); }; }, []); useEffect(() => { if (!connected) return; refreshAgents(); }, [connected]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [transcriptMessages]); // Auto-load modes when agent changes useEffect(() => { if (connected && agentId && !modesByAgent[agentId]) { loadModes(agentId); } }, [connected, agentId]); // Set default mode when modes are loaded useEffect(() => { const modes = modesByAgent[agentId]; if (modes && modes.length > 0 && !agentMode) { setAgentMode(modes[0].id); } }, [modesByAgent, agentId]); const availableAgents = agents.length ? agents.map((agent) => agent.id) : defaultAgents; const currentAgent = agents.find((a) => a.id === agentId); const activeModes = modesByAgent[agentId] ?? []; const pendingApprovals = questionRequests.length + permissionRequests.length; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const toggleStream = () => { if (polling) { if (streamMode === "poll") { stopPolling(); } else { stopSse(); } } else if (streamMode === "poll") { startPolling(); } else { startSse(); } }; if (!connected) { return (
SA
Sandbox Agent
SA

Sandbox Agent

Universal API for running Claude Code, Codex, OpenCode, and Amp inside sandboxes.

Connect to Daemon
{connectError && (
{connectError}
)}

Start the daemon with CORS enabled for browser access:
sandbox-agent --cors-allow-origin http://localhost:5173

); } return (
SA
Sandbox Agent
{endpoint}
{/* Session Sidebar */}
Sessions
{sessions.length === 0 ? (
No sessions yet.
) : ( sessions.map((session) => ( )) )}
{/* Chat Panel */}
Session {sessionId && {sessionId}}
{polling && ( Live )}
{!sessionId ? (
No Session Selected

Create a new session to start chatting with an agent.

) : transcriptMessages.length === 0 && !sessionError ? (
Ready to Chat

Send a message to start a conversation with the agent.

) : (
{transcriptMessages.map((msg) => (
{msg.role === "user" ? "U" : "AI"}
{msg.content}
))} {sessionError && (
{sessionError}
)}
)}
{/* Input Area */}