import { Archive, ArrowLeft, ArrowUpRight, Plus, RefreshCw } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { AgentInfo } from "sandbox-agent"; import { formatShortId } from "../utils/format"; type AgentModeInfo = { id: string; name: string; description: string }; type AgentModelInfo = { id: string; name?: string }; import SessionCreateMenu, { type SessionConfig } from "./SessionCreateMenu"; type SessionListItem = { sessionId: string; agent: string; ended: boolean; archived: boolean; }; const agentLabels: Record = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", amp: "Amp", pi: "Pi", cursor: "Cursor", }; const persistenceDocsUrl = "https://sandboxagent.dev/docs/session-persistence"; const MIN_REFRESH_SPIN_MS = 350; const SessionSidebar = ({ sessions, selectedSessionId, onSelectSession, onRefresh, onCreateSession, onSelectAgent, agents, agentsLoading, agentsError, sessionsLoading, sessionsError, modesByAgent, modelsByAgent, defaultModelByAgent, }: { sessions: SessionListItem[]; selectedSessionId: string; onSelectSession: (session: SessionListItem) => void; onRefresh: () => void; onCreateSession: (agentId: string, config: SessionConfig) => Promise; onSelectAgent: (agentId: string) => Promise; agents: AgentInfo[]; agentsLoading: boolean; agentsError: string | null; sessionsLoading: boolean; sessionsError: string | null; modesByAgent: Record; modelsByAgent: Record; defaultModelByAgent: Record; }) => { const [showMenu, setShowMenu] = useState(false); const [showArchived, setShowArchived] = useState(false); const [refreshing, setRefreshing] = useState(false); const menuRef = useRef(null); const archivedCount = sessions.filter((session) => session.archived).length; const activeSessions = sessions.filter((session) => !session.archived); const archivedSessions = sessions.filter((session) => session.archived); const visibleSessions = showArchived ? archivedSessions : activeSessions; const orderedVisibleSessions = showArchived ? [...visibleSessions].sort((a, b) => Number(a.ended) - Number(b.ended)) : visibleSessions; useEffect(() => { if (!showMenu) return; const handler = (event: MouseEvent) => { if (!menuRef.current) return; if (!menuRef.current.contains(event.target as Node)) { setShowMenu(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [showMenu]); useEffect(() => { // Prevent getting stuck in archived view when there are no archived sessions. if (!showArchived) return; if (archivedSessions.length === 0) { setShowArchived(false); } }, [showArchived, archivedSessions.length]); const handleRefresh = async () => { if (refreshing) return; const startedAt = Date.now(); setRefreshing(true); try { await Promise.resolve(onRefresh()); } finally { const elapsedMs = Date.now() - startedAt; if (elapsedMs < MIN_REFRESH_SPIN_MS) { await new Promise((resolve) => window.setTimeout(resolve, MIN_REFRESH_SPIN_MS - elapsedMs)); } setRefreshing(false); } }; return (
Sessions
{archivedCount > 0 && ( )}
setShowMenu(false)} />
{sessionsLoading ? (
Loading sessions...
) : sessionsError ? (
{sessionsError}
) : visibleSessions.length === 0 ? (
{showArchived ? "No archived sessions." : "No sessions yet."}
) : ( <> {showArchived &&
Archived Sessions
} {orderedVisibleSessions.map((session) => (
))} )}
Sessions are persisted in your browser using IndexedDB. These sessions are only from your browser; your SDK sessions are separate. Adding inspector support for SDK soon.{" "} Configure persistence
); }; export default SessionSidebar;