Files
flynn/src/gateway/handlers/sessions.ts
T
William Valentin 22230a3e3f feat: add web UI dashboard SPA with dashboard, chat, sessions, and settings pages
- Add SPA shell with hash-based router, sidebar navigation, and WebSocket RPC client
- Add dashboard page with system health cards, channel status, and auto-refresh
- Add chat page with session selector, streaming tool events, and markdown rendering
- Add sessions page with list, history viewer, and delete functionality
- Add settings page with hook pattern editor, tool list, and config viewer
- Add backend handlers: sessions.delete, sessions.switch, system.channels, system.usage
- Wire channelRegistry into gateway server for channel status reporting
- Extend static file server with .mjs, .png, .ico, .woff2 content types
2026-02-07 10:07:45 -08:00

101 lines
3.8 KiB
TypeScript

import type { GatewayRequest, OutboundMessage } from '../protocol.js';
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
import type { SessionManager } from '../../session/manager.js';
import type { SessionBridge } from '../session-bridge.js';
export interface SessionHandlerDeps {
sessionManager: SessionManager;
sessionBridge?: SessionBridge;
}
export function createSessionHandlers(deps: SessionHandlerDeps) {
return {
'sessions.list': async (request: GatewayRequest): Promise<OutboundMessage> => {
const sessionIds = deps.sessionManager.listSessions();
const sessions = sessionIds.map(id => ({
id,
messageCount: deps.sessionManager.getSession(
id.split(':')[0],
id.split(':').slice(1).join(':')
).getHistory().length,
}));
return makeResponse(request.id, { sessions });
},
'sessions.history': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { sessionId?: string; limit?: number; offset?: number } | undefined;
if (!params?.sessionId) {
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
}
const { sessionId, limit, offset } = params;
const parts = sessionId.split(':');
const frontend = parts[0];
const userId = parts.slice(1).join(':');
const session = deps.sessionManager.getSession(frontend, userId);
const allMessages = session.getHistory();
const start = offset ?? 0;
const end = limit ? start + limit : allMessages.length;
const messages = allMessages.slice(start, end);
return makeResponse(request.id, {
messages,
total: allMessages.length,
});
},
'sessions.create': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { sessionId?: string } | undefined;
const sessionId = params?.sessionId ?? `ws:${Date.now()}`;
const parts = sessionId.split(':');
const frontend = parts[0];
const userId = parts.slice(1).join(':');
// Creating a session via getSession is idempotent
deps.sessionManager.getSession(frontend, userId);
return makeResponse(request.id, { sessionId });
},
'sessions.delete': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { sessionId?: string } | undefined;
if (!params?.sessionId) {
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
}
const { sessionId } = params;
const parts = sessionId.split(':');
const frontend = parts[0];
const userId = parts.slice(1).join(':');
const session = deps.sessionManager.getSession(frontend, userId);
session.clear();
return makeResponse(request.id, { deleted: true, sessionId });
},
'sessions.switch': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { sessionId?: string; connectionId?: string } | undefined;
if (!params?.sessionId) {
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
}
if (!deps.sessionBridge) {
return makeError(request.id, ErrorCode.InternalError, 'Session switching not available');
}
const connectionId = params.connectionId as string;
if (!connectionId) {
return makeError(request.id, ErrorCode.InvalidRequest, 'connectionId is required');
}
try {
deps.sessionBridge.switchSession(connectionId, params.sessionId);
return makeResponse(request.id, { switched: true, sessionId: params.sessionId });
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to switch session';
return makeError(request.id, ErrorCode.InternalError, message);
}
},
};
}