22230a3e3f
- 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
101 lines
3.8 KiB
TypeScript
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);
|
|
}
|
|
},
|
|
};
|
|
}
|