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/delete-session-dialog"; import { DeleteAllArchivedSessionsDialog } from "@/components/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 */}
); }