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"; const API_PREFIX = "/v1"; type AgentInfo = { id: string; installed: boolean; version?: string; path?: string; }; type SessionInfo = { sessionId: string; agent: string; agentMode: string; permissionMode: string; model?: string; variant?: string; agentSessionId?: string; ended: boolean; eventCount: number; }; type AgentMode = { id: string; name: string; description?: string; }; type UniversalEvent = { id: number; timestamp: string; sessionId: string; agent: string; agentSessionId?: string; data: UniversalEventData; }; type UniversalEventData = | { message: UniversalMessage } | { started: StartedInfo } | { error: CrashInfo } | { questionAsked: QuestionRequest } | { permissionAsked: PermissionRequest }; type UniversalMessagePart = { type: string; text?: string; name?: string; input?: unknown; output?: unknown; }; type UniversalMessage = { role?: string; parts?: UniversalMessagePart[]; raw?: unknown; error?: string; }; type StartedInfo = { message?: string; pid?: number; [key: string]: unknown; }; type CrashInfo = { message?: string; code?: string; detail?: string; [key: string]: unknown; }; type QuestionOption = { label: string; description?: string; }; type QuestionItem = { header?: string; question: string; options: QuestionOption[]; multiSelect?: boolean; }; type QuestionRequest = { id: string; sessionID?: string; questions: QuestionItem[]; tool?: { messageID?: string; callID?: string }; }; type PermissionRequest = { id: string; sessionID?: string; permission: string; patterns?: string[]; metadata?: Record; always?: string[]; tool?: { messageID?: string; callID?: string }; }; 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 buildUrl = (endpoint: string, path: string, query?: Record) => { const base = endpoint.replace(/\/$/, ""); const fullPath = path.startsWith("/") ? path : `/${path}`; const url = new URL(`${base}${fullPath}`); if (query) { Object.entries(query).forEach(([key, value]) => { if (value !== "") { url.searchParams.set(key, value); } }); } return url.toString(); }; const safeJson = (text: string) => { if (!text) return null; try { return JSON.parse(text); } catch { return text; } }; 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 eventSourceRef = useRef(null); 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 logRequest = useCallback((entry: RequestLog) => { setRequestLog((prev) => { const next = [entry, ...prev]; return next.slice(0, 200); }); }, []); const apiFetch = useCallback( async ( path: string, options?: { method?: string; body?: unknown; query?: Record; } ) => { const method = options?.method ?? "GET"; const url = buildUrl(endpoint, path, options?.query); const bodyText = options?.body ? JSON.stringify(options.body) : undefined; const headers: Record = {}; if (bodyText) { headers["Content-Type"] = "application/json"; } if (token) { headers.Authorization = `Bearer ${token}`; } 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(url, { method, headers, body: bodyText }); const text = await response.text(); const data = safeJson(text); logRequest({ ...entry, status: response.status }); logged = true; if (!response.ok) { const errorMessage = (typeof data === "object" && data && "detail" in data && data.detail) || (typeof data === "object" && data && "title" in data && data.title) || (typeof data === "string" ? data : `Request failed with ${response.status}`); throw new Error(String(errorMessage)); } return data; } catch (error) { const message = error instanceof Error ? error.message : "Request failed"; if (!logged) { logRequest({ ...entry, status: 0, error: message }); } throw error; } }, [endpoint, token, logRequest] ); const connect = async () => { setConnecting(true); setConnectError(null); try { await apiFetch(`${API_PREFIX}/health`); setConnected(true); await refreshAgents(); await fetchSessions(); } catch (error) { const message = error instanceof Error ? error.message : "Unable to connect"; setConnectError(message); setConnected(false); } finally { setConnecting(false); } }; const disconnect = () => { setConnected(false); setSessionError(null); setEvents([]); setOffset(0); offsetRef.current = 0; setEventError(null); stopPolling(); stopSse(); }; const refreshAgents = async () => { try { const data = await apiFetch(`${API_PREFIX}/agents`); const agentList = (data as { agents?: AgentInfo[] })?.agents ?? []; setAgents(agentList); // Auto-load modes for installed agents for (const agent of agentList) { if (agent.installed) { loadModes(agent.id); } } } catch (error) { setConnectError(error instanceof Error ? error.message : "Unable to refresh agents"); } }; const fetchSessions = async () => { try { const data = await apiFetch(`${API_PREFIX}/sessions`); const sessionList = (data as { sessions?: SessionInfo[] })?.sessions ?? []; setSessions(sessionList); } catch { // Silently fail - sessions list is supplementary } }; const installAgent = async (targetId: string, reinstall: boolean) => { try { await apiFetch(`${API_PREFIX}/agents/${targetId}/install`, { method: "POST", body: { reinstall } }); await refreshAgents(); } catch (error) { setConnectError(error instanceof Error ? error.message : "Install failed"); } }; const loadModes = async (targetId: string) => { try { const data = await apiFetch(`${API_PREFIX}/agents/${targetId}/modes`); const modes = (data as { modes?: AgentMode[] })?.modes ?? []; setModesByAgent((prev) => ({ ...prev, [targetId]: modes })); } catch { // Silently fail - modes are optional } }; const sendMessage = async () => { if (!message.trim()) return; setSessionError(null); try { await apiFetch(`${API_PREFIX}/sessions/${sessionId}/messages`, { method: "POST", body: { message } }); setMessage(""); // Auto-start polling if not already if (!polling && streamMode === "poll") { startPolling(); } } catch (error) { setSessionError(error instanceof Error ? error.message : "Unable to send message"); } }; const createSession = async () => { setSessionError(null); try { const body: Record = { agent: agentId }; if (agentMode) body.agentMode = agentMode; if (permissionMode) body.permissionMode = permissionMode; if (model) body.model = model; if (variant) body.variant = variant; await apiFetch(`${API_PREFIX}/sessions/${sessionId}`, { method: "POST", body }); await fetchSessions(); } catch (error) { setSessionError(error instanceof Error ? error.message : "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: Record = { agent: agentId }; if (agentMode) body.agentMode = agentMode; if (permissionMode) body.permissionMode = permissionMode; if (model) body.model = model; if (variant) body.variant = variant; await apiFetch(`${API_PREFIX}/sessions/${id}`, { method: "POST", body }); await fetchSessions(); } catch (error) { setSessionError(error instanceof Error ? error.message : "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 data = await apiFetch(`${API_PREFIX}/sessions/${sessionId}/events`, { query: { offset: String(offsetRef.current), limit: "200" } }); const response = data as { events?: UniversalEvent[]; hasMore?: boolean }; const newEvents = response.events ?? []; appendEvents(newEvents); setEventError(null); } catch (error) { setEventError(error instanceof Error ? error.message : "Unable to fetch events"); } }, [apiFetch, appendEvents, 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 (eventSourceRef.current) return; if (token) { setEventError("SSE streams cannot send auth headers. Use polling or run daemon with --no-token."); return; } const url = buildUrl(endpoint, `${API_PREFIX}/sessions/${sessionId}/events/sse`, { offset: String(offsetRef.current) }); const source = new EventSource(url); eventSourceRef.current = source; source.onmessage = (event) => { try { const parsed = safeJson(event.data); if (Array.isArray(parsed)) { appendEvents(parsed as UniversalEvent[]); } else if (parsed && typeof parsed === "object") { appendEvents([parsed as UniversalEvent]); } } catch (error) { setEventError(error instanceof Error ? error.message : "SSE parse error"); } }; source.onerror = () => { setEventError("SSE connection error. Falling back to polling."); stopSse(); }; }; const stopSse = () => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } }; 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 apiFetch(`${API_PREFIX}/sessions/${sessionId}/questions/${request.id}/reply`, { method: "POST", body: { answers } }); setQuestionStatus((prev) => ({ ...prev, [request.id]: "replied" })); } catch (error) { setEventError(error instanceof Error ? error.message : "Unable to reply"); } }; const rejectQuestion = async (requestId: string) => { try { await apiFetch(`${API_PREFIX}/sessions/${sessionId}/questions/${requestId}/reject`, { method: "POST", body: {} }); setQuestionStatus((prev) => ({ ...prev, [requestId]: "rejected" })); } catch (error) { setEventError(error instanceof Error ? error.message : "Unable to reject"); } }; const replyPermission = async (requestId: string, reply: "once" | "always" | "reject") => { try { await apiFetch(`${API_PREFIX}/sessions/${sessionId}/permissions/${requestId}/reply`, { method: "POST", body: { reply } }); setPermissionStatus((prev) => ({ ...prev, [requestId]: "replied" })); } catch (error) { setEventError(error instanceof Error ? error.message : "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; // Extract text from parts array const content = msg?.parts ?.filter((part) => part.type === "text" && part.text) .map((part) => part.text) .join("\n") ?? ""; return { id: event.id, role: 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) { stopPolling(); } else if (streamMode === "poll") { startPolling(); } else { startSse(); } }; if (!connected) { return (
SA
Sandbox Agent
Disconnected
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 */}