import { useCallback, useState } from 'react'; import { Alert, Box, IconButton, Typography, CircularProgress, Divider, } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; import { useSessionStore } from '@/store/sessionStore'; import { useAgentStore } from '@/store/agentStore'; import { apiFetch } from '@/utils/api'; interface SessionSidebarProps { onClose?: () => void; } export default function SessionSidebar({ onClose }: SessionSidebarProps) { const { sessions, activeSessionId, createSession, deleteSession, switchSession } = useSessionStore(); const { setPlan, clearPanel } = useAgentStore(); const [isCreatingSession, setIsCreatingSession] = useState(false); const [capacityError, setCapacityError] = useState(null); // -- Handlers ----------------------------------------------------------- const handleNewSession = useCallback(async () => { if (isCreatingSession) return; setIsCreatingSession(true); setCapacityError(null); try { const response = await apiFetch('/api/session', { method: 'POST' }); if (response.status === 503) { const data = await response.json(); setCapacityError(data.detail || 'Server is at capacity.'); return; } const data = await response.json(); createSession(data.session_id); setPlan([]); clearPanel(); onClose?.(); } catch { setCapacityError('Failed to create session.'); } finally { setIsCreatingSession(false); } }, [isCreatingSession, createSession, setPlan, clearPanel, onClose]); const handleDelete = useCallback( async (sessionId: string, e: React.MouseEvent) => { e.stopPropagation(); useAgentStore.getState().clearSessionState(sessionId); try { await apiFetch(`/api/session/${sessionId}`, { method: 'DELETE' }); deleteSession(sessionId); } catch { deleteSession(sessionId); } }, [deleteSession], ); const handleSelect = useCallback( (sessionId: string) => { switchSession(sessionId); // Per-session state (plan, panel, activity) is restored automatically // by SessionChat's useEffect when isActive flips to true. onClose?.(); }, [switchSession, onClose], ); const formatTime = (d: string) => new Date(d).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); // -- Render ------------------------------------------------------------- return ( {/* -- Header -------------------------------------------------------- */} Recent chats {/* -- Capacity error ------------------------------------------------ */} {capacityError && ( setCapacityError(null)} sx={{ m: 1, fontSize: '0.7rem', py: 0.25, '& .MuiAlert-message': { py: 0 }, borderColor: '#FF9D00', color: 'var(--text)', }} > {capacityError} )} {/* -- Session list -------------------------------------------------- */} {sessions.length === 0 ? ( No sessions yet ) : ( [...sessions].reverse().map((session, index) => { const num = sessions.length - index; const isSelected = session.id === activeSessionId; return ( handleSelect(session.id)} sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 1.5, py: 0.875, mx: 0.75, borderRadius: '10px', cursor: 'pointer', transition: 'background-color 0.12s ease', bgcolor: isSelected ? 'var(--hover-bg)' : 'transparent', '&:hover': { bgcolor: 'var(--hover-bg)', }, '& .delete-btn': { opacity: 0, transition: 'opacity 0.12s', }, '&:hover .delete-btn': { opacity: 1, }, }} > {session.title.startsWith('Chat ') ? `Session ${String(num).padStart(2, '0')}` : session.title} {formatTime(session.createdAt)} {/* Attention badge — pulsing dot when background session needs approval */} {session.needsAttention && !isSelected && ( )} handleDelete(session.id, e)} sx={{ color: 'var(--muted-text)', width: 26, height: 26, flexShrink: 0, '&:hover': { color: 'var(--accent-red)', bgcolor: 'rgba(244,67,54,0.08)' }, }} > ); }) )} {/* -- Footer: New Task + status ------------------------------------- */} {isCreatingSession ? ( <> Creating... ) : ( <> New Task )} ); }