import { useCallback, useEffect, useMemo, useState } from 'react'; import type { TFunction } from 'i18next'; import { api } from '../../../utils/api'; import type { Project, ProjectSession, SessionProvider } from '../../../types/app'; import type { AdditionalSessionsByProject, DeleteProjectConfirmation, LoadingSessionsByProject, ProjectSortOrder, SessionDeleteConfirmation, SessionWithProvider, } from '../types/types'; import { filterProjects, getAllSessions, loadStarredProjects, persistStarredProjects, readProjectSortOrder, sortProjects, } from '../utils/utils'; type UseSidebarControllerArgs = { projects: Project[]; selectedProject: Project | null; selectedSession: ProjectSession | null; isLoading: boolean; isMobile: boolean; t: TFunction; onRefresh: () => Promise | void; onProjectSelect: (project: Project) => void; onSessionSelect: (session: ProjectSession) => void; onSessionDelete?: (sessionId: string) => void; onProjectDelete?: (projectName: string) => void; setCurrentProject: (project: Project) => void; setSidebarVisible: (visible: boolean) => void; sidebarVisible: boolean; }; export function useSidebarController({ projects, selectedProject, selectedSession, isLoading, isMobile, t, onRefresh, onProjectSelect, onSessionSelect, onSessionDelete, onProjectDelete, setCurrentProject, setSidebarVisible, sidebarVisible, }: UseSidebarControllerArgs) { const [expandedProjects, setExpandedProjects] = useState>(new Set()); const [editingProject, setEditingProject] = useState(null); const [showNewProject, setShowNewProject] = useState(false); const [editingName, setEditingName] = useState(''); const [loadingSessions, setLoadingSessions] = useState({}); const [additionalSessions, setAdditionalSessions] = useState({}); const [initialSessionsLoaded, setInitialSessionsLoaded] = useState>(new Set()); const [currentTime, setCurrentTime] = useState(new Date()); const [projectSortOrder, setProjectSortOrder] = useState('name'); const [isRefreshing, setIsRefreshing] = useState(false); const [projectHasMoreOverrides, setProjectHasMoreOverrides] = useState>({}); const [editingSession, setEditingSession] = useState(null); const [editingSessionName, setEditingSessionName] = useState(''); const [searchFilter, setSearchFilter] = useState(''); const [deletingProjects, setDeletingProjects] = useState>(new Set()); const [deleteConfirmation, setDeleteConfirmation] = useState(null); const [sessionDeleteConfirmation, setSessionDeleteConfirmation] = useState(null); const [showVersionModal, setShowVersionModal] = useState(false); const [starredProjects, setStarredProjects] = useState>(() => loadStarredProjects()); const isSidebarCollapsed = !isMobile && !sidebarVisible; useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); }, 60000); return () => clearInterval(timer); }, []); useEffect(() => { setAdditionalSessions({}); setInitialSessionsLoaded(new Set()); setProjectHasMoreOverrides({}); }, [projects]); useEffect(() => { if (selectedProject) { setExpandedProjects((prev) => { if (prev.has(selectedProject.name)) { return prev; } const next = new Set(prev); next.add(selectedProject.name); return next; }); } }, [selectedSession, selectedProject]); useEffect(() => { if (projects.length > 0 && !isLoading) { const loadedProjects = new Set(); projects.forEach((project) => { if (project.sessions && project.sessions.length >= 0) { loadedProjects.add(project.name); } }); setInitialSessionsLoaded(loadedProjects); } }, [projects, isLoading]); useEffect(() => { const loadSortOrder = () => { setProjectSortOrder(readProjectSortOrder()); }; loadSortOrder(); const handleStorageChange = (event: StorageEvent) => { if (event.key === 'claude-settings') { loadSortOrder(); } }; window.addEventListener('storage', handleStorageChange); const interval = setInterval(() => { if (document.hasFocus()) { loadSortOrder(); } }, 1000); return () => { window.removeEventListener('storage', handleStorageChange); clearInterval(interval); }; }, []); const toggleProject = useCallback((projectName: string) => { setExpandedProjects((prev) => { const next = new Set(); if (!prev.has(projectName)) { next.add(projectName); } return next; }); }, []); const handleSessionClick = useCallback( (session: SessionWithProvider, projectName: string) => { onSessionSelect({ ...session, __projectName: projectName }); }, [onSessionSelect], ); const toggleStarProject = useCallback((projectName: string) => { setStarredProjects((prev) => { const next = new Set(prev); if (next.has(projectName)) { next.delete(projectName); } else { next.add(projectName); } persistStarredProjects(next); return next; }); }, []); const isProjectStarred = useCallback( (projectName: string) => starredProjects.has(projectName), [starredProjects], ); const getProjectSessions = useCallback( (project: Project) => getAllSessions(project, additionalSessions), [additionalSessions], ); const projectsWithSessionMeta = useMemo( () => projects.map((project) => { const hasMoreOverride = projectHasMoreOverrides[project.name]; if (hasMoreOverride === undefined) { return project; } return { ...project, sessionMeta: { ...project.sessionMeta, hasMore: hasMoreOverride }, }; }), [projectHasMoreOverrides, projects], ); const sortedProjects = useMemo( () => sortProjects(projectsWithSessionMeta, projectSortOrder, starredProjects, additionalSessions), [additionalSessions, projectSortOrder, projectsWithSessionMeta, starredProjects], ); const filteredProjects = useMemo( () => filterProjects(sortedProjects, searchFilter), [searchFilter, sortedProjects], ); const startEditing = useCallback((project: Project) => { setEditingProject(project.name); setEditingName(project.displayName); }, []); const cancelEditing = useCallback(() => { setEditingProject(null); setEditingName(''); }, []); const saveProjectName = useCallback( async (projectName: string) => { try { const response = await api.renameProject(projectName, editingName); if (response.ok) { if (window.refreshProjects) { await window.refreshProjects(); } else { window.location.reload(); } } else { console.error('Failed to rename project'); } } catch (error) { console.error('Error renaming project:', error); } finally { setEditingProject(null); setEditingName(''); } }, [editingName], ); const showDeleteSessionConfirmation = useCallback( ( projectName: string, sessionId: string, sessionTitle: string, provider: SessionDeleteConfirmation['provider'] = 'claude', ) => { setSessionDeleteConfirmation({ projectName, sessionId, sessionTitle, provider }); }, [], ); const confirmDeleteSession = useCallback(async () => { if (!sessionDeleteConfirmation) { return; } const { projectName, sessionId, provider } = sessionDeleteConfirmation; setSessionDeleteConfirmation(null); try { let response; if (provider === 'codex') { response = await api.deleteCodexSession(sessionId); } else if (provider === 'gemini') { response = await api.deleteGeminiSession(sessionId); } else { response = await api.deleteSession(projectName, sessionId); } if (response.ok) { onSessionDelete?.(sessionId); } else { const errorText = await response.text(); console.error('[Sidebar] Failed to delete session:', { status: response.status, error: errorText, }); alert(t('messages.deleteSessionFailed')); } } catch (error) { console.error('[Sidebar] Error deleting session:', error); alert(t('messages.deleteSessionError')); } }, [onSessionDelete, sessionDeleteConfirmation, t]); const requestProjectDelete = useCallback( (project: Project) => { setDeleteConfirmation({ project, sessionCount: getProjectSessions(project).length, }); }, [getProjectSessions], ); const confirmDeleteProject = useCallback(async () => { if (!deleteConfirmation) { return; } const { project, sessionCount } = deleteConfirmation; const isEmpty = sessionCount === 0; setDeleteConfirmation(null); setDeletingProjects((prev) => new Set([...prev, project.name])); try { const response = await api.deleteProject(project.name, !isEmpty); if (response.ok) { onProjectDelete?.(project.name); } else { const error = (await response.json()) as { error?: string }; alert(error.error || t('messages.deleteProjectFailed')); } } catch (error) { console.error('Error deleting project:', error); alert(t('messages.deleteProjectError')); } finally { setDeletingProjects((prev) => { const next = new Set(prev); next.delete(project.name); return next; }); } }, [deleteConfirmation, onProjectDelete, t]); const loadMoreSessions = useCallback( async (project: Project) => { const hasMoreOverride = projectHasMoreOverrides[project.name]; const canLoadMore = hasMoreOverride !== undefined ? hasMoreOverride : project.sessionMeta?.hasMore === true; if (!canLoadMore || loadingSessions[project.name]) { return; } setLoadingSessions((prev) => ({ ...prev, [project.name]: true })); try { const currentSessionCount = (project.sessions?.length || 0) + (additionalSessions[project.name]?.length || 0); const response = await api.sessions(project.name, 5, currentSessionCount); if (!response.ok) { return; } const result = (await response.json()) as { sessions?: ProjectSession[]; hasMore?: boolean; }; setAdditionalSessions((prev) => ({ ...prev, [project.name]: [...(prev[project.name] || []), ...(result.sessions || [])], })); if (result.hasMore === false) { // Keep hasMore state in local hook state instead of mutating the project prop object. setProjectHasMoreOverrides((prev) => ({ ...prev, [project.name]: false })); } } catch (error) { console.error('Error loading more sessions:', error); } finally { setLoadingSessions((prev) => ({ ...prev, [project.name]: false })); } }, [additionalSessions, loadingSessions, projectHasMoreOverrides], ); const handleProjectSelect = useCallback( (project: Project) => { onProjectSelect(project); setCurrentProject(project); }, [onProjectSelect, setCurrentProject], ); const refreshProjects = useCallback(async () => { setIsRefreshing(true); try { await onRefresh(); } finally { setIsRefreshing(false); } }, [onRefresh]); const updateSessionSummary = useCallback( async (_projectName: string, sessionId: string, summary: string, provider: SessionProvider) => { const trimmed = summary.trim(); if (!trimmed) { setEditingSession(null); setEditingSessionName(''); return; } try { const response = await api.renameSession(sessionId, trimmed, provider); if (response.ok) { await onRefresh(); } else { console.error('[Sidebar] Failed to rename session:', response.status); alert(t('messages.renameSessionFailed')); } } catch (error) { console.error('[Sidebar] Error renaming session:', error); alert(t('messages.renameSessionError')); } finally { setEditingSession(null); setEditingSessionName(''); } }, [onRefresh, t], ); const collapseSidebar = useCallback(() => { setSidebarVisible(false); }, [setSidebarVisible]); const expandSidebar = useCallback(() => { setSidebarVisible(true); }, [setSidebarVisible]); return { isSidebarCollapsed, expandedProjects, editingProject, showNewProject, editingName, loadingSessions, additionalSessions, initialSessionsLoaded, currentTime, projectSortOrder, isRefreshing, editingSession, editingSessionName, searchFilter, deletingProjects, deleteConfirmation, sessionDeleteConfirmation, showVersionModal, starredProjects, filteredProjects, toggleProject, handleSessionClick, toggleStarProject, isProjectStarred, getProjectSessions, startEditing, cancelEditing, saveProjectName, showDeleteSessionConfirmation, confirmDeleteSession, requestProjectDelete, confirmDeleteProject, loadMoreSessions, handleProjectSelect, refreshProjects, updateSessionSummary, collapseSidebar, expandSidebar, setShowNewProject, setEditingName, setEditingSession, setEditingSessionName, setSearchFilter, setDeleteConfirmation, setSessionDeleteConfirmation, setShowVersionModal, }; }