mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
refactor: extract LiveSessionManager and CallbackRegistry from WebServer
- Extract LiveSessionManager (178 lines) for live session and AutoRun state tracking - Extract CallbackRegistry (208 lines) for centralized callback management - Reduce WebServer.ts from 778 to 582 lines (25% reduction) - Add managers/ directory with proper exports - Maintain consistent public API (no breaking changes)
This commit is contained in:
@@ -34,6 +34,7 @@ import { logger } from '../utils/logger';
|
||||
import { WebSocketMessageHandler } from './handlers';
|
||||
import { BroadcastService } from './services';
|
||||
import { ApiRoutes, StaticRoutes, WsRoute } from './routes';
|
||||
import { LiveSessionManager, CallbackRegistry } from './managers';
|
||||
|
||||
// Import shared types from canonical location
|
||||
import type {
|
||||
@@ -80,20 +81,6 @@ export class WebServer {
|
||||
private isRunning: boolean = false;
|
||||
private webClients: Map<string, WebClient> = new Map();
|
||||
private rateLimitConfig: RateLimitConfig = { ...DEFAULT_RATE_LIMIT_CONFIG };
|
||||
private getSessionsCallback: GetSessionsCallback | null = null;
|
||||
private getSessionDetailCallback: GetSessionDetailCallback | null = null;
|
||||
private getThemeCallback: GetThemeCallback | null = null;
|
||||
private getCustomCommandsCallback: GetCustomCommandsCallback | null = null;
|
||||
private writeToSessionCallback: WriteToSessionCallback | null = null;
|
||||
private executeCommandCallback: ExecuteCommandCallback | null = null;
|
||||
private interruptSessionCallback: InterruptSessionCallback | null = null;
|
||||
private switchModeCallback: SwitchModeCallback | null = null;
|
||||
private selectSessionCallback: SelectSessionCallback | null = null;
|
||||
private selectTabCallback: SelectTabCallback | null = null;
|
||||
private newTabCallback: NewTabCallback | null = null;
|
||||
private closeTabCallback: CloseTabCallback | null = null;
|
||||
private renameTabCallback: RenameTabCallback | null = null;
|
||||
private getHistoryCallback: GetHistoryCallback | null = null;
|
||||
private webAssetsPath: string | null = null;
|
||||
|
||||
// Security token - regenerated on each app startup
|
||||
@@ -102,11 +89,9 @@ export class WebServer {
|
||||
// Local IP address for generating URLs (detected at startup)
|
||||
private localIpAddress: string = 'localhost';
|
||||
|
||||
// Live sessions - only these appear in the web interface
|
||||
private liveSessions: Map<string, LiveSessionInfo> = new Map();
|
||||
|
||||
// AutoRun states per session - tracks which sessions have active batch processing
|
||||
private autoRunStates: Map<string, AutoRunState> = new Map();
|
||||
// Extracted managers
|
||||
private liveSessionManager: LiveSessionManager;
|
||||
private callbackRegistry: CallbackRegistry;
|
||||
|
||||
// WebSocket message handler instance
|
||||
private messageHandler: WebSocketMessageHandler;
|
||||
@@ -114,7 +99,7 @@ export class WebServer {
|
||||
// Broadcast service instance
|
||||
private broadcastService: BroadcastService;
|
||||
|
||||
// Route instances (extracted from web-server.ts)
|
||||
// Route instances
|
||||
private apiRoutes: ApiRoutes;
|
||||
private staticRoutes: StaticRoutes;
|
||||
private wsRoute: WsRoute;
|
||||
@@ -135,6 +120,10 @@ export class WebServer {
|
||||
// Determine web assets path (production vs development)
|
||||
this.webAssetsPath = this.resolveWebAssetsPath();
|
||||
|
||||
// Initialize managers
|
||||
this.liveSessionManager = new LiveSessionManager();
|
||||
this.callbackRegistry = new CallbackRegistry();
|
||||
|
||||
// Initialize the WebSocket message handler
|
||||
this.messageHandler = new WebSocketMessageHandler();
|
||||
|
||||
@@ -142,6 +131,16 @@ export class WebServer {
|
||||
this.broadcastService = new BroadcastService();
|
||||
this.broadcastService.setGetWebClientsCallback(() => this.webClients);
|
||||
|
||||
// Wire up live session manager to broadcast service
|
||||
this.liveSessionManager.setBroadcastCallbacks({
|
||||
broadcastSessionLive: (sessionId, agentSessionId) =>
|
||||
this.broadcastService.broadcastSessionLive(sessionId, agentSessionId),
|
||||
broadcastSessionOffline: (sessionId) =>
|
||||
this.broadcastService.broadcastSessionOffline(sessionId),
|
||||
broadcastAutoRunState: (sessionId, state) =>
|
||||
this.broadcastService.broadcastAutoRunState(sessionId, state),
|
||||
});
|
||||
|
||||
// Initialize route handlers
|
||||
this.apiRoutes = new ApiRoutes(this.securityToken, this.rateLimitConfig);
|
||||
this.staticRoutes = new StaticRoutes(this.securityToken, this.webAssetsPath);
|
||||
@@ -180,60 +179,34 @@ export class WebServer {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============ Live Session Management ============
|
||||
// ============ Live Session Management (Delegated to LiveSessionManager) ============
|
||||
|
||||
/**
|
||||
* Mark a session as live (visible in web interface)
|
||||
*/
|
||||
setSessionLive(sessionId: string, agentSessionId?: string): void {
|
||||
this.liveSessions.set(sessionId, {
|
||||
sessionId,
|
||||
agentSessionId,
|
||||
enabledAt: Date.now(),
|
||||
});
|
||||
logger.info(
|
||||
`Session ${sessionId} marked as live (total: ${this.liveSessions.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.broadcastService.broadcastSessionLive(sessionId, agentSessionId);
|
||||
this.liveSessionManager.setSessionLive(sessionId, agentSessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as offline (no longer visible in web interface)
|
||||
*/
|
||||
setSessionOffline(sessionId: string): void {
|
||||
const wasLive = this.liveSessions.delete(sessionId);
|
||||
if (wasLive) {
|
||||
logger.info(
|
||||
`Session ${sessionId} marked as offline (remaining: ${this.liveSessions.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
|
||||
// Clean up any associated AutoRun state to prevent memory leaks
|
||||
if (this.autoRunStates.has(sessionId)) {
|
||||
this.autoRunStates.delete(sessionId);
|
||||
logger.debug(`Cleaned up AutoRun state for offline session ${sessionId}`, LOG_CONTEXT);
|
||||
}
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.broadcastService.broadcastSessionOffline(sessionId);
|
||||
}
|
||||
this.liveSessionManager.setSessionOffline(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session is currently live
|
||||
*/
|
||||
isSessionLive(sessionId: string): boolean {
|
||||
return this.liveSessions.has(sessionId);
|
||||
return this.liveSessionManager.isSessionLive(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all live session IDs
|
||||
*/
|
||||
getLiveSessions(): LiveSessionInfo[] {
|
||||
return Array.from(this.liveSessions.values());
|
||||
return this.liveSessionManager.getLiveSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,128 +232,67 @@ export class WebServer {
|
||||
return `http://${this.localIpAddress}:${this.port}/${this.securityToken}/session/${sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for fetching current sessions list
|
||||
* This is called when a new client connects to send the initial state
|
||||
*/
|
||||
setGetSessionsCallback(callback: GetSessionsCallback) {
|
||||
this.getSessionsCallback = callback;
|
||||
// ============ Callback Setters (Delegated to CallbackRegistry) ============
|
||||
|
||||
setGetSessionsCallback(callback: GetSessionsCallback): void {
|
||||
this.callbackRegistry.setGetSessionsCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for fetching single session details
|
||||
* This is called by the /api/session/:id endpoint
|
||||
*/
|
||||
setGetSessionDetailCallback(callback: GetSessionDetailCallback) {
|
||||
this.getSessionDetailCallback = callback;
|
||||
setGetSessionDetailCallback(callback: GetSessionDetailCallback): void {
|
||||
this.callbackRegistry.setGetSessionDetailCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for fetching current theme
|
||||
* This is called when a new client connects to send the initial theme
|
||||
*/
|
||||
setGetThemeCallback(callback: GetThemeCallback) {
|
||||
this.getThemeCallback = callback;
|
||||
setGetThemeCallback(callback: GetThemeCallback): void {
|
||||
this.callbackRegistry.setGetThemeCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for fetching custom AI commands
|
||||
* This is called when a new client connects to send the initial custom commands
|
||||
*/
|
||||
setGetCustomCommandsCallback(callback: GetCustomCommandsCallback) {
|
||||
this.getCustomCommandsCallback = callback;
|
||||
setGetCustomCommandsCallback(callback: GetCustomCommandsCallback): void {
|
||||
this.callbackRegistry.setGetCustomCommandsCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for writing commands to a session
|
||||
* This is called by the /api/session/:id/send endpoint
|
||||
*/
|
||||
setWriteToSessionCallback(callback: WriteToSessionCallback) {
|
||||
this.writeToSessionCallback = callback;
|
||||
setWriteToSessionCallback(callback: WriteToSessionCallback): void {
|
||||
this.callbackRegistry.setWriteToSessionCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for executing commands through the desktop
|
||||
* This forwards commands to the renderer which handles spawn, state management, and broadcasts
|
||||
*/
|
||||
setExecuteCommandCallback(callback: ExecuteCommandCallback) {
|
||||
this.executeCommandCallback = callback;
|
||||
setExecuteCommandCallback(callback: ExecuteCommandCallback): void {
|
||||
this.callbackRegistry.setExecuteCommandCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for interrupting a session through the desktop
|
||||
* This forwards to the renderer which handles state updates and broadcasts
|
||||
*/
|
||||
setInterruptSessionCallback(callback: InterruptSessionCallback) {
|
||||
this.interruptSessionCallback = callback;
|
||||
setInterruptSessionCallback(callback: InterruptSessionCallback): void {
|
||||
this.callbackRegistry.setInterruptSessionCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for switching session mode through the desktop
|
||||
* This forwards to the renderer which handles state updates and broadcasts
|
||||
*/
|
||||
setSwitchModeCallback(callback: SwitchModeCallback) {
|
||||
logger.info('[WebServer] setSwitchModeCallback called', LOG_CONTEXT);
|
||||
this.switchModeCallback = callback;
|
||||
setSwitchModeCallback(callback: SwitchModeCallback): void {
|
||||
this.callbackRegistry.setSwitchModeCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for selecting/switching to a session in the desktop
|
||||
* This forwards to the renderer which handles state updates and broadcasts
|
||||
*/
|
||||
setSelectSessionCallback(callback: SelectSessionCallback) {
|
||||
logger.info('[WebServer] setSelectSessionCallback called', LOG_CONTEXT);
|
||||
this.selectSessionCallback = callback;
|
||||
setSelectSessionCallback(callback: SelectSessionCallback): void {
|
||||
this.callbackRegistry.setSelectSessionCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for selecting a tab within a session
|
||||
* This forwards to the renderer which handles tab state updates and broadcasts
|
||||
*/
|
||||
setSelectTabCallback(callback: SelectTabCallback) {
|
||||
logger.info('[WebServer] setSelectTabCallback called', LOG_CONTEXT);
|
||||
this.selectTabCallback = callback;
|
||||
setSelectTabCallback(callback: SelectTabCallback): void {
|
||||
this.callbackRegistry.setSelectTabCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for creating a new tab within a session
|
||||
* This forwards to the renderer which handles tab creation and broadcasts
|
||||
*/
|
||||
setNewTabCallback(callback: NewTabCallback) {
|
||||
logger.info('[WebServer] setNewTabCallback called', LOG_CONTEXT);
|
||||
this.newTabCallback = callback;
|
||||
setNewTabCallback(callback: NewTabCallback): void {
|
||||
this.callbackRegistry.setNewTabCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for closing a tab within a session
|
||||
* This forwards to the renderer which handles tab removal and broadcasts
|
||||
*/
|
||||
setCloseTabCallback(callback: CloseTabCallback) {
|
||||
logger.info('[WebServer] setCloseTabCallback called', LOG_CONTEXT);
|
||||
this.closeTabCallback = callback;
|
||||
setCloseTabCallback(callback: CloseTabCallback): void {
|
||||
this.callbackRegistry.setCloseTabCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for renaming a tab within a session
|
||||
* This forwards to the renderer which handles tab rename and broadcasts
|
||||
*/
|
||||
setRenameTabCallback(callback: RenameTabCallback) {
|
||||
logger.info('[WebServer] setRenameTabCallback called', LOG_CONTEXT);
|
||||
this.renameTabCallback = callback;
|
||||
setRenameTabCallback(callback: RenameTabCallback): void {
|
||||
this.callbackRegistry.setRenameTabCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback function for fetching history entries
|
||||
* This is called by the /api/history endpoint
|
||||
*/
|
||||
setGetHistoryCallback(callback: GetHistoryCallback) {
|
||||
this.getHistoryCallback = callback;
|
||||
setGetHistoryCallback(callback: GetHistoryCallback): void {
|
||||
this.callbackRegistry.setGetHistoryCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rate limiting configuration
|
||||
*/
|
||||
setRateLimitConfig(config: Partial<RateLimitConfig>) {
|
||||
// ============ Rate Limiting ============
|
||||
|
||||
setRateLimitConfig(config: Partial<RateLimitConfig>): void {
|
||||
this.rateLimitConfig = { ...this.rateLimitConfig, ...config };
|
||||
logger.info(
|
||||
`Rate limiting ${this.rateLimitConfig.enabled ? 'enabled' : 'disabled'} (max: ${this.rateLimitConfig.max}/min, maxPost: ${this.rateLimitConfig.maxPost}/min)`,
|
||||
@@ -388,14 +300,13 @@ export class WebServer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current rate limiting configuration
|
||||
*/
|
||||
getRateLimitConfig(): RateLimitConfig {
|
||||
return { ...this.rateLimitConfig };
|
||||
}
|
||||
|
||||
private async setupMiddleware() {
|
||||
// ============ Server Setup ============
|
||||
|
||||
private async setupMiddleware(): Promise<void> {
|
||||
// Enable CORS for web access
|
||||
await this.server.register(cors, {
|
||||
origin: true,
|
||||
@@ -450,36 +361,33 @@ export class WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup all routes by delegating to extracted route classes
|
||||
*/
|
||||
private setupRoutes() {
|
||||
private setupRoutes(): void {
|
||||
// Setup static routes (dashboard, PWA files, health check)
|
||||
this.staticRoutes.registerRoutes(this.server);
|
||||
|
||||
// Setup API routes callbacks and register routes
|
||||
this.apiRoutes.setCallbacks({
|
||||
getSessions: () => this.getSessionsCallback?.() ?? [],
|
||||
getSessions: () => this.callbackRegistry.getSessions(),
|
||||
getSessionDetail: (sessionId, tabId) =>
|
||||
this.getSessionDetailCallback?.(sessionId, tabId) ?? null,
|
||||
getTheme: () => this.getThemeCallback?.() ?? null,
|
||||
writeToSession: (sessionId, data) => this.writeToSessionCallback?.(sessionId, data) ?? false,
|
||||
interruptSession: async (sessionId) => this.interruptSessionCallback?.(sessionId) ?? false,
|
||||
this.callbackRegistry.getSessionDetail(sessionId, tabId),
|
||||
getTheme: () => this.callbackRegistry.getTheme(),
|
||||
writeToSession: (sessionId, data) => this.callbackRegistry.writeToSession(sessionId, data),
|
||||
interruptSession: async (sessionId) => this.callbackRegistry.interruptSession(sessionId),
|
||||
getHistory: (projectPath, sessionId) =>
|
||||
this.getHistoryCallback?.(projectPath, sessionId) ?? [],
|
||||
getLiveSessionInfo: (sessionId) => this.liveSessions.get(sessionId),
|
||||
isSessionLive: (sessionId) => this.liveSessions.has(sessionId),
|
||||
this.callbackRegistry.getHistory(projectPath, sessionId),
|
||||
getLiveSessionInfo: (sessionId) => this.liveSessionManager.getLiveSessionInfo(sessionId),
|
||||
isSessionLive: (sessionId) => this.liveSessionManager.isSessionLive(sessionId),
|
||||
});
|
||||
this.apiRoutes.registerRoutes(this.server);
|
||||
|
||||
// Setup WebSocket route callbacks and register route
|
||||
this.wsRoute.setCallbacks({
|
||||
getSessions: () => this.getSessionsCallback?.() ?? [],
|
||||
getTheme: () => this.getThemeCallback?.() ?? null,
|
||||
getCustomCommands: () => this.getCustomCommandsCallback?.() ?? [],
|
||||
getAutoRunStates: () => this.autoRunStates,
|
||||
getLiveSessionInfo: (sessionId) => this.liveSessions.get(sessionId),
|
||||
isSessionLive: (sessionId) => this.liveSessions.has(sessionId),
|
||||
getSessions: () => this.callbackRegistry.getSessions(),
|
||||
getTheme: () => this.callbackRegistry.getTheme(),
|
||||
getCustomCommands: () => this.callbackRegistry.getCustomCommands(),
|
||||
getAutoRunStates: () => this.liveSessionManager.getAutoRunStates(),
|
||||
getLiveSessionInfo: (sessionId) => this.liveSessionManager.getLiveSessionInfo(sessionId),
|
||||
isSessionLive: (sessionId) => this.liveSessionManager.isSessionLive(sessionId),
|
||||
onClientConnect: (client) => {
|
||||
this.webClients.set(client.id, client);
|
||||
logger.info(`Client connected: ${client.id} (total: ${this.webClients.size})`, LOG_CONTEXT);
|
||||
@@ -501,36 +409,45 @@ export class WebServer {
|
||||
this.wsRoute.registerRoute(this.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages from web clients
|
||||
* Delegates to the WebSocketMessageHandler for all message processing
|
||||
*/
|
||||
private handleWebClientMessage(clientId: string, message: WebClientMessage) {
|
||||
private handleWebClientMessage(clientId: string, message: WebClientMessage): void {
|
||||
const client = this.webClients.get(clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Delegate to the message handler
|
||||
this.messageHandler.handleMessage(client, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to all connected web clients
|
||||
*/
|
||||
private setupMessageHandlerCallbacks(): void {
|
||||
this.messageHandler.setCallbacks({
|
||||
getSessionDetail: (sessionId: string) => this.callbackRegistry.getSessionDetail(sessionId),
|
||||
executeCommand: async (sessionId: string, command: string, inputMode?: 'ai' | 'terminal') =>
|
||||
this.callbackRegistry.executeCommand(sessionId, command, inputMode),
|
||||
switchMode: async (sessionId: string, mode: 'ai' | 'terminal') =>
|
||||
this.callbackRegistry.switchMode(sessionId, mode),
|
||||
selectSession: async (sessionId: string, tabId?: string) =>
|
||||
this.callbackRegistry.selectSession(sessionId, tabId),
|
||||
selectTab: async (sessionId: string, tabId: string) =>
|
||||
this.callbackRegistry.selectTab(sessionId, tabId),
|
||||
newTab: async (sessionId: string) => this.callbackRegistry.newTab(sessionId),
|
||||
closeTab: async (sessionId: string, tabId: string) =>
|
||||
this.callbackRegistry.closeTab(sessionId, tabId),
|
||||
renameTab: async (sessionId: string, tabId: string, newName: string) =>
|
||||
this.callbackRegistry.renameTab(sessionId, tabId, newName),
|
||||
getSessions: () => this.callbackRegistry.getSessions(),
|
||||
getLiveSessionInfo: (sessionId: string) =>
|
||||
this.liveSessionManager.getLiveSessionInfo(sessionId),
|
||||
isSessionLive: (sessionId: string) => this.liveSessionManager.isSessionLive(sessionId),
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Broadcast Methods (Delegated to BroadcastService) ============
|
||||
|
||||
broadcastToWebClients(message: object): void {
|
||||
this.broadcastService.broadcastToAll(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to clients subscribed to a specific session
|
||||
*/
|
||||
broadcastToSessionClients(sessionId: string, message: object): void {
|
||||
this.broadcastService.broadcastToSession(sessionId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a session state change to all connected web clients
|
||||
* Called when any session's state changes (idle, busy, error, connecting)
|
||||
*/
|
||||
broadcastSessionStateChange(
|
||||
sessionId: string,
|
||||
state: string,
|
||||
@@ -545,150 +462,48 @@ export class WebServer {
|
||||
this.broadcastService.broadcastSessionStateChange(sessionId, state, additionalData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast when a session is added
|
||||
*/
|
||||
broadcastSessionAdded(session: SessionBroadcastData): void {
|
||||
this.broadcastService.broadcastSessionAdded(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast when a session is removed
|
||||
*/
|
||||
broadcastSessionRemoved(sessionId: string): void {
|
||||
this.broadcastService.broadcastSessionRemoved(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast the full sessions list to all connected web clients
|
||||
* Used for initial sync or bulk updates
|
||||
*/
|
||||
broadcastSessionsList(sessions: SessionBroadcastData[]): void {
|
||||
this.broadcastService.broadcastSessionsList(sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast active session change to all connected web clients
|
||||
* Called when the user switches sessions in the desktop app
|
||||
*/
|
||||
broadcastActiveSessionChange(sessionId: string): void {
|
||||
this.broadcastService.broadcastActiveSessionChange(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast tab change to all connected web clients
|
||||
* Called when the tabs array or active tab changes in a session
|
||||
*/
|
||||
broadcastTabsChange(sessionId: string, aiTabs: AITabData[], activeTabId: string): void {
|
||||
this.broadcastService.broadcastTabsChange(sessionId, aiTabs, activeTabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast theme change to all connected web clients
|
||||
* Called when the user changes the theme in the desktop app
|
||||
*/
|
||||
broadcastThemeChange(theme: Theme): void {
|
||||
this.broadcastService.broadcastThemeChange(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast custom commands update to all connected web clients
|
||||
* Called when the user modifies custom AI commands in the desktop app
|
||||
*/
|
||||
broadcastCustomCommands(commands: CustomAICommand[]): void {
|
||||
this.broadcastService.broadcastCustomCommands(commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast AutoRun state to all connected web clients
|
||||
* Called when batch processing starts, progresses, or stops
|
||||
* Also stores state locally so new clients can receive it on connect
|
||||
*/
|
||||
broadcastAutoRunState(sessionId: string, state: AutoRunState | null): void {
|
||||
// Store state locally for new clients connecting later
|
||||
if (state && state.isRunning) {
|
||||
this.autoRunStates.set(sessionId, state);
|
||||
logger.info(
|
||||
`AutoRun state stored for session ${sessionId}: tasks=${state.completedTasks}/${state.totalTasks} (total stored: ${this.autoRunStates.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
} else {
|
||||
const wasStored = this.autoRunStates.has(sessionId);
|
||||
this.autoRunStates.delete(sessionId);
|
||||
if (wasStored) {
|
||||
logger.info(
|
||||
`AutoRun state removed for session ${sessionId} (total stored: ${this.autoRunStates.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
}
|
||||
}
|
||||
this.broadcastService.broadcastAutoRunState(sessionId, state);
|
||||
this.liveSessionManager.setAutoRunState(sessionId, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast user input to web clients subscribed to a session
|
||||
* Called when a command is sent from the desktop app so web clients stay in sync
|
||||
*/
|
||||
broadcastUserInput(sessionId: string, command: string, inputMode: 'ai' | 'terminal'): void {
|
||||
this.broadcastService.broadcastUserInput(sessionId, command, inputMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of connected web clients
|
||||
*/
|
||||
// ============ Server Lifecycle ============
|
||||
|
||||
getWebClientCount(): number {
|
||||
return this.webClients.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up the message handler callbacks
|
||||
* Called during start() to ensure all callbacks are set before accepting connections
|
||||
*/
|
||||
private setupMessageHandlerCallbacks(): void {
|
||||
this.messageHandler.setCallbacks({
|
||||
getSessionDetail: (sessionId: string) => {
|
||||
return this.getSessionDetailCallback?.(sessionId) ?? null;
|
||||
},
|
||||
executeCommand: async (sessionId: string, command: string, inputMode?: 'ai' | 'terminal') => {
|
||||
if (!this.executeCommandCallback) return false;
|
||||
return this.executeCommandCallback(sessionId, command, inputMode);
|
||||
},
|
||||
switchMode: async (sessionId: string, mode: 'ai' | 'terminal') => {
|
||||
if (!this.switchModeCallback) return false;
|
||||
return this.switchModeCallback(sessionId, mode);
|
||||
},
|
||||
selectSession: async (sessionId: string, tabId?: string) => {
|
||||
if (!this.selectSessionCallback) return false;
|
||||
return this.selectSessionCallback(sessionId, tabId);
|
||||
},
|
||||
selectTab: async (sessionId: string, tabId: string) => {
|
||||
if (!this.selectTabCallback) return false;
|
||||
return this.selectTabCallback(sessionId, tabId);
|
||||
},
|
||||
newTab: async (sessionId: string) => {
|
||||
if (!this.newTabCallback) return null;
|
||||
return this.newTabCallback(sessionId);
|
||||
},
|
||||
closeTab: async (sessionId: string, tabId: string) => {
|
||||
if (!this.closeTabCallback) return false;
|
||||
return this.closeTabCallback(sessionId, tabId);
|
||||
},
|
||||
renameTab: async (sessionId: string, tabId: string, newName: string) => {
|
||||
if (!this.renameTabCallback) return false;
|
||||
return this.renameTabCallback(sessionId, tabId, newName);
|
||||
},
|
||||
getSessions: () => {
|
||||
return this.getSessionsCallback?.() ?? [];
|
||||
},
|
||||
getLiveSessionInfo: (sessionId: string) => {
|
||||
return this.liveSessions.get(sessionId);
|
||||
},
|
||||
isSessionLive: (sessionId: string) => {
|
||||
return this.liveSessions.has(sessionId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<{ port: number; token: string; url: string }> {
|
||||
if (this.isRunning) {
|
||||
return {
|
||||
@@ -731,24 +546,13 @@ export class WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark all live sessions as offline (this also cleans up autoRunStates)
|
||||
for (const sessionId of this.liveSessions.keys()) {
|
||||
this.setSessionOffline(sessionId);
|
||||
}
|
||||
|
||||
// Clear any remaining autoRunStates as a safety measure
|
||||
if (this.autoRunStates.size > 0) {
|
||||
logger.debug(
|
||||
`Clearing ${this.autoRunStates.size} remaining AutoRun states on server stop`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
this.autoRunStates.clear();
|
||||
}
|
||||
// Clear all session state (handles live sessions and autorun states)
|
||||
this.liveSessionManager.clearAll();
|
||||
|
||||
try {
|
||||
await this.server.close();
|
||||
|
||||
@@ -57,6 +57,10 @@ export type { SessionDetailForHandler, MessageHandlerCallbacks } from './handler
|
||||
export { BroadcastService } from './services';
|
||||
export type { WebClientInfo } from './services';
|
||||
|
||||
// ============ Managers ============
|
||||
export { LiveSessionManager, CallbackRegistry } from './managers';
|
||||
export type { LiveSessionBroadcastCallbacks, WebServerCallbacks } from './managers';
|
||||
|
||||
// ============ Routes ============
|
||||
export { ApiRoutes, StaticRoutes, WsRoute } from './routes';
|
||||
export type { ApiRouteCallbacks, WsRouteCallbacks, WsSessionData } from './routes';
|
||||
|
||||
204
src/main/web-server/managers/CallbackRegistry.ts
Normal file
204
src/main/web-server/managers/CallbackRegistry.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* CallbackRegistry - Manages callback functions for the WebServer
|
||||
*
|
||||
* Centralizes all callback storage and provides typed getter/setter methods.
|
||||
* This separates callback management from the core WebServer logic.
|
||||
*/
|
||||
|
||||
import { logger } from '../../utils/logger';
|
||||
import type {
|
||||
GetSessionsCallback,
|
||||
GetSessionDetailCallback,
|
||||
WriteToSessionCallback,
|
||||
ExecuteCommandCallback,
|
||||
InterruptSessionCallback,
|
||||
SwitchModeCallback,
|
||||
SelectSessionCallback,
|
||||
SelectTabCallback,
|
||||
NewTabCallback,
|
||||
CloseTabCallback,
|
||||
RenameTabCallback,
|
||||
GetThemeCallback,
|
||||
GetCustomCommandsCallback,
|
||||
GetHistoryCallback,
|
||||
} from '../types';
|
||||
|
||||
const LOG_CONTEXT = 'CallbackRegistry';
|
||||
|
||||
/**
|
||||
* All callback types supported by the WebServer
|
||||
*/
|
||||
export interface WebServerCallbacks {
|
||||
getSessions: GetSessionsCallback | null;
|
||||
getSessionDetail: GetSessionDetailCallback | null;
|
||||
getTheme: GetThemeCallback | null;
|
||||
getCustomCommands: GetCustomCommandsCallback | null;
|
||||
writeToSession: WriteToSessionCallback | null;
|
||||
executeCommand: ExecuteCommandCallback | null;
|
||||
interruptSession: InterruptSessionCallback | null;
|
||||
switchMode: SwitchModeCallback | null;
|
||||
selectSession: SelectSessionCallback | null;
|
||||
selectTab: SelectTabCallback | null;
|
||||
newTab: NewTabCallback | null;
|
||||
closeTab: CloseTabCallback | null;
|
||||
renameTab: RenameTabCallback | null;
|
||||
getHistory: GetHistoryCallback | null;
|
||||
}
|
||||
|
||||
export class CallbackRegistry {
|
||||
private callbacks: WebServerCallbacks = {
|
||||
getSessions: null,
|
||||
getSessionDetail: null,
|
||||
getTheme: null,
|
||||
getCustomCommands: null,
|
||||
writeToSession: null,
|
||||
executeCommand: null,
|
||||
interruptSession: null,
|
||||
switchMode: null,
|
||||
selectSession: null,
|
||||
selectTab: null,
|
||||
newTab: null,
|
||||
closeTab: null,
|
||||
renameTab: null,
|
||||
getHistory: null,
|
||||
};
|
||||
|
||||
// ============ Getter Methods ============
|
||||
|
||||
getSessions(): ReturnType<GetSessionsCallback> | [] {
|
||||
return this.callbacks.getSessions?.() ?? [];
|
||||
}
|
||||
|
||||
getSessionDetail(sessionId: string, tabId?: string): ReturnType<GetSessionDetailCallback> | null {
|
||||
return this.callbacks.getSessionDetail?.(sessionId, tabId) ?? null;
|
||||
}
|
||||
|
||||
getTheme(): ReturnType<GetThemeCallback> | null {
|
||||
return this.callbacks.getTheme?.() ?? null;
|
||||
}
|
||||
|
||||
getCustomCommands(): ReturnType<GetCustomCommandsCallback> | [] {
|
||||
return this.callbacks.getCustomCommands?.() ?? [];
|
||||
}
|
||||
|
||||
writeToSession(sessionId: string, data: string): boolean {
|
||||
return this.callbacks.writeToSession?.(sessionId, data) ?? false;
|
||||
}
|
||||
|
||||
async executeCommand(
|
||||
sessionId: string,
|
||||
command: string,
|
||||
inputMode?: 'ai' | 'terminal'
|
||||
): Promise<boolean> {
|
||||
if (!this.callbacks.executeCommand) return false;
|
||||
return this.callbacks.executeCommand(sessionId, command, inputMode);
|
||||
}
|
||||
|
||||
async interruptSession(sessionId: string): Promise<boolean> {
|
||||
return this.callbacks.interruptSession?.(sessionId) ?? false;
|
||||
}
|
||||
|
||||
async switchMode(sessionId: string, mode: 'ai' | 'terminal'): Promise<boolean> {
|
||||
if (!this.callbacks.switchMode) return false;
|
||||
return this.callbacks.switchMode(sessionId, mode);
|
||||
}
|
||||
|
||||
async selectSession(sessionId: string, tabId?: string): Promise<boolean> {
|
||||
if (!this.callbacks.selectSession) return false;
|
||||
return this.callbacks.selectSession(sessionId, tabId);
|
||||
}
|
||||
|
||||
async selectTab(sessionId: string, tabId: string): Promise<boolean> {
|
||||
if (!this.callbacks.selectTab) return false;
|
||||
return this.callbacks.selectTab(sessionId, tabId);
|
||||
}
|
||||
|
||||
async newTab(sessionId: string): Promise<{ tabId: string } | null> {
|
||||
if (!this.callbacks.newTab) return null;
|
||||
return this.callbacks.newTab(sessionId);
|
||||
}
|
||||
|
||||
async closeTab(sessionId: string, tabId: string): Promise<boolean> {
|
||||
if (!this.callbacks.closeTab) return false;
|
||||
return this.callbacks.closeTab(sessionId, tabId);
|
||||
}
|
||||
|
||||
async renameTab(sessionId: string, tabId: string, newName: string): Promise<boolean> {
|
||||
if (!this.callbacks.renameTab) return false;
|
||||
return this.callbacks.renameTab(sessionId, tabId, newName);
|
||||
}
|
||||
|
||||
getHistory(projectPath?: string, sessionId?: string): ReturnType<GetHistoryCallback> | [] {
|
||||
return this.callbacks.getHistory?.(projectPath, sessionId) ?? [];
|
||||
}
|
||||
|
||||
// ============ Setter Methods ============
|
||||
|
||||
setGetSessionsCallback(callback: GetSessionsCallback): void {
|
||||
this.callbacks.getSessions = callback;
|
||||
}
|
||||
|
||||
setGetSessionDetailCallback(callback: GetSessionDetailCallback): void {
|
||||
this.callbacks.getSessionDetail = callback;
|
||||
}
|
||||
|
||||
setGetThemeCallback(callback: GetThemeCallback): void {
|
||||
this.callbacks.getTheme = callback;
|
||||
}
|
||||
|
||||
setGetCustomCommandsCallback(callback: GetCustomCommandsCallback): void {
|
||||
this.callbacks.getCustomCommands = callback;
|
||||
}
|
||||
|
||||
setWriteToSessionCallback(callback: WriteToSessionCallback): void {
|
||||
this.callbacks.writeToSession = callback;
|
||||
}
|
||||
|
||||
setExecuteCommandCallback(callback: ExecuteCommandCallback): void {
|
||||
this.callbacks.executeCommand = callback;
|
||||
}
|
||||
|
||||
setInterruptSessionCallback(callback: InterruptSessionCallback): void {
|
||||
this.callbacks.interruptSession = callback;
|
||||
}
|
||||
|
||||
setSwitchModeCallback(callback: SwitchModeCallback): void {
|
||||
logger.info('[CallbackRegistry] setSwitchModeCallback called', LOG_CONTEXT);
|
||||
this.callbacks.switchMode = callback;
|
||||
}
|
||||
|
||||
setSelectSessionCallback(callback: SelectSessionCallback): void {
|
||||
logger.info('[CallbackRegistry] setSelectSessionCallback called', LOG_CONTEXT);
|
||||
this.callbacks.selectSession = callback;
|
||||
}
|
||||
|
||||
setSelectTabCallback(callback: SelectTabCallback): void {
|
||||
logger.info('[CallbackRegistry] setSelectTabCallback called', LOG_CONTEXT);
|
||||
this.callbacks.selectTab = callback;
|
||||
}
|
||||
|
||||
setNewTabCallback(callback: NewTabCallback): void {
|
||||
logger.info('[CallbackRegistry] setNewTabCallback called', LOG_CONTEXT);
|
||||
this.callbacks.newTab = callback;
|
||||
}
|
||||
|
||||
setCloseTabCallback(callback: CloseTabCallback): void {
|
||||
logger.info('[CallbackRegistry] setCloseTabCallback called', LOG_CONTEXT);
|
||||
this.callbacks.closeTab = callback;
|
||||
}
|
||||
|
||||
setRenameTabCallback(callback: RenameTabCallback): void {
|
||||
logger.info('[CallbackRegistry] setRenameTabCallback called', LOG_CONTEXT);
|
||||
this.callbacks.renameTab = callback;
|
||||
}
|
||||
|
||||
setGetHistoryCallback(callback: GetHistoryCallback): void {
|
||||
this.callbacks.getHistory = callback;
|
||||
}
|
||||
|
||||
// ============ Check Methods ============
|
||||
|
||||
hasCallback(name: keyof WebServerCallbacks): boolean {
|
||||
return this.callbacks[name] !== null;
|
||||
}
|
||||
}
|
||||
177
src/main/web-server/managers/LiveSessionManager.ts
Normal file
177
src/main/web-server/managers/LiveSessionManager.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* LiveSessionManager - Manages live session tracking for the web interface
|
||||
*
|
||||
* Handles:
|
||||
* - Tracking which sessions are marked as "live" (visible in web interface)
|
||||
* - AutoRun state management for batch processing
|
||||
* - Providing session info for connected web clients
|
||||
*/
|
||||
|
||||
import { logger } from '../../utils/logger';
|
||||
import type { LiveSessionInfo, AutoRunState } from '../types';
|
||||
|
||||
const LOG_CONTEXT = 'LiveSessionManager';
|
||||
|
||||
/**
|
||||
* Callback for broadcasting session live status changes
|
||||
*/
|
||||
export interface LiveSessionBroadcastCallbacks {
|
||||
broadcastSessionLive: (sessionId: string, agentSessionId?: string) => void;
|
||||
broadcastSessionOffline: (sessionId: string) => void;
|
||||
broadcastAutoRunState: (sessionId: string, state: AutoRunState | null) => void;
|
||||
}
|
||||
|
||||
export class LiveSessionManager {
|
||||
// Live sessions - only these appear in the web interface
|
||||
private liveSessions: Map<string, LiveSessionInfo> = new Map();
|
||||
|
||||
// AutoRun states per session - tracks which sessions have active batch processing
|
||||
private autoRunStates: Map<string, AutoRunState> = new Map();
|
||||
|
||||
// Broadcast callbacks (set by WebServer)
|
||||
private broadcastCallbacks: LiveSessionBroadcastCallbacks | null = null;
|
||||
|
||||
/**
|
||||
* Set the broadcast callbacks for notifying clients of changes
|
||||
*/
|
||||
setBroadcastCallbacks(callbacks: LiveSessionBroadcastCallbacks): void {
|
||||
this.broadcastCallbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as live (visible in web interface)
|
||||
*/
|
||||
setSessionLive(sessionId: string, agentSessionId?: string): void {
|
||||
this.liveSessions.set(sessionId, {
|
||||
sessionId,
|
||||
agentSessionId,
|
||||
enabledAt: Date.now(),
|
||||
});
|
||||
logger.info(
|
||||
`Session ${sessionId} marked as live (total: ${this.liveSessions.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.broadcastCallbacks?.broadcastSessionLive(sessionId, agentSessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as offline (no longer visible in web interface)
|
||||
*/
|
||||
setSessionOffline(sessionId: string): void {
|
||||
const wasLive = this.liveSessions.delete(sessionId);
|
||||
if (wasLive) {
|
||||
logger.info(
|
||||
`Session ${sessionId} marked as offline (remaining: ${this.liveSessions.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
|
||||
// Clean up any associated AutoRun state to prevent memory leaks
|
||||
if (this.autoRunStates.has(sessionId)) {
|
||||
this.autoRunStates.delete(sessionId);
|
||||
logger.debug(`Cleaned up AutoRun state for offline session ${sessionId}`, LOG_CONTEXT);
|
||||
}
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.broadcastCallbacks?.broadcastSessionOffline(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session is currently live
|
||||
*/
|
||||
isSessionLive(sessionId: string): boolean {
|
||||
return this.liveSessions.has(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get live session info for a specific session
|
||||
*/
|
||||
getLiveSessionInfo(sessionId: string): LiveSessionInfo | undefined {
|
||||
return this.liveSessions.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all live session IDs
|
||||
*/
|
||||
getLiveSessions(): LiveSessionInfo[] {
|
||||
return Array.from(this.liveSessions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all live session IDs as an iterable
|
||||
*/
|
||||
getLiveSessionIds(): IterableIterator<string> {
|
||||
return this.liveSessions.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of live sessions
|
||||
*/
|
||||
getLiveSessionCount(): number {
|
||||
return this.liveSessions.size;
|
||||
}
|
||||
|
||||
// ============ AutoRun State Management ============
|
||||
|
||||
/**
|
||||
* Update AutoRun state for a session
|
||||
* Also stores state locally so new clients can receive it on connect
|
||||
*/
|
||||
setAutoRunState(sessionId: string, state: AutoRunState | null): void {
|
||||
// Store state locally for new clients connecting later
|
||||
if (state && state.isRunning) {
|
||||
this.autoRunStates.set(sessionId, state);
|
||||
logger.info(
|
||||
`AutoRun state stored for session ${sessionId}: tasks=${state.completedTasks}/${state.totalTasks} (total stored: ${this.autoRunStates.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
} else {
|
||||
const wasStored = this.autoRunStates.has(sessionId);
|
||||
this.autoRunStates.delete(sessionId);
|
||||
if (wasStored) {
|
||||
logger.info(
|
||||
`AutoRun state removed for session ${sessionId} (total stored: ${this.autoRunStates.size})`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.broadcastCallbacks?.broadcastAutoRunState(sessionId, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AutoRun state for a specific session
|
||||
*/
|
||||
getAutoRunState(sessionId: string): AutoRunState | undefined {
|
||||
return this.autoRunStates.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all AutoRun states (for new client connections)
|
||||
*/
|
||||
getAutoRunStates(): Map<string, AutoRunState> {
|
||||
return this.autoRunStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all state (called during server shutdown)
|
||||
*/
|
||||
clearAll(): void {
|
||||
// Mark all live sessions as offline
|
||||
for (const sessionId of this.liveSessions.keys()) {
|
||||
this.setSessionOffline(sessionId);
|
||||
}
|
||||
|
||||
// Clear any remaining autoRunStates as a safety measure
|
||||
if (this.autoRunStates.size > 0) {
|
||||
logger.debug(
|
||||
`Clearing ${this.autoRunStates.size} remaining AutoRun states on cleanup`,
|
||||
LOG_CONTEXT
|
||||
);
|
||||
this.autoRunStates.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main/web-server/managers/index.ts
Normal file
11
src/main/web-server/managers/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Managers Module Index
|
||||
*
|
||||
* Exports manager classes for the WebServer.
|
||||
*/
|
||||
|
||||
export { LiveSessionManager } from './LiveSessionManager';
|
||||
export type { LiveSessionBroadcastCallbacks } from './LiveSessionManager';
|
||||
|
||||
export { CallbackRegistry } from './CallbackRegistry';
|
||||
export type { WebServerCallbacks } from './CallbackRegistry';
|
||||
Reference in New Issue
Block a user