- Introduced an HTTP server to facilitate communication with the application. - Added route handlers for managing application configuration, including getting and updating settings. - Implemented notification operations with routes for retrieving, marking, and deleting notifications. - Created project and session management routes to list projects, sessions, and their details. - Developed SSH connection management routes for connecting, disconnecting, and retrieving SSH state and configuration. - Enhanced the application architecture to support real-time event streaming via Server-Sent Events (SSE). This commit significantly expands the application's capabilities by integrating an HTTP server and various management routes, improving user interaction and functionality.
279 lines
9.1 KiB
TypeScript
279 lines
9.1 KiB
TypeScript
/**
|
|
* HTTP route handlers for Session Operations.
|
|
*
|
|
* Routes:
|
|
* - GET /api/projects/:projectId/sessions - List sessions
|
|
* - GET /api/projects/:projectId/sessions-paginated - Paginated sessions
|
|
* - GET /api/projects/:projectId/sessions/:sessionId - Full session detail
|
|
* - GET /api/projects/:projectId/sessions/:sessionId/groups - Conversation groups
|
|
* - GET /api/projects/:projectId/sessions/:sessionId/metrics - Session metrics
|
|
* - GET /api/projects/:projectId/sessions/:sessionId/waterfall - Waterfall data
|
|
*/
|
|
|
|
import { createLogger } from '@shared/utils/logger';
|
|
|
|
import { coercePageLimit, validateProjectId, validateSessionId } from '../ipc/guards';
|
|
import { DataCache } from '../services';
|
|
|
|
import type { SessionsPaginationOptions } from '../types';
|
|
import type { HttpServices } from './index';
|
|
import type { FastifyInstance } from 'fastify';
|
|
|
|
const logger = createLogger('HTTP:sessions');
|
|
|
|
export function registerSessionRoutes(app: FastifyInstance, services: HttpServices): void {
|
|
// List sessions
|
|
app.get<{ Params: { projectId: string } }>(
|
|
'/api/projects/:projectId/sessions',
|
|
async (request) => {
|
|
try {
|
|
const validated = validateProjectId(request.params.projectId);
|
|
if (!validated.valid) {
|
|
logger.error(`GET sessions rejected: ${validated.error ?? 'unknown'}`);
|
|
return [];
|
|
}
|
|
|
|
const sessions = await services.projectScanner.listSessions(validated.value!);
|
|
return sessions;
|
|
} catch (error) {
|
|
logger.error(`Error in GET sessions for ${request.params.projectId}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
);
|
|
|
|
// Paginated sessions
|
|
app.get<{
|
|
Params: { projectId: string };
|
|
Querystring: {
|
|
cursor?: string;
|
|
limit?: string;
|
|
includeTotalCount?: string;
|
|
prefilterAll?: string;
|
|
};
|
|
}>('/api/projects/:projectId/sessions-paginated', async (request) => {
|
|
try {
|
|
const validated = validateProjectId(request.params.projectId);
|
|
if (!validated.valid) {
|
|
logger.error(`GET sessions-paginated rejected: ${validated.error ?? 'unknown'}`);
|
|
return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };
|
|
}
|
|
|
|
const cursor = request.query.cursor || null;
|
|
const limit = coercePageLimit(
|
|
request.query.limit ? Number(request.query.limit) : undefined,
|
|
20
|
|
);
|
|
const options: SessionsPaginationOptions = {
|
|
includeTotalCount: request.query.includeTotalCount !== 'false',
|
|
prefilterAll: request.query.prefilterAll !== 'false',
|
|
};
|
|
|
|
const result = await services.projectScanner.listSessionsPaginated(
|
|
validated.value!,
|
|
cursor,
|
|
limit,
|
|
options
|
|
);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error(`Error in GET sessions-paginated for ${request.params.projectId}:`, error);
|
|
return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };
|
|
}
|
|
});
|
|
|
|
// Session detail
|
|
app.get<{ Params: { projectId: string; sessionId: string } }>(
|
|
'/api/projects/:projectId/sessions/:sessionId',
|
|
async (request) => {
|
|
try {
|
|
const validatedProject = validateProjectId(request.params.projectId);
|
|
const validatedSession = validateSessionId(request.params.sessionId);
|
|
if (!validatedProject.valid || !validatedSession.valid) {
|
|
logger.error(
|
|
`GET session-detail rejected: ${validatedProject.error ?? validatedSession.error ?? 'unknown'}`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const safeProjectId = validatedProject.value!;
|
|
const safeSessionId = validatedSession.value!;
|
|
const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
|
|
|
|
// Check cache first
|
|
let sessionDetail = services.dataCache.get(cacheKey);
|
|
if (sessionDetail) {
|
|
return sessionDetail;
|
|
}
|
|
|
|
// Get session metadata
|
|
const session = await services.projectScanner.getSession(safeProjectId, safeSessionId);
|
|
if (!session) {
|
|
logger.error(`Session not found: ${safeSessionId}`);
|
|
return null;
|
|
}
|
|
|
|
// Parse session messages
|
|
const parsedSession = await services.sessionParser.parseSession(
|
|
safeProjectId,
|
|
safeSessionId
|
|
);
|
|
|
|
// Resolve subagents
|
|
const subagents = await services.subagentResolver.resolveSubagents(
|
|
safeProjectId,
|
|
safeSessionId,
|
|
parsedSession.taskCalls,
|
|
parsedSession.messages
|
|
);
|
|
|
|
// Build session detail with chunks
|
|
sessionDetail = services.chunkBuilder.buildSessionDetail(
|
|
session,
|
|
parsedSession.messages,
|
|
subagents
|
|
);
|
|
|
|
// Cache the result
|
|
services.dataCache.set(cacheKey, sessionDetail);
|
|
|
|
return sessionDetail;
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error in GET session-detail for ${request.params.projectId}/${request.params.sessionId}:`,
|
|
error
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Conversation groups
|
|
app.get<{ Params: { projectId: string; sessionId: string } }>(
|
|
'/api/projects/:projectId/sessions/:sessionId/groups',
|
|
async (request) => {
|
|
try {
|
|
const validatedProject = validateProjectId(request.params.projectId);
|
|
const validatedSession = validateSessionId(request.params.sessionId);
|
|
if (!validatedProject.valid || !validatedSession.valid) {
|
|
logger.error(
|
|
`GET session-groups rejected: ${validatedProject.error ?? validatedSession.error ?? 'unknown'}`
|
|
);
|
|
return [];
|
|
}
|
|
|
|
const safeProjectId = validatedProject.value!;
|
|
const safeSessionId = validatedSession.value!;
|
|
|
|
const parsedSession = await services.sessionParser.parseSession(
|
|
safeProjectId,
|
|
safeSessionId
|
|
);
|
|
|
|
const subagents = await services.subagentResolver.resolveSubagents(
|
|
safeProjectId,
|
|
safeSessionId,
|
|
parsedSession.taskCalls,
|
|
parsedSession.messages
|
|
);
|
|
|
|
const groups = services.chunkBuilder.buildGroups(parsedSession.messages, subagents);
|
|
return groups;
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error in GET session-groups for ${request.params.projectId}/${request.params.sessionId}:`,
|
|
error
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
);
|
|
|
|
// Session metrics
|
|
app.get<{ Params: { projectId: string; sessionId: string } }>(
|
|
'/api/projects/:projectId/sessions/:sessionId/metrics',
|
|
async (request) => {
|
|
try {
|
|
const validatedProject = validateProjectId(request.params.projectId);
|
|
const validatedSession = validateSessionId(request.params.sessionId);
|
|
if (!validatedProject.valid || !validatedSession.valid) {
|
|
return null;
|
|
}
|
|
|
|
const safeProjectId = validatedProject.value!;
|
|
const safeSessionId = validatedSession.value!;
|
|
|
|
// Try cache first
|
|
const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
|
|
const cached = services.dataCache.get(cacheKey);
|
|
if (cached) {
|
|
return cached.metrics;
|
|
}
|
|
|
|
const parsedSession = await services.sessionParser.parseSession(
|
|
safeProjectId,
|
|
safeSessionId
|
|
);
|
|
return parsedSession.metrics;
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error in GET session-metrics for ${request.params.projectId}/${request.params.sessionId}:`,
|
|
error
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Waterfall data
|
|
app.get<{ Params: { projectId: string; sessionId: string } }>(
|
|
'/api/projects/:projectId/sessions/:sessionId/waterfall',
|
|
async (request) => {
|
|
try {
|
|
const validatedProject = validateProjectId(request.params.projectId);
|
|
const validatedSession = validateSessionId(request.params.sessionId);
|
|
if (!validatedProject.valid || !validatedSession.valid) {
|
|
return null;
|
|
}
|
|
|
|
const safeProjectId = validatedProject.value!;
|
|
const safeSessionId = validatedSession.value!;
|
|
const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
|
|
|
|
// Try cache first for session detail
|
|
let detail = services.dataCache.get(cacheKey);
|
|
|
|
if (!detail) {
|
|
const session = await services.projectScanner.getSession(safeProjectId, safeSessionId);
|
|
if (!session) return null;
|
|
|
|
const parsedSession = await services.sessionParser.parseSession(
|
|
safeProjectId,
|
|
safeSessionId
|
|
);
|
|
const subagents = await services.subagentResolver.resolveSubagents(
|
|
safeProjectId,
|
|
safeSessionId,
|
|
parsedSession.taskCalls,
|
|
parsedSession.messages
|
|
);
|
|
|
|
detail = services.chunkBuilder.buildSessionDetail(
|
|
session,
|
|
parsedSession.messages,
|
|
subagents
|
|
);
|
|
services.dataCache.set(cacheKey, detail);
|
|
}
|
|
|
|
return services.chunkBuilder.buildWaterfallData(detail.chunks, detail.processes);
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error in GET waterfall for ${request.params.projectId}/${request.params.sessionId}:`,
|
|
error
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
);
|
|
}
|