/** * 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 initializelet 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<Session[]> { 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<PaginatedSessionsResult> { 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<Session[]> { 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<SessionDetail | null> { 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<ConversationGroup[]> { 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<SessionMetrics | null> { 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<WaterfallData | null> { 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; }}
/**
* 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<Session[]> {
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<PaginatedSessionsResult> {
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<Session[]> {
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<SessionDetail | null> {
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<ConversationGroup[]> {
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<SessionMetrics | null> {
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<WaterfallData | null> {
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;
}
}