import { BrowserWindow } from 'electron'; import { windowManager } from '@electron/service/window-service'; import logManager from '@electron/service/logger'; import type { GatewayEvent, RuntimeRefreshTopic } from './types'; import * as chatHandlers from './handlers/chat'; import * as providerHandlers from './handlers/provider'; import * as skillHandlers from './handlers/skills'; type RuntimeChangeBroadcast = { topics: RuntimeRefreshTopic[]; reason?: string; warnings?: string[]; channelType?: string; accountId?: string; }; class GatewayManager { private initialized = false; private status: 'connected' | 'disconnected' | 'reconnecting' = 'disconnected'; private setStatus(status: 'connected' | 'disconnected' | 'reconnecting'): void { this.status = status; this.broadcast({ type: 'gateway:status', status }); } async init(): Promise { if (this.initialized) return; this.initialized = true; this.status = 'connected'; logManager.info('GatewayManager initialized'); this.broadcast({ type: 'gateway:status', status: 'connected' }); } async start(): Promise { await this.init(); } async stop(): Promise { this.initialized = false; this.setStatus('disconnected'); } async restart(options?: RuntimeChangeBroadcast): Promise { this.initialized = false; this.setStatus('reconnecting'); await this.init(); if (options) { this.notifyRuntimeChanged(options); } } getStatus(): { status: 'connected' | 'disconnected' | 'reconnecting'; initialized: boolean; mode: 'in-process'; } { return { status: this.status, initialized: this.initialized, mode: 'in-process', }; } async checkHealth(): Promise<{ ok: boolean; status: 'connected' | 'disconnected' | 'reconnecting'; initialized: boolean; mode: 'in-process'; }> { return { ok: this.initialized && this.status === 'connected', ...this.getStatus(), }; } async rpc(method: string, params: any): Promise { if (!this.initialized) { await this.init(); } logManager.info(`Gateway RPC: ${method}`, params); switch (method) { case 'chat.send': return chatHandlers.handleChatSend(params, (event) => this.broadcast(event)); case 'chat.history': return chatHandlers.handleChatHistory(params); case 'chat.abort': return chatHandlers.handleChatAbort(params, (event) => this.broadcast(event)); case 'session.list': return chatHandlers.handleSessionList(); case 'session.delete': return chatHandlers.handleSessionDelete(params); case 'provider.list': return providerHandlers.handleProviderList(); case 'provider.getDefault': return providerHandlers.handleProviderGetDefault(); case 'skills.status': return skillHandlers.handleSkillsStatus(); case 'skills.update': return skillHandlers.handleSkillsUpdate(params); default: throw new Error(`Unknown gateway RPC method: ${method}`); } } broadcast(event: GatewayEvent): void { const mainWindow = BrowserWindow.getAllWindows().find( (win) => windowManager.getName(win) === 'main' ); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:event', event); } } notifyRuntimeChanged(options: RuntimeChangeBroadcast): void { const topics = Array.from(new Set(options.topics.filter(Boolean))); if (topics.length === 0) { return; } this.broadcast({ type: 'runtime:changed', topics, reason: options.reason, warnings: options.warnings && options.warnings.length > 0 ? options.warnings : undefined, channelType: options.channelType, accountId: options.accountId, syncedAt: new Date().toISOString(), }); } reloadProviders(options?: RuntimeChangeBroadcast): void { logManager.info('GatewayManager reloading providers'); // For now, providers are resolved on each chat.send call, // so no in-memory cache to invalidate. Future: notify active sessions. this.notifyRuntimeChanged({ topics: options?.topics ?? ['providers', 'models'], reason: options?.reason ?? 'providers:reload', warnings: options?.warnings, channelType: options?.channelType, accountId: options?.accountId, }); } } export const gatewayManager = new GatewayManager();