From 5bf41c6ed85e8bcfff2d7b649a9f185cd19d46a3 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 12 Feb 2026 01:04:20 +0000 Subject: [PATCH] refactor(02-02): main/index.ts to use ServiceContextRegistry - Replace global service variables with ServiceContextRegistry - Create local context at startup with LocalFileSystemProvider - Register local context in registry and start it - Wire file watcher events via wireFileWatcherEvents helper - Export onContextSwitched callback for SSH handler - Remove handleModeSwitch callback (replaced by registry pattern) - Update initializeIpcHandlers to accept registry instead of individual services - Remove reinitializeServiceHandlers entirely from handlers.ts - ServiceContextRegistry pattern eliminates need for service recreation on mode switch --- src/main/index.ts | 226 ++++++++++++++++++--------------------- src/main/ipc/handlers.ts | 58 ++-------- 2 files changed, 117 insertions(+), 167 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index da97bd7b..6bd43bfd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,30 +4,23 @@ * Responsibilities: * - Initialize Electron app and main window * - Set up IPC handlers for data access - * - Initialize services (ProjectScanner, SessionParser, etc.) + * - Initialize ServiceContextRegistry with local context * - Start file watcher for live updates * - Manage application lifecycle */ import { - CACHE_CLEANUP_INTERVAL_MINUTES, - CACHE_TTL_MINUTES, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, DEV_SERVER_PORT, getTrafficLightPositionForZoom, - MAX_CACHE_SESSIONS, WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL, } from '@shared/constants'; import { createLogger } from '@shared/utils/logger'; import { app, BrowserWindow } from 'electron'; import { join } from 'path'; -import { - initializeIpcHandlers, - reinitializeServiceHandlers, - removeIpcHandlers, -} from './ipc/handlers'; +import { initializeIpcHandlers, removeIpcHandlers } from './ipc/handlers'; // Icon path - works for both dev and production const getIconPath = (): string => { @@ -42,15 +35,12 @@ const logger = createLogger('App'); import { SSH_STATUS } from '@preload/constants/ipcChannels'; import { - ChunkBuilder, configManager, - DataCache, - FileWatcher, + LocalFileSystemProvider, NotificationManager, - ProjectScanner, - SessionParser, + ServiceContext, + ServiceContextRegistry, SshConnectionManager, - SubagentResolver, UpdaterService, } from './services'; @@ -60,17 +50,71 @@ import { let mainWindow: BrowserWindow | null = null; -// Service instances -let projectScanner: ProjectScanner; -let sessionParser: SessionParser; -let subagentResolver: SubagentResolver; -let chunkBuilder: ChunkBuilder; -let dataCache: DataCache; -let fileWatcher: FileWatcher; +// Service registry and global services +let contextRegistry: ServiceContextRegistry; let notificationManager: NotificationManager; let updaterService: UpdaterService; let sshConnectionManager: SshConnectionManager; -let cleanupInterval: NodeJS.Timeout | null = null; + +// File watcher event cleanup functions +let fileChangeCleanup: (() => void) | null = null; +let todoChangeCleanup: (() => void) | null = null; + +/** + * Wires file watcher events from a ServiceContext to the renderer. + * Cleans up previous listeners before adding new ones. + */ +function wireFileWatcherEvents(context: ServiceContext): void { + logger.info(`Wiring FileWatcher events for context: ${context.id}`); + + // Clean up previous listeners + if (fileChangeCleanup) { + fileChangeCleanup(); + fileChangeCleanup = null; + } + if (todoChangeCleanup) { + todoChangeCleanup(); + todoChangeCleanup = null; + } + + // Wire file-change events + const fileChangeHandler = (event: unknown) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('file-change', event); + } + }; + context.fileWatcher.on('file-change', fileChangeHandler); + fileChangeCleanup = () => context.fileWatcher.off('file-change', fileChangeHandler); + + // Wire todo-change events + const todoChangeHandler = (event: unknown) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('todo-change', event); + } + }; + context.fileWatcher.on('todo-change', todoChangeHandler); + todoChangeCleanup = () => context.fileWatcher.off('todo-change', todoChangeHandler); + + logger.info(`FileWatcher events wired for context: ${context.id}`); +} + +/** + * Callback invoked when context switches (called by SSH IPC handler). + * Re-wires file watcher events and notifies renderer. + */ +export function onContextSwitched(context: ServiceContext): void { + // Re-wire file watcher events to new context + wireFileWatcherEvents(context); + + // Notify renderer of context change + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus()); + mainWindow.webContents.send('context-changed', { + contextId: context.id, + type: context.type, + }); + } +} /** * Initializes all services. @@ -81,71 +125,36 @@ function initializeServices(): void { // Initialize SSH connection manager sshConnectionManager = new SshConnectionManager(); - // Initialize services (paths are set automatically from environment) - projectScanner = new ProjectScanner(); - sessionParser = new SessionParser(projectScanner); - subagentResolver = new SubagentResolver(projectScanner); - chunkBuilder = new ChunkBuilder(); - const disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1'; - dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache); + // Create ServiceContextRegistry + contextRegistry = new ServiceContextRegistry(); + + // Create local context + const localContext = new ServiceContext({ + id: 'local', + type: 'local', + fsProvider: new LocalFileSystemProvider(), + }); + + // Register and start local context + contextRegistry.registerContext(localContext); + localContext.start(); + + logger.info(`Projects directory: ${localContext.projectScanner.getProjectsDir()}`); + + // Initialize notification manager (singleton, not context-scoped) + notificationManager = NotificationManager.getInstance(); + + // Set notification manager on local context's file watcher + localContext.fileWatcher.setNotificationManager(notificationManager); + + // Wire file watcher events for local context + wireFileWatcherEvents(localContext); + + // Initialize updater service updaterService = new UpdaterService(); - logger.info(`Projects directory: ${projectScanner.getProjectsDir()}`); - - // Mode switch callback: recreates services with new provider when switching local↔SSH - const handleModeSwitch = async (mode: 'local' | 'ssh'): Promise => { - logger.info(`Switching to ${mode} mode`); - - // Stop file watcher - fileWatcher.stop(); - - // Clear data cache - dataCache.clear(); - - // Get provider and projects path from connection manager - const provider = sshConnectionManager.getProvider(); - const projectsDir = - mode === 'ssh' ? (sshConnectionManager.getRemoteProjectsPath() ?? undefined) : undefined; - - // Recreate services with new provider - projectScanner = new ProjectScanner(projectsDir, undefined, provider); - sessionParser = new SessionParser(projectScanner); - subagentResolver = new SubagentResolver(projectScanner); - - // Re-initialize IPC handler service references so subsequent calls use new instances - reinitializeServiceHandlers( - projectScanner, - sessionParser, - subagentResolver, - chunkBuilder, - dataCache - ); - - // Update file watcher provider - fileWatcher.setFileSystemProvider(provider); - - // Restart file watcher - fileWatcher.start(); - - // Notify renderer to re-fetch all data - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus()); - } - - logger.info(`Mode switch to ${mode} complete`); - }; - - // Initialize IPC handlers (including SSH) - initializeIpcHandlers( - projectScanner, - sessionParser, - subagentResolver, - chunkBuilder, - dataCache, - updaterService, - sshConnectionManager, - handleModeSwitch - ); + // Initialize IPC handlers with registry + initializeIpcHandlers(contextRegistry, updaterService, sshConnectionManager); // Forward SSH state changes to renderer sshConnectionManager.on('state-change', (status: unknown) => { @@ -154,33 +163,6 @@ function initializeServices(): void { } }); - // Initialize notification manager using singleton pattern - // This ensures IPC handlers and FileWatcher use the same instance - // Note: mainWindow will be set later via setMainWindow() when window is created - notificationManager = NotificationManager.getInstance(); - - // Start file watcher with notification manager for error detection - fileWatcher = new FileWatcher(dataCache); - fileWatcher.setNotificationManager(notificationManager); - fileWatcher.start(); - - // Forward file change events to renderer - // Note: Error detection is handled internally by FileWatcher via NotificationManager - fileWatcher.on('file-change', (event) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('file-change', event); - } - }); - - fileWatcher.on('todo-change', (event) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('todo-change', event); - } - }); - - // Start automatic cache cleanup - cleanupInterval = dataCache.startAutoCleanup(CACHE_CLEANUP_INTERVAL_MINUTES); - logger.info('Services initialized successfully'); } @@ -190,15 +172,19 @@ function initializeServices(): void { function shutdownServices(): void { logger.info('Shutting down services...'); - // Stop file watcher - if (fileWatcher) { - fileWatcher.stop(); + // Clean up file watcher event listeners + if (fileChangeCleanup) { + fileChangeCleanup(); + fileChangeCleanup = null; + } + if (todoChangeCleanup) { + todoChangeCleanup(); + todoChangeCleanup = null; } - // Stop cache cleanup - if (cleanupInterval) { - clearInterval(cleanupInterval); - cleanupInterval = null; + // Dispose all contexts (including local) + if (contextRegistry) { + contextRegistry.dispose(); } // Dispose SSH connection manager diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index fd8b8033..169917b7 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -45,38 +45,23 @@ import { import { registerUtilityHandlers, removeUtilityHandlers } from './utility'; import { registerValidationHandlers, removeValidationHandlers } from './validation'; -import type { - ChunkBuilder, - DataCache, - ProjectScanner, - SessionParser, - SshConnectionManager, - SubagentResolver, - UpdaterService, -} from '../services'; +import type { ServiceContextRegistry, SshConnectionManager, UpdaterService } from '../services'; /** - * Initializes IPC handlers with service instances. + * Initializes IPC handlers with service registry. */ export function initializeIpcHandlers( - scanner: ProjectScanner, - parser: SessionParser, - resolver: SubagentResolver, - builder: ChunkBuilder, - cache: DataCache, + registry: ServiceContextRegistry, updater: UpdaterService, - sshManager?: SshConnectionManager, - sshModeSwitchCallback?: (mode: 'local' | 'ssh') => Promise + sshManager: SshConnectionManager ): void { - // Initialize domain handlers with their required services - initializeProjectHandlers(scanner); - initializeSessionHandlers(scanner, parser, resolver, builder, cache); - initializeSearchHandlers(scanner); - initializeSubagentHandlers(builder, cache, parser, resolver, scanner); + // Initialize domain handlers with registry + initializeProjectHandlers(registry); + initializeSessionHandlers(registry); + initializeSearchHandlers(registry); + initializeSubagentHandlers(registry); initializeUpdaterHandlers(updater); - if (sshManager && sshModeSwitchCallback) { - initializeSshHandlers(sshManager, sshModeSwitchCallback); - } + initializeSshHandlers(sshManager, registry); // Register all handlers registerProjectHandlers(ipcMain); @@ -88,32 +73,11 @@ export function initializeIpcHandlers( registerNotificationHandlers(ipcMain); registerConfigHandlers(ipcMain); registerUpdaterHandlers(ipcMain); - if (sshManager) { - registerSshHandlers(ipcMain); - } + registerSshHandlers(ipcMain); logger.info('All handlers registered'); } -/** - * Re-initializes service-dependent IPC handlers after a mode switch (local ↔ SSH). - * This updates the module-level service references held by each domain handler module, - * ensuring IPC calls after the switch use the new service instances. - */ -export function reinitializeServiceHandlers( - scanner: ProjectScanner, - parser: SessionParser, - resolver: SubagentResolver, - builder: ChunkBuilder, - cache: DataCache -): void { - initializeProjectHandlers(scanner); - initializeSessionHandlers(scanner, parser, resolver, builder, cache); - initializeSearchHandlers(scanner); - initializeSubagentHandlers(builder, cache, parser, resolver, scanner); - logger.info('Service handlers re-initialized after mode switch'); -} - /** * Removes all IPC handlers. * Should be called when shutting down.