/** * IPC Handlers for Session Operations. * * Handlers: * - get-sessions: List sessions for a project * - get-sessions-paginated: List sessions with cursor-based pagination * - get-session-detail: Get full session detail with subagents * - get-session-groups: Get conversation groups for a session * - get-session-metrics: Get metrics for a session */ import { createLogger } from '@shared/utils/logger'; import { type IpcMain, type IpcMainInvokeEvent } from 'electron'; import { DataCache } from '../services'; import { type ConversationGroup, type PaginatedSessionsResult, type Session, type SessionDetail, type SessionMetrics, type SessionsByIdsOptions, type SessionsPaginationOptions, } from '../types'; import { coercePageLimit, validateProjectId, validateSessionId } from './guards'; import type { ServiceContextRegistry } from '../services'; import type { WaterfallData } from '@shared/types'; const logger = createLogger('IPC:sessions'); // Service registry - set via initialize let registry: ServiceContextRegistry; /** * Initializes session handlers with service registry. */ export function initializeSessionHandlers(contextRegistry: ServiceContextRegistry): void { registry = contextRegistry; } /** * Registers all session-related IPC handlers. */ export function registerSessionHandlers(ipcMain: IpcMain): void { ipcMain.handle('get-sessions', handleGetSessions); ipcMain.handle('get-sessions-paginated', handleGetSessionsPaginated); ipcMain.handle('get-sessions-by-ids', handleGetSessionsByIds); ipcMain.handle('get-session-detail', handleGetSessionDetail); ipcMain.handle('get-session-groups', handleGetSessionGroups); ipcMain.handle('get-session-metrics', handleGetSessionMetrics); ipcMain.handle('get-waterfall-data', handleGetWaterfallData); logger.info('Session handlers registered'); } /** * Removes all session IPC handlers. */ export function removeSessionHandlers(ipcMain: IpcMain): void { ipcMain.removeHandler('get-sessions'); ipcMain.removeHandler('get-sessions-paginated'); ipcMain.removeHandler('get-sessions-by-ids'); ipcMain.removeHandler('get-session-detail'); ipcMain.removeHandler('get-session-groups'); ipcMain.removeHandler('get-session-metrics'); ipcMain.removeHandler('get-waterfall-data'); logger.info('Session handlers removed'); } // ============================================================================= // Handler Implementations // ============================================================================= /** * Handler for 'get-sessions' IPC call. * Lists all sessions for a given project. */ async function handleGetSessions( _event: IpcMainInvokeEvent, projectId: string ): Promise { try { const validatedProject = validateProjectId(projectId); if (!validatedProject.valid) { logger.error(`get-sessions rejected: ${validatedProject.error ?? 'Invalid projectId'}`); return []; } const { projectScanner } = registry.getActive(); const sessions = await projectScanner.listSessions(validatedProject.value!); return sessions; } catch (error) { logger.error(`Error in get-sessions for project ${projectId}:`, error); return []; } } /** * Handler for 'get-sessions-paginated' IPC call. * Lists sessions for a project with cursor-based pagination. */ async function handleGetSessionsPaginated( _event: IpcMainInvokeEvent, projectId: string, cursor: string | null, limit?: number, options?: SessionsPaginationOptions ): Promise { try { const validatedProject = validateProjectId(projectId); if (!validatedProject.valid) { logger.error( `get-sessions-paginated rejected: ${validatedProject.error ?? 'Invalid projectId'}` ); return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 }; } const { projectScanner } = registry.getActive(); const safeLimit = coercePageLimit(limit, 20); const result = await projectScanner.listSessionsPaginated( validatedProject.value!, cursor, safeLimit, options ); return result; } catch (error) { logger.error(`Error in get-sessions-paginated for project ${projectId}:`, error); return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 }; } } /** * Handler for 'get-sessions-by-ids' IPC call. * Fetches lightweight session metadata for specific session IDs. * Used to load pinned sessions that may not be in the paginated list. */ async function handleGetSessionsByIds( _event: IpcMainInvokeEvent, projectId: string, sessionIds: string[], options?: SessionsByIdsOptions ): Promise { try { const validatedProject = validateProjectId(projectId); if (!validatedProject.valid) { logger.error( `get-sessions-by-ids rejected: ${validatedProject.error ?? 'Invalid projectId'}` ); return []; } if (!Array.isArray(sessionIds)) { logger.error('get-sessions-by-ids rejected: sessionIds must be an array'); return []; } // Cap at 50 IDs const capped = sessionIds.slice(0, 50); // Validate each session ID const validIds: string[] = []; for (const id of capped) { const validated = validateSessionId(id); if (validated.valid) { validIds.push(validated.value!); } } if (validIds.length === 0) { return []; } const { projectScanner } = registry.getActive(); const fsType = projectScanner.getFileSystemProvider().type; const metadataLevel = options?.metadataLevel ?? (fsType === 'ssh' ? 'light' : 'deep'); const results = await Promise.all( validIds.map((id) => projectScanner.getSessionWithOptions(validatedProject.value!, id, { metadataLevel }) ) ); return results.filter((s): s is Session => s !== null); } catch (error) { logger.error(`Error in get-sessions-by-ids for project ${projectId}:`, error); return []; } } /** * Handler for 'get-session-detail' IPC call. * Gets full session detail including parsed chunks and subagents. * Uses cache to avoid re-parsing large files. */ async function handleGetSessionDetail( _event: IpcMainInvokeEvent, projectId: string, sessionId: string ): Promise { try { const validatedProject = validateProjectId(projectId); const validatedSession = validateSessionId(sessionId); if (!validatedProject.valid || !validatedSession.valid) { logger.error( `get-session-detail rejected: ${validatedProject.error ?? validatedSession.error ?? 'Invalid parameters'}` ); return null; } const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } = registry.getActive(); const safeProjectId = validatedProject.value!; const safeSessionId = validatedSession.value!; const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId); // Check cache first let sessionDetail = dataCache.get(cacheKey); if (sessionDetail) { return sessionDetail; } const fsType = projectScanner.getFileSystemProvider().type; // In SSH mode, avoid an extra deep metadata scan before full parse. const session = await projectScanner.getSessionWithOptions(safeProjectId, safeSessionId, { metadataLevel: fsType === 'ssh' ? 'light' : 'deep', }); if (!session) { logger.error(`Session not found: ${sessionId}`); return null; } // Parse session messages const parsedSession = await sessionParser.parseSession(safeProjectId, safeSessionId); // Resolve subagents const subagents = await subagentResolver.resolveSubagents( safeProjectId, safeSessionId, parsedSession.taskCalls, parsedSession.messages ); session.hasSubagents = subagents.length > 0; // Build session detail with chunks sessionDetail = chunkBuilder.buildSessionDetail(session, parsedSession.messages, subagents); // Cache the result dataCache.set(cacheKey, sessionDetail); return sessionDetail; } catch (error) { logger.error(`Error in get-session-detail for ${projectId}/${sessionId}:`, error); return null; } } /** * Handler for 'get-session-groups' IPC call. * Gets conversation groups for a session using the new buildGroups API. * This is an alternative to chunks that provides a simpler, more natural grouping. */ async function handleGetSessionGroups( _event: IpcMainInvokeEvent, projectId: string, sessionId: string ): Promise { try { const validatedProject = validateProjectId(projectId); const validatedSession = validateSessionId(sessionId); if (!validatedProject.valid || !validatedSession.valid) { logger.error( `get-session-groups rejected: ${validatedProject.error ?? validatedSession.error ?? 'Invalid parameters'}` ); return []; } const { sessionParser, subagentResolver, chunkBuilder } = registry.getActive(); const safeProjectId = validatedProject.value!; const safeSessionId = validatedSession.value!; // Parse session messages const parsedSession = await sessionParser.parseSession(safeProjectId, safeSessionId); // Resolve subagents const subagents = await subagentResolver.resolveSubagents( safeProjectId, safeSessionId, parsedSession.taskCalls, parsedSession.messages ); // Build conversation groups using the new API const groups = chunkBuilder.buildGroups(parsedSession.messages, subagents); return groups; } catch (error) { logger.error(`Error in get-session-groups for ${projectId}/${sessionId}:`, error); return []; } } /** * Handler for 'get-session-metrics' IPC call. * Gets metrics for a session without full detail. */ async function handleGetSessionMetrics( _event: IpcMainInvokeEvent, projectId: string, sessionId: string ): Promise { try { const validatedProject = validateProjectId(projectId); const validatedSession = validateSessionId(sessionId); if (!validatedProject.valid || !validatedSession.valid) { return null; } const { sessionParser, dataCache } = registry.getActive(); const safeProjectId = validatedProject.value!; const safeSessionId = validatedSession.value!; // Try to get from cache first const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId); const cached = dataCache.get(cacheKey); if (cached) { return cached.metrics; } // Parse session to get metrics const parsedSession = await sessionParser.parseSession(safeProjectId, safeSessionId); return parsedSession.metrics; } catch (error) { logger.error(`Error in get-session-metrics for ${projectId}/${sessionId}:`, error); return null; } } /** * Handler for 'get-waterfall-data' IPC call. * Builds waterfall chart data for a session. */ async function handleGetWaterfallData( _event: IpcMainInvokeEvent, projectId: string, sessionId: string ): Promise { try { const detail = await handleGetSessionDetail(_event, projectId, sessionId); if (!detail) { return null; } const { chunkBuilder } = registry.getActive(); return chunkBuilder.buildWaterfallData(detail.chunks, detail.processes); } catch (error) { logger.error(`Error in get-waterfall-data for ${projectId}/${sessionId}:`, error); return null; } }