// frontend/components/SandboxStatusWidget.jsx // // Always-visible sandbox health pill for the sidebar. Polls // /api/sandbox/status every 30s and surfaces: // // ● MatrixLab Ready (backend up, /health green) // ⚠ MatrixLab unavailable (network/timeout on /health) // ● Local active (using subprocess by choice) // // When degraded, offers two one-click recoveries: // * Repair — opens Settings → Sandbox to run the install/repair flow // * Use Local — flips backend to subprocess via PUT /api/sandbox/config // // Purely informational: failures here never block the chat / planner. import React, { useCallback, useEffect, useRef, useState } from "react"; import { apiUrl } from "../utils/api.js"; const POLL_MS = 30_000; const BACKEND_LABEL = { subprocess: "Local", matrixlab: "MatrixLab", off: "Pass-through", }; export default function SandboxStatusWidget({ onOpenSettings }) { const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [switching, setSwitching] = useState(false); const timerRef = useRef(null); const refresh = useCallback(async () => { try { const res = await fetch(apiUrl("/api/sandbox/status")); const data = await res.json(); if (!res.ok) { setError(data.detail || `HTTP ${res.status}`); return; } setStatus(data); setError(null); } catch (err) { setError(err.message || "Unable to reach sandbox status"); } }, []); useEffect(() => { refresh(); timerRef.current = setInterval(refresh, POLL_MS); return () => clearInterval(timerRef.current); }, [refresh]); const switchToLocal = async () => { setSwitching(true); try { await fetch(apiUrl("/api/sandbox/config"), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ backend: "subprocess" }), }); await refresh(); } finally { setSwitching(false); } }; if (!status && !error) { return (
Sandbox
Checking…
); } if (error) { return (
Sandbox
Status check failed
{error}
); } const backend = status.backend || "subprocess"; const ok = !!status.ok; const label = BACKEND_LABEL[backend] || backend; const dot = ok ? "●" : "⚠"; const dotColor = ok ? "#10B981" : "#f59e0b"; const stateText = ok ? "Ready" : "Unavailable"; return (
Sandbox
{dot} {label} · {stateText}
{status.matrixlab_url && backend === "matrixlab" && (
URL {status.matrixlab_url}
)}
Timeout {status.timeout_sec}s
Network {status.allow_network ? "Enabled" : "Disabled"}
{onOpenSettings && ( )} {!ok && backend === "matrixlab" && ( )}
); } const s = { shell: { padding: "10px 12px", background: "#0d0e17", border: "1px solid #1f2937", borderRadius: 8, fontFamily: "system-ui, sans-serif", fontSize: 12, color: "#e4e4e7", }, titleRow: { display: "flex", justifyContent: "space-between", alignItems: "center" }, title: { fontSize: 10, fontWeight: 700, letterSpacing: "0.06em", color: "#9092b5", textTransform: "uppercase", }, refresh: { background: "transparent", color: "#71717a", border: "none", cursor: "pointer", fontSize: 14, padding: 0, }, statusRow: { marginTop: 6, display: "flex", alignItems: "center" }, metaRow: { marginTop: 4, display: "flex", justifyContent: "space-between" }, metaKey: { color: "#9092b5", fontSize: 11 }, metaVal: { color: "#d4d4d8", fontSize: 11, fontFamily: "ui-monospace, monospace" }, actions: { display: "flex", gap: 6, marginTop: 8 }, action: { flex: 1, padding: "4px 8px", fontSize: 11, background: "transparent", color: "#a1a1aa", border: "1px solid #3F3F46", borderRadius: 4, cursor: "pointer", }, dim: { color: "#71717a", fontSize: 11 }, };