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:
Raza Rauf
2026-01-29 02:01:34 +05:00
committed by Pedram Amini
parent 68945cb946
commit e9af499004
5 changed files with 503 additions and 303 deletions

View File

@@ -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();

View File

@@ -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';

View 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;
}
}

View 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();
}
}
}

View 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';