import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { HotkeyButton } from '@/components/ui/hotkey-button'; import { Input } from '@/components/ui/input'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Plus, MessageSquare, Archive, Trash2, Edit2, Check, X, ArchiveRestore, Loader2, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { SessionListItem } from '@/types/electron'; import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; import { getElectronAPI } from '@/lib/electron'; import { DeleteSessionDialog } from '@/components/dialogs/delete-session-dialog'; import { DeleteAllArchivedSessionsDialog } from '@/components/dialogs/delete-all-archived-sessions-dialog'; // Random session name generator const adjectives = [ 'Swift', 'Bright', 'Clever', 'Dynamic', 'Eager', 'Focused', 'Gentle', 'Happy', 'Inventive', 'Jolly', 'Keen', 'Lively', 'Mighty', 'Noble', 'Optimal', 'Peaceful', 'Quick', 'Radiant', 'Smart', 'Tranquil', 'Unique', 'Vibrant', 'Wise', 'Zealous', ]; const nouns = [ 'Agent', 'Builder', 'Coder', 'Developer', 'Explorer', 'Forge', 'Garden', 'Helper', 'Innovator', 'Journey', 'Kernel', 'Lighthouse', 'Mission', 'Navigator', 'Oracle', 'Project', 'Quest', 'Runner', 'Spark', 'Task', 'Unicorn', 'Voyage', 'Workshop', ]; function generateRandomSessionName(): string { const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]; const noun = nouns[Math.floor(Math.random() * nouns.length)]; const number = Math.floor(Math.random() * 100); return `${adjective} ${noun} ${number}`; } interface SessionManagerProps { currentSessionId: string | null; onSelectSession: (sessionId: string | null) => void; projectPath: string; isCurrentSessionThinking?: boolean; onQuickCreateRef?: React.MutableRefObject<(() => Promise) | null>; } export function SessionManager({ currentSessionId, onSelectSession, projectPath, isCurrentSessionThinking = false, onQuickCreateRef, }: SessionManagerProps) { const shortcuts = useKeyboardShortcutsConfig(); const [sessions, setSessions] = useState([]); const [activeTab, setActiveTab] = useState<'active' | 'archived'>('active'); const [editingSessionId, setEditingSessionId] = useState(null); const [editingName, setEditingName] = useState(''); const [isCreating, setIsCreating] = useState(false); const [newSessionName, setNewSessionName] = useState(''); const [runningSessions, setRunningSessions] = useState>(new Set()); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [sessionToDelete, setSessionToDelete] = useState(null); const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false); // Check running state for all sessions const checkRunningSessions = async (sessionList: SessionListItem[]) => { const api = getElectronAPI(); if (!api?.agent) return; const runningIds = new Set(); // Check each session's running state for (const session of sessionList) { try { const result = await api.agent.getHistory(session.id); if (result.success && result.isRunning) { runningIds.add(session.id); } } catch (err) { // Ignore errors for individual session checks console.warn(`[SessionManager] Failed to check running state for ${session.id}:`, err); } } setRunningSessions(runningIds); }; // Load sessions const loadSessions = async () => { const api = getElectronAPI(); if (!api?.sessions) return; // Always load all sessions and filter client-side const result = await api.sessions.list(true); if (result.success && result.sessions) { setSessions(result.sessions); // Check running state for all sessions await checkRunningSessions(result.sessions); } }; useEffect(() => { loadSessions(); }, []); // Periodically check running state for sessions (useful for detecting when agents finish) useEffect(() => { // Only poll if there are running sessions if (runningSessions.size === 0 && !isCurrentSessionThinking) return; const interval = setInterval(async () => { if (sessions.length > 0) { await checkRunningSessions(sessions); } }, 3000); // Check every 3 seconds return () => clearInterval(interval); }, [sessions, runningSessions.size, isCurrentSessionThinking]); // Create new session with random name const handleCreateSession = async () => { const api = getElectronAPI(); if (!api?.sessions) return; const sessionName = newSessionName.trim() || generateRandomSessionName(); const result = await api.sessions.create(sessionName, projectPath, projectPath); if (result.success && result.session?.id) { setNewSessionName(''); setIsCreating(false); await loadSessions(); onSelectSession(result.session.id); } }; // Create new session directly with a random name (one-click) const handleQuickCreateSession = async () => { const api = getElectronAPI(); if (!api?.sessions) return; const sessionName = generateRandomSessionName(); const result = await api.sessions.create(sessionName, projectPath, projectPath); if (result.success && result.session?.id) { await loadSessions(); onSelectSession(result.session.id); } }; // Expose the quick create function via ref for keyboard shortcuts useEffect(() => { if (onQuickCreateRef) { onQuickCreateRef.current = handleQuickCreateSession; } return () => { if (onQuickCreateRef) { onQuickCreateRef.current = null; } }; }, [onQuickCreateRef, projectPath]); // Rename session const handleRenameSession = async (sessionId: string) => { const api = getElectronAPI(); if (!editingName.trim() || !api?.sessions) return; const result = await api.sessions.update(sessionId, editingName, undefined); if (result.success) { setEditingSessionId(null); setEditingName(''); await loadSessions(); } }; // Archive session const handleArchiveSession = async (sessionId: string) => { const api = getElectronAPI(); if (!api?.sessions) { console.error('[SessionManager] Sessions API not available'); return; } try { const result = await api.sessions.archive(sessionId); if (result.success) { // If the archived session was currently selected, deselect it if (currentSessionId === sessionId) { onSelectSession(null); } await loadSessions(); } else { console.error('[SessionManager] Archive failed:', result.error); } } catch (error) { console.error('[SessionManager] Archive error:', error); } }; // Unarchive session const handleUnarchiveSession = async (sessionId: string) => { const api = getElectronAPI(); if (!api?.sessions) { console.error('[SessionManager] Sessions API not available'); return; } try { const result = await api.sessions.unarchive(sessionId); if (result.success) { await loadSessions(); } else { console.error('[SessionManager] Unarchive failed:', result.error); } } catch (error) { console.error('[SessionManager] Unarchive error:', error); } }; // Open delete session dialog const handleDeleteSession = (session: SessionListItem) => { setSessionToDelete(session); setIsDeleteDialogOpen(true); }; // Confirm delete session const confirmDeleteSession = async (sessionId: string) => { const api = getElectronAPI(); if (!api?.sessions) return; const result = await api.sessions.delete(sessionId); if (result.success) { await loadSessions(); if (currentSessionId === sessionId) { // Switch to another session or create a new one const activeSessionsList = sessions.filter((s) => !s.isArchived); if (activeSessionsList.length > 0) { onSelectSession(activeSessionsList[0].id); } } } setSessionToDelete(null); }; // Delete all archived sessions const handleDeleteAllArchivedSessions = async () => { const api = getElectronAPI(); if (!api?.sessions) return; // Delete each archived session for (const session of archivedSessions) { await api.sessions.delete(session.id); } await loadSessions(); setIsDeleteAllArchivedDialogOpen(false); }; const activeSessions = sessions.filter((s) => !s.isArchived); const archivedSessions = sessions.filter((s) => s.isArchived); const displayedSessions = activeTab === 'active' ? activeSessions : archivedSessions; return (
Agent Sessions { // Switch to active tab if on archived tab if (activeTab === 'archived') { setActiveTab('active'); } handleQuickCreateSession(); }} hotkey={shortcuts.newSession} hotkeyActive={false} data-testid="new-session-button" title={`New Session (${shortcuts.newSession})`} > New
setActiveTab(value as 'active' | 'archived')} className="w-full" > Active ({activeSessions.length}) Archived ({archivedSessions.length})
{/* Create new session */} {isCreating && (
setNewSessionName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleCreateSession(); if (e.key === 'Escape') { setIsCreating(false); setNewSessionName(''); } }} autoFocus />
)} {/* Delete All Archived button - shown at the top of archived sessions */} {activeTab === 'archived' && archivedSessions.length > 0 && (
)} {/* Session list */} {displayedSessions.map((session) => (
!session.isArchived && onSelectSession(session.id)} data-testid={`session-item-${session.id}`} >
{editingSessionId === session.id ? (
setEditingName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleRenameSession(session.id); if (e.key === 'Escape') { setEditingSessionId(null); setEditingName(''); } }} onClick={(e) => e.stopPropagation()} autoFocus className="h-7" />
) : ( <>
{/* Show loading indicator if this session is running (either current session thinking or any session in runningSessions) */} {(currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id) ? ( ) : ( )}

{session.name}

{((currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id)) && ( thinking... )}
{session.preview && (

{session.preview}

)}
{session.messageCount} messages ยท {new Date(session.updatedAt).toLocaleDateString()}
)}
{/* Actions */} {!session.isArchived && (
e.stopPropagation()}>
)} {session.isArchived && (
e.stopPropagation()}>
)}
))} {displayedSessions.length === 0 && (

{activeTab === 'active' ? 'No active sessions' : 'No archived sessions'}

{activeTab === 'active' ? 'Create your first session to get started' : 'Archive sessions to see them here'}

)}
{/* Delete Session Confirmation Dialog */} {/* Delete All Archived Sessions Confirmation Dialog */}
); }