import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { TFunction } from 'i18next'; import { api } from '../../../utils/api'; import { usePaletteOps } from '../../../contexts/PaletteOpsContext'; import type { Project, ProjectSession, LLMProvider } from '../../../types/app'; import type { SessionActivityMap } from '../../../hooks/useSessionProtection'; import type { ArchivedProjectListItem, ArchivedSessionListItem, DeleteProjectConfirmation, ProjectSortOrder, SidebarSearchMode, SessionDeleteConfirmation, SessionWithProvider, } from '../types/types'; import { clearLegacyStarredProjectIds, filterProjects, getAllSessions, readLegacyStarredProjectIds, readProjectSortOrder, sortProjects, } from '../utils/utils'; type SnippetHighlight = { start: number; end: number; }; type ConversationMatch = { role: string; snippet: string; highlights: SnippetHighlight[]; timestamp: string | null; provider?: string; messageUuid?: string | null; }; type ConversationSession = { sessionId: string; sessionSummary: string; provider?: string; matches: ConversationMatch[]; }; type ConversationProjectResult = { // Emitted by the provider search service so the sidebar can map a // match back to the Project in its current state by projectId. projectId: string | null; projectName: string; projectDisplayName: string; sessions: ConversationSession[]; }; export type ConversationSearchResults = { results: ConversationProjectResult[]; totalMatches: number; query: string; }; export type SearchProgress = { scannedProjects: number; totalProjects: number; }; type ArchivedSessionsApiPayload = { success?: boolean; data?: { sessions?: ArchivedSessionListItem[]; }; }; type ArchivedProjectsApiPayload = { success?: boolean; data?: { projects?: ArchivedProjectListItem[]; }; }; type UseSidebarControllerArgs = { projects: Project[]; selectedProject: Project | null; selectedSession: ProjectSession | null; activeSessions: SessionActivityMap; isLoading: boolean; isMobile: boolean; t: TFunction; onRefresh: () => Promise | void; onProjectSelect: (project: Project) => void; onSessionSelect: (session: ProjectSession) => void; onSessionDelete?: (sessionId: string) => void; onLoadMoreSessions?: (projectId: string) => Promise | void; // `projectId` is the DB-assigned identifier; callbacks use that post-migration. onProjectDelete?: (projectId: string) => void; setCurrentProject: (project: Project) => void; setSidebarVisible: (visible: boolean) => void; sidebarVisible: boolean; }; export function useSidebarController({ projects, selectedProject, selectedSession: _selectedSession, activeSessions, isLoading, isMobile, t, onRefresh, onProjectSelect, onSessionSelect, onSessionDelete, onLoadMoreSessions, onProjectDelete, setCurrentProject, setSidebarVisible, sidebarVisible, }: UseSidebarControllerArgs) { const paletteOps = usePaletteOps(); const [expandedProjects, setExpandedProjects] = useState>(new Set()); const [editingProject, setEditingProject] = useState(null); const [showNewProject, setShowNewProject] = useState(false); const [editingName, setEditingName] = useState(''); const [initialSessionsLoaded, setInitialSessionsLoaded] = useState>(new Set()); const [currentTime, setCurrentTime] = useState(new Date()); const [projectSortOrder, setProjectSortOrder] = useState('name'); const [isRefreshing, setIsRefreshing] = useState(false); 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 [searchMode, setSearchMode] = useState('projects'); const [conversationResults, setConversationResults] = useState(null); const [isSearching, setIsSearching] = useState(false); const [searchProgress, setSearchProgress] = useState(null); const [archivedProjects, setArchivedProjects] = useState([]); const [archivedSessions, setArchivedSessions] = useState([]); const [isArchivedSessionsLoading, setIsArchivedSessionsLoading] = useState(false); const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); const [optimisticStarByProjectId, setOptimisticStarByProjectId] = useState>(new Map()); const [loadingMoreProjects, setLoadingMoreProjects] = useState>(new Set()); const searchSeqRef = useRef(0); const eventSourceRef = useRef(null); const starToggleSequenceByProjectRef = useRef>(new Map()); const migrationStartedRef = useRef(false); const onRefreshRef = useRef(onRefresh); const isSidebarCollapsed = !isMobile && !sidebarVisible; const activeSessionIds = useMemo(() => new Set(activeSessions.keys()), [activeSessions]); const runningSessionsCount = activeSessionIds.size; useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); }, 60000); return () => clearInterval(timer); }, []); useEffect(() => { setInitialSessionsLoaded(new Set()); }, [projects]); useEffect(() => { // Auto-expand only when the selected project identity changes. // Depending on the full `selectedProject` object (or `selectedSession`) causes // websocket-driven list refreshes to re-open projects users manually collapsed. const selectedProjectId = selectedProject?.projectId; if (!selectedProjectId) { return; } setExpandedProjects((prev) => { if (prev.has(selectedProjectId)) { return prev; } const next = new Set(prev); next.add(selectedProjectId); return next; }); }, [selectedProject?.projectId]); useEffect(() => { if (projects.length > 0 && !isLoading) { const loadedProjects = new Set(); projects.forEach((project) => { if (project.sessions && project.sessions.length >= 0) { loadedProjects.add(project.projectId); } }); 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); }; }, []); useEffect(() => { onRefreshRef.current = onRefresh; }, [onRefresh]); const fetchArchivedSessions = useCallback(async () => { setIsArchivedSessionsLoading(true); try { const [archivedProjectsResponse, archivedSessionsResponse] = await Promise.all([ api.archivedProjects(), api.getArchivedSessions(), ]); if (!archivedProjectsResponse.ok) { throw new Error(`Failed to load archived projects: ${archivedProjectsResponse.status}`); } if (!archivedSessionsResponse.ok) { throw new Error(`Failed to load archived sessions: ${archivedSessionsResponse.status}`); } const archivedProjectsPayload = (await archivedProjectsResponse.json()) as ArchivedProjectsApiPayload; const archivedSessionsPayload = (await archivedSessionsResponse.json()) as ArchivedSessionsApiPayload; const nextProjects = Array.isArray(archivedProjectsPayload.data?.projects) ? archivedProjectsPayload.data.projects : []; const archivedProjectIds = new Set(nextProjects.map((project) => project.projectId)); const nextStandaloneSessions = Array.isArray(archivedSessionsPayload.data?.sessions) ? archivedSessionsPayload.data.sessions.filter((session) => !session.projectId || !archivedProjectIds.has(session.projectId)) : []; setArchivedProjects(nextProjects); setArchivedSessions(nextStandaloneSessions); } catch (error) { console.error('[Sidebar] Failed to load archived sessions:', error); } finally { setIsArchivedSessionsLoading(false); } }, []); useEffect(() => { if (migrationStartedRef.current) { return; } const legacyStarredProjectIds = readLegacyStarredProjectIds(); if (legacyStarredProjectIds.length === 0) { return; } migrationStartedRef.current = true; const migrateLegacyStars = async () => { try { await api.migrateLegacyProjectStars(legacyStarredProjectIds); await onRefreshRef.current(); } catch (error) { console.error('[Sidebar] Failed to migrate legacy starred projects:', error); } finally { clearLegacyStarredProjectIds(); } }; void migrateLegacyStars(); }, [onRefresh]); useEffect(() => { void fetchArchivedSessions(); }, [fetchArchivedSessions]); useEffect(() => { if (searchMode !== 'archived') { return; } // Refresh archive contents when the archived tab opens so restore actions // and background synchronizer updates are reflected without a full reload. void fetchArchivedSessions(); }, [fetchArchivedSessions, searchMode]); useEffect(() => { setOptimisticStarByProjectId((previous) => { if (previous.size === 0) { return previous; } const next = new Map(previous); let changed = false; for (const [projectId, optimisticValue] of previous.entries()) { const project = projects.find((candidate) => candidate.projectId === projectId); if (!project) { next.delete(projectId); changed = true; continue; } if (Boolean(project.isStarred) === optimisticValue) { next.delete(projectId); changed = true; } } return changed ? next : previous; }); }, [projects]); // Debounce search text updates so both project filtering and conversation // SSE requests avoid running on every keypress. useEffect(() => { const timeout = setTimeout(() => { setDebouncedSearchQuery(searchFilter.trim()); }, 300); return () => { clearTimeout(timeout); }; }, [searchFilter]); // Debounced conversation search with SSE streaming useEffect(() => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } const query = debouncedSearchQuery; if (searchMode !== 'conversations' || query.length < 2) { searchSeqRef.current += 1; setConversationResults(null); setSearchProgress(null); setIsSearching(false); return; } setIsSearching(true); const seq = ++searchSeqRef.current; if (seq !== searchSeqRef.current) { return; } const url = api.searchConversationsUrl(query); const es = new EventSource(url); eventSourceRef.current = es; const accumulated: ConversationProjectResult[] = []; let totalMatches = 0; es.addEventListener('result', (evt) => { if (seq !== searchSeqRef.current) { es.close(); return; } try { const data = JSON.parse(evt.data) as { projectResult: ConversationProjectResult; totalMatches: number; scannedProjects: number; totalProjects: number; }; accumulated.push(data.projectResult); totalMatches = data.totalMatches; setConversationResults({ results: [...accumulated], totalMatches, query }); setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects }); } catch { // Ignore malformed SSE data } }); es.addEventListener('progress', (evt) => { if (seq !== searchSeqRef.current) { es.close(); return; } try { const data = JSON.parse(evt.data) as { totalMatches: number; scannedProjects: number; totalProjects: number }; totalMatches = data.totalMatches; setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects }); } catch { // Ignore malformed SSE data } }); es.addEventListener('done', () => { if (seq !== searchSeqRef.current) { es.close(); return; } es.close(); eventSourceRef.current = null; setIsSearching(false); setSearchProgress(null); if (accumulated.length === 0) { setConversationResults({ results: [], totalMatches: 0, query }); } }); es.addEventListener('error', () => { if (seq !== searchSeqRef.current) { es.close(); return; } es.close(); eventSourceRef.current = null; setIsSearching(false); setSearchProgress(null); if (accumulated.length === 0) { setConversationResults({ results: [], totalMatches: 0, query }); } }); return () => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } }; }, [debouncedSearchQuery, searchMode]); // All sidebar state keys (expanded, starred, loading, etc.) use the DB // `projectId` as their identifier after the migration. const toggleProject = useCallback((projectId: string) => { setExpandedProjects((prev) => { const next = new Set(); if (!prev.has(projectId)) { next.add(projectId); } return next; }); }, []); const handleSessionClick = useCallback( (session: SessionWithProvider, projectId: string) => { // Tag the session with its owning projectId so downstream handlers // can correlate it with the selectedProject in the app state. onSessionSelect({ ...session, __projectId: projectId }); }, [onSessionSelect], ); const resolveProjectStarState = useCallback( (projectId: string): boolean => { if (optimisticStarByProjectId.has(projectId)) { return Boolean(optimisticStarByProjectId.get(projectId)); } return projects.some((project) => project.projectId === projectId && Boolean(project.isStarred)); }, [optimisticStarByProjectId, projects], ); const toggleStarProject = useCallback((projectId: string) => { const previousStarState = resolveProjectStarState(projectId); const optimisticStarState = !previousStarState; const latestSequence = (starToggleSequenceByProjectRef.current.get(projectId) ?? 0) + 1; starToggleSequenceByProjectRef.current.set(projectId, latestSequence); setOptimisticStarByProjectId((previous) => { const next = new Map(previous); next.set(projectId, optimisticStarState); return next; }); const updateStar = async () => { try { const response = await api.toggleProjectStar(projectId); if (!response.ok) { const payload = (await response.json()) as { error?: string | { message?: string } }; const errorPayload = payload.error; const message = typeof errorPayload === 'string' ? errorPayload : errorPayload && typeof errorPayload === 'object' && errorPayload.message ? errorPayload.message : t('messages.updateProjectError'); throw new Error(message); } const payload = (await response.json()) as { isStarred?: boolean }; const isLatestSequence = starToggleSequenceByProjectRef.current.get(projectId) === latestSequence; if (!isLatestSequence) { return; } setOptimisticStarByProjectId((previous) => { const next = new Map(previous); next.set(projectId, Boolean(payload.isStarred)); return next; }); } catch (error) { const isLatestSequence = starToggleSequenceByProjectRef.current.get(projectId) === latestSequence; if (!isLatestSequence) { return; } setOptimisticStarByProjectId((previous) => { const next = new Map(previous); next.set(projectId, previousStarState); return next; }); console.error('[Sidebar] Failed to toggle project star:', error); alert(t('messages.updateProjectError')); } }; void updateStar(); }, [resolveProjectStarState, t]); const isProjectStarred = useCallback( (projectId: string) => resolveProjectStarState(projectId), [resolveProjectStarState], ); const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []); const loadMoreSessionsForProject = useCallback(async (projectId: string) => { if (!onLoadMoreSessions) { return; } let shouldLoad = false; setLoadingMoreProjects((previous) => { if (previous.has(projectId)) { return previous; } shouldLoad = true; const next = new Set(previous); next.add(projectId); return next; }); if (!shouldLoad) { return; } try { await onLoadMoreSessions(projectId); } catch (error) { console.error('[Sidebar] Failed to load more sessions:', error); alert(t('messages.refreshError')); } finally { setLoadingMoreProjects((previous) => { const next = new Set(previous); next.delete(projectId); return next; }); } }, [onLoadMoreSessions, t]); const projectsWithResolvedStarState = useMemo(() => { if (optimisticStarByProjectId.size === 0) { return projects; } return projects.map((project) => { const optimisticStarState = optimisticStarByProjectId.get(project.projectId); if (optimisticStarState === undefined) { return project; } const currentStarState = Boolean(project.isStarred); if (currentStarState === optimisticStarState) { return project; } return { ...project, isStarred: optimisticStarState, }; }); }, [optimisticStarByProjectId, projects]); const sortedProjects = useMemo( () => sortProjects(projectsWithResolvedStarState, projectSortOrder), [projectSortOrder, projectsWithResolvedStarState], ); const runningProjects = useMemo(() => { if (activeSessionIds.size === 0) { return []; } return sortedProjects.reduce((acc, project) => { const sessions = (project.sessions ?? []).filter((session) => activeSessionIds.has(String(session.id))); const cursorSessions = (project.cursorSessions ?? []).filter((session) => activeSessionIds.has(String(session.id))); const codexSessions = (project.codexSessions ?? []).filter((session) => activeSessionIds.has(String(session.id))); const geminiSessions = (project.geminiSessions ?? []).filter((session) => activeSessionIds.has(String(session.id))); const opencodeSessions = (project.opencodeSessions ?? []).filter((session) => activeSessionIds.has(String(session.id))); const runningCount = sessions.length + cursorSessions.length + codexSessions.length + geminiSessions.length + opencodeSessions.length; if (runningCount === 0) { return acc; } acc.push({ ...project, sessions, cursorSessions, codexSessions, geminiSessions, opencodeSessions, sessionMeta: { ...project.sessionMeta, total: runningCount, hasMore: false, }, }); return acc; }, []); }, [activeSessionIds, sortedProjects]); const filteredProjects = useMemo( () => filterProjects(searchMode === 'running' ? runningProjects : sortedProjects, debouncedSearchQuery), [debouncedSearchQuery, runningProjects, searchMode, sortedProjects], ); const filteredArchivedSessions = useMemo(() => { const normalizedSearch = debouncedSearchQuery.trim().toLowerCase(); if (!normalizedSearch) { return archivedSessions; } return archivedSessions.filter((session) => { const searchableFields = [ session.sessionTitle, session.projectDisplayName, session.projectPath ?? '', session.provider, ]; return searchableFields.some((value) => value.toLowerCase().includes(normalizedSearch)); }); }, [archivedSessions, debouncedSearchQuery]); const filteredArchivedProjects = useMemo(() => { const normalizedSearch = debouncedSearchQuery.trim().toLowerCase(); if (!normalizedSearch) { return archivedProjects; } return archivedProjects.filter((project) => { const projectMatches = [ project.displayName, project.fullPath || '', ].some((value) => value.toLowerCase().includes(normalizedSearch)); if (projectMatches) { return true; } return getAllSessions(project).some((session) => { const sessionSummary = typeof session.summary === 'string' && session.summary.trim().length > 0 ? session.summary : typeof session.name === 'string' ? session.name : ''; return [ sessionSummary, session.__provider, ].some((value) => value.toLowerCase().includes(normalizedSearch)); }); }); }, [archivedProjects, debouncedSearchQuery]); const startEditing = useCallback((project: Project) => { // `editingProject` is keyed by projectId so it stays stable across // display-name mutations that happen while the input is open. setEditingProject(project.projectId); setEditingName(project.displayName); }, []); const cancelEditing = useCallback(() => { setEditingProject(null); setEditingName(''); }, []); const saveProjectName = useCallback( // `projectId` is the DB primary key; the rename API resolves the path // through the `projects` table before writing the new display name. async (projectId: string) => { try { const response = await api.renameProject(projectId, editingName); if (response.ok) { await paletteOps.refreshProjects(); } else { console.error('Failed to rename project'); } } catch (error) { console.error('Error renaming project:', error); } finally { setEditingProject(null); setEditingName(''); } }, [editingName, paletteOps], ); const showDeleteSessionConfirmation = useCallback( // Kept with project/provider arguments for component wiring compatibility; // deletion now uses only `sessionId` via /api/providers/sessions/:sessionId. ( projectId: string | null, sessionId: string, sessionTitle: string, provider: SessionDeleteConfirmation['provider'] = 'claude', options: { isArchived?: boolean; } = {}, ) => { setSessionDeleteConfirmation({ projectId, sessionId, sessionTitle, provider, isArchived: Boolean(options.isArchived), }); }, [], ); const confirmDeleteSession = useCallback(async (hardDelete = false) => { if (!sessionDeleteConfirmation) { return; } const { sessionId } = sessionDeleteConfirmation; setSessionDeleteConfirmation(null); try { const response = await api.deleteSession(sessionId, hardDelete); if (response.ok) { onSessionDelete?.(sessionId); await fetchArchivedSessions(); } 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')); } }, [fetchArchivedSessions, onSessionDelete, sessionDeleteConfirmation, t]); const requestProjectDelete = useCallback( (project: Project) => { setDeleteConfirmation({ project, sessionCount: getProjectSessions(project).length, }); }, [getProjectSessions], ); const confirmDeleteProject = useCallback(async (deleteData = false) => { if (!deleteConfirmation) { return; } const { project } = deleteConfirmation; setDeleteConfirmation(null); // Track in-flight deletes by projectId so the UI can disable actions // even if the project object is rebuilt while the request is flying. setDeletingProjects((prev) => new Set([...prev, project.projectId])); try { const response = await api.deleteProject(project.projectId, deleteData); if (response.ok) { onProjectDelete?.(project.projectId); } else { const data = (await response.json()) as { error?: string | { message?: string } }; const err = data.error; const message = typeof err === 'string' ? err : err && typeof err === 'object' && err.message ? err.message : t('messages.deleteProjectFailed'); alert(message); } } catch (error) { console.error('Error deleting project:', error); alert(t('messages.deleteProjectError')); } finally { setDeletingProjects((prev) => { const next = new Set(prev); next.delete(project.projectId); return next; }); } }, [deleteConfirmation, onProjectDelete, t]); const handleProjectSelect = useCallback( (project: Project) => { onProjectSelect(project); setCurrentProject(project); }, [onProjectSelect, setCurrentProject], ); const openArchivedSession = useCallback((session: ArchivedSessionListItem) => { const activeProject = session.projectId ? projects.find((candidate) => candidate.projectId === session.projectId) : null; const archivedProject = session.projectId ? archivedProjects.find((candidate) => candidate.projectId === session.projectId) : null; const matchingProject = activeProject ?? archivedProject ?? null; const sessionPayload: ProjectSession = { id: session.sessionId, summary: session.sessionTitle, __provider: session.provider, __projectId: matchingProject?.projectId ?? session.projectId ?? undefined, }; // Archived sessions still need a selected project context. Active projects // come from the normal sidebar list, while archived-project sessions resolve // through the archive payload loaded by this controller. if (matchingProject) { handleProjectSelect(matchingProject); } onSessionSelect(sessionPayload); }, [archivedProjects, handleProjectSelect, onSessionSelect, projects]); const restoreArchivedProject = useCallback(async (projectId: string) => { try { const response = await api.restoreProject(projectId); if (!response.ok) { const errorText = await response.text(); console.error('[Sidebar] Failed to restore project:', { status: response.status, error: errorText, }); alert(t('messages.restoreProjectFailed', 'Failed to restore project. Please try again.')); return; } await Promise.all([ Promise.resolve(onRefresh()), fetchArchivedSessions(), ]); } catch (error) { console.error('[Sidebar] Error restoring project:', error); alert(t('messages.restoreProjectError', 'Error restoring project. Please try again.')); } }, [fetchArchivedSessions, onRefresh, t]); const restoreArchivedSession = useCallback(async (sessionId: string) => { try { const response = await api.restoreSession(sessionId); if (!response.ok) { const errorText = await response.text(); console.error('[Sidebar] Failed to restore session:', { status: response.status, error: errorText, }); alert(t('messages.restoreSessionFailed', 'Failed to restore session. Please try again.')); return; } await Promise.all([ Promise.resolve(onRefresh()), fetchArchivedSessions(), ]); } catch (error) { console.error('[Sidebar] Error restoring session:', error); alert(t('messages.restoreSessionError', 'Error restoring session. Please try again.')); } }, [fetchArchivedSessions, onRefresh, t]); const refreshProjects = useCallback(async () => { setIsRefreshing(true); try { await Promise.all([ Promise.resolve(onRefresh()), fetchArchivedSessions(), ]); } finally { setIsRefreshing(false); } }, [fetchArchivedSessions, onRefresh]); const updateSessionSummary = useCallback( // `_projectId` and `_provider` are preserved for compatibility with // existing sidebar callback signatures; backend rename only needs sessionId. async (_projectId: string, sessionId: string, summary: string, _provider: LLMProvider) => { const trimmed = summary.trim(); if (!trimmed) { setEditingSession(null); setEditingSessionName(''); return; } try { const response = await api.renameSession(sessionId, trimmed); 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, initialSessionsLoaded, currentTime, projectSortOrder, isRefreshing, editingSession, editingSessionName, searchFilter, deletingProjects, loadingMoreProjects, deleteConfirmation, sessionDeleteConfirmation, showVersionModal, filteredProjects, runningSessionsCount, archivedProjects: filteredArchivedProjects, archivedSessions: filteredArchivedSessions, archivedSessionsCount: archivedProjects.length + archivedSessions.length, isArchivedSessionsLoading, toggleProject, handleSessionClick, toggleStarProject, isProjectStarred, getProjectSessions, loadMoreSessionsForProject, startEditing, cancelEditing, saveProjectName, showDeleteSessionConfirmation, confirmDeleteSession, requestProjectDelete, confirmDeleteProject, handleProjectSelect, openArchivedSession, restoreArchivedProject, restoreArchivedSession, refreshProjects, updateSessionSummary, collapseSidebar, expandSidebar, setShowNewProject, setEditingName, setEditingSession, setEditingSessionName, searchMode, setSearchMode, conversationResults, isSearching, searchProgress, clearConversationResults: useCallback(() => { searchSeqRef.current += 1; if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } setIsSearching(false); setSearchProgress(null); setConversationResults(null); }, []), setSearchFilter, setDeleteConfirmation, setSessionDeleteConfirmation, setShowVersionModal, }; }