--- phase: 02-service-infrastructure plan: 02 type: execute wave: 2 depends_on: ["02-01"] files_modified: - src/main/index.ts - src/main/ipc/handlers.ts - src/main/ipc/sessions.ts - src/main/ipc/projects.ts - src/main/ipc/search.ts - src/main/ipc/subagents.ts - src/main/ipc/ssh.ts autonomous: true must_haves: truths: - "main/index.ts creates a ServiceContextRegistry instead of individual global service variables" - "Local context is created and registered at startup, behaving identically to current behavior" - "IPC handlers route through the registry to get service instances, not module-level variables" - "SSH connect creates a new ServiceContext in the registry instead of destroying/recreating globals" - "SSH disconnect destroys the SSH context but leaves local context untouched" - "reinitializeServiceHandlers is no longer needed — handlers always fetch from registry" artifacts: - path: "src/main/index.ts" provides: "Registry-based service initialization" contains: "ServiceContextRegistry" - path: "src/main/ipc/handlers.ts" provides: "Registry-aware IPC initialization" contains: "ServiceContextRegistry" - path: "src/main/ipc/sessions.ts" provides: "Registry-routed session handlers" contains: "registry" - path: "src/main/ipc/projects.ts" provides: "Registry-routed project handlers" contains: "registry" - path: "src/main/ipc/search.ts" provides: "Registry-routed search handlers" contains: "registry" - path: "src/main/ipc/subagents.ts" provides: "Registry-routed subagent handlers" contains: "registry" key_links: - from: "src/main/index.ts" to: "src/main/services/infrastructure/ServiceContextRegistry.ts" via: "creates and uses registry" pattern: "new ServiceContextRegistry" - from: "src/main/ipc/handlers.ts" to: "src/main/services/infrastructure/ServiceContextRegistry.ts" via: "receives registry for handler initialization" pattern: "ServiceContextRegistry" - from: "src/main/ipc/sessions.ts" to: "src/main/services/infrastructure/ServiceContextRegistry.ts" via: "calls registry.getActive() for service resolution" pattern: "registry\\.getActive\\(\\)" - from: "src/main/ipc/ssh.ts" to: "src/main/services/infrastructure/ServiceContextRegistry.ts" via: "creates SSH context and switches via registry" pattern: "registry\\.(createSshContext|switch|destroy)" --- Replace the global service variables in main/index.ts with ServiceContextRegistry, and update all IPC handler modules to route through the registry instead of holding module-level service references. Purpose: This is the core integration that transforms the app from single-mode (global services) to multi-context (registry-managed services). After this plan, SSH connections will create new contexts instead of destroying/recreating globals, and local context remains alive throughout. Output: Refactored main/index.ts using registry pattern, all IPC handlers routing via registry.getActive(). @.planning/phases/02-service-infrastructure/02-RESEARCH.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-service-infrastructure/02-01-PLAN.md @src/main/index.ts @src/main/ipc/handlers.ts @src/main/ipc/sessions.ts @src/main/ipc/projects.ts @src/main/ipc/search.ts @src/main/ipc/subagents.ts @src/main/ipc/ssh.ts @src/main/services/infrastructure/ServiceContext.ts @src/main/services/infrastructure/ServiceContextRegistry.ts Task 1: Refactor main/index.ts to use ServiceContextRegistry src/main/index.ts src/main/ipc/handlers.ts **src/main/index.ts** - Replace global service variables with registry: 1. Remove these individual service variable declarations: - `let projectScanner: ProjectScanner;` - `let sessionParser: SessionParser;` - `let subagentResolver: SubagentResolver;` - `let chunkBuilder: ChunkBuilder;` - `let dataCache: DataCache;` - `let fileWatcher: FileWatcher;` - `let cleanupInterval: NodeJS.Timeout | null = null;` 2. Add: `let contextRegistry: ServiceContextRegistry;` 3. Rewrite `initializeServices()`: - Create `sshConnectionManager` (unchanged) - Create `contextRegistry = new ServiceContextRegistry()` - Create local ServiceContext: ``` const localContext = new ServiceContext({ id: 'local', type: 'local', fsProvider: new LocalFileSystemProvider(), }); ``` - Register: `contextRegistry.registerContext(localContext)` - Start: `localContext.start()` - Initialize notification manager (unchanged — singleton, stays global, not context-scoped) - Set notification manager on local context's fileWatcher: `localContext.fileWatcher.setNotificationManager(notificationManager)` - Forward file-change and todo-change events from local context's fileWatcher to renderer (same as current, but using `localContext.fileWatcher.on(...)`) - Initialize IPC handlers — change signature to pass `contextRegistry` instead of individual services. Also pass `sshConnectionManager`. - Forward SSH state changes (unchanged) 4. Remove the `handleModeSwitch` callback entirely. SSH connect/disconnect will be handled by the SSH IPC handler using the registry directly (Plan 02 Task 2 below). 5. Rewrite `shutdownServices()`: - Call `contextRegistry.dispose()` (disposes all contexts including local) - Dispose `sshConnectionManager` - Call `removeIpcHandlers()` 6. **File watcher event forwarding concern:** Currently events are wired to the single global fileWatcher. With multi-context, we need to forward events from the ACTIVE context's fileWatcher. Two approaches: - Simple (do this): Wire events on local context at startup. When context switches happen (in SSH IPC handler), re-wire events to new active context's fileWatcher. - Complex (don't do this): Create a proxy event system. For now, wire local context's fileWatcher events at startup. The SSH IPC handler (Task 2) will handle re-wiring on switch. Store the event cleanup functions so they can be unwired: ``` let fileChangeCleanup: (() => void) | null = null; let todoChangeCleanup: (() => void) | null = null; ``` Create a `wireFileWatcherEvents(context: ServiceContext)` helper that: - Cleans up previous listeners (if any) - Adds new listeners to context.fileWatcher for 'file-change' and 'todo-change' - Stores cleanup functions Export this function (or make it accessible to IPC handlers via a setter). Actually, simpler approach: have the registry emit a 'context-switched' event, and index.ts listens to it to re-wire. But the registry doesn't extend EventEmitter. Even simpler: have `switch()` in the registry return the previous and current contexts, and let the caller (SSH IPC handler) call a function on index.ts to re-wire. Simplest approach: Create and export a `rewireFileWatcherEvents(context: ServiceContext, mainWindow: BrowserWindow | null)` function from index.ts that the SSH handler can import and call after switching contexts. This function removes old listeners, adds new ones. **src/main/ipc/handlers.ts** - Update initialization signature: 1. Change `initializeIpcHandlers` signature to accept `ServiceContextRegistry` instead of individual services: ```typescript export function initializeIpcHandlers( registry: ServiceContextRegistry, updater: UpdaterService, sshManager: SshConnectionManager, ): void ``` 2. Inside, pass registry to each domain handler's initialize function: - `initializeProjectHandlers(registry)` - `initializeSessionHandlers(registry)` - `initializeSearchHandlers(registry)` - `initializeSubagentHandlers(registry)` - `initializeSshHandlers(sshManager, registry)` — SSH handler now gets registry instead of mode switch callback - `initializeUpdaterHandlers(updater)` — unchanged (not context-dependent) 3. Remove `reinitializeServiceHandlers()` entirely — no longer needed because handlers always call `registry.getActive()` to get current services. 4. Keep `removeIpcHandlers()` unchanged. Run `pnpm typecheck` — must pass with zero errors. The main/index.ts should no longer have individual service variables (projectScanner, sessionParser, etc.) and should use contextRegistry instead. main/index.ts creates ServiceContextRegistry at startup with local context registered. handlers.ts accepts ServiceContextRegistry. reinitializeServiceHandlers is removed. Type checking passes. Task 2: Update domain IPC handlers to route via registry src/main/ipc/sessions.ts src/main/ipc/projects.ts src/main/ipc/search.ts src/main/ipc/subagents.ts src/main/ipc/ssh.ts Update each domain IPC handler to receive `ServiceContextRegistry` instead of individual service instances, and call `registry.getActive()` to resolve services at invocation time. **src/main/ipc/projects.ts:** 1. Change `initializeProjectHandlers(scanner: ProjectScanner)` to `initializeProjectHandlers(registry: ServiceContextRegistry)` 2. Store registry as module-level variable: `let registry: ServiceContextRegistry;` 3. Remove module-level `scanner` variable 4. In each handler, resolve scanner from registry: `const { projectScanner } = registry.getActive();` 5. Update all references from `scanner.` to `projectScanner.` **src/main/ipc/sessions.ts:** 1. Change `initializeSessionHandlers(scanner, parser, resolver, builder, cache)` to `initializeSessionHandlers(registry: ServiceContextRegistry)` 2. Store registry as module-level variable 3. Remove module-level scanner, parser, resolver, builder, cache variables 4. In each handler, destructure from registry: `const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } = registry.getActive();` 5. Update all references to use destructured names **src/main/ipc/search.ts:** 1. Change `initializeSearchHandlers(scanner: ProjectScanner)` to `initializeSearchHandlers(registry: ServiceContextRegistry)` 2. Store registry, resolve from `registry.getActive()` in handlers 3. Update references **src/main/ipc/subagents.ts:** 1. Change `initializeSubagentHandlers(builder, cache, parser, resolver, scanner)` to `initializeSubagentHandlers(registry: ServiceContextRegistry)` 2. Store registry, resolve from `registry.getActive()` in handlers 3. In the handler, get `fsProvider` and `projectsDir` from `registry.getActive().projectScanner` as was done in Phase 1 **src/main/ipc/ssh.ts:** 1. Change `initializeSshHandlers(manager, modeSwitchCallback)` to `initializeSshHandlers(manager: SshConnectionManager, registry: ServiceContextRegistry)` 2. Store registry as module-level variable, remove `onModeSwitch` callback 3. Rewrite `SSH_CONNECT` handler: - Call `connectionManager.connect(config)` (unchanged) - Get provider and remoteProjectsPath from connectionManager - Generate contextId: `ssh-${config.host}` (or `ssh-${config.host}-${config.port}` if port is non-standard) - Check if context already exists in registry (reconnection case): if yes, destroy old and create new - Create new ServiceContext: `new ServiceContext({ id: contextId, type: 'ssh', fsProvider: provider, projectsDir: remoteProjectsPath })` - Register in registry: `registry.registerContext(context)` - Start context: `context.start()` - Switch registry to new context: `registry.switch(contextId)` - Import and call `rewireFileWatcherEvents(context, mainWindow)` from index.ts (or accept a callback for this) - Return success with status 4. Rewrite `SSH_DISCONNECT` handler: - Get current contextId from registry if it's SSH type - Call `connectionManager.disconnect()` - Call `registry.switch('local')` to switch back - Call `registry.destroy(contextId)` to clean up SSH context - Re-wire file watcher events to local context - Return success with status 5. All other SSH handlers (test, getConfigHosts, resolveHost, saveLastConnection, getLastConnection) remain unchanged — they operate on SshConnectionManager directly, not service contexts. **Important:** For the file watcher re-wiring, two approaches: - Option A: Export `rewireFileWatcherEvents` from index.ts (simple but circular dependency risk) - Option B: Pass a callback to `initializeSshHandlers` that the SSH handler calls after switching - Option C: Have `initializeSshHandlers` also accept `mainWindow` reference and handle rewiring internally Choose Option B: Add a `onContextSwitched: (context: ServiceContext) => void` callback parameter to `initializeSshHandlers`. index.ts passes a callback that re-wires FileWatcher events and sends context-changed notification to renderer. Updated signature: `initializeSshHandlers(manager: SshConnectionManager, registry: ServiceContextRegistry, onContextSwitched: (context: ServiceContext) => void)` In index.ts, the callback: ```typescript const onContextSwitched = (context: ServiceContext) => { // Re-wire file watcher events rewireFileWatcherEvents(context); // Notify renderer if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus()); mainWindow.webContents.send('context-changed', { contextId: context.id, type: context.type, }); } }; ``` The `rewireFileWatcherEvents` function in index.ts removes old listeners and adds new ones from context.fileWatcher, forwarding to mainWindow. 1. Run `pnpm typecheck` — must pass with zero errors 2. Run `pnpm test` — all existing tests must pass (IPC handler tests may need mock updates) 3. Verify no remaining module-level service variables in sessions.ts, projects.ts, search.ts, subagents.ts (should all use `registry`) 4. Verify ssh.ts creates ServiceContext on connect and destroys on disconnect All domain IPC handlers (sessions, projects, search, subagents) resolve services via registry.getActive() at invocation time. SSH handler creates/destroys ServiceContext instances and switches registry on connect/disconnect. No module-level service variables remain — all routing goes through the registry. reinitializeServiceHandlers is fully removed. All tests pass. 1. `pnpm typecheck` passes with zero errors 2. `pnpm test` — all existing tests pass 3. main/index.ts has `contextRegistry` instead of individual service globals 4. `reinitializeServiceHandlers` no longer exists in handlers.ts 5. All IPC handlers use `registry.getActive()` pattern 6. SSH connect creates a new ServiceContext and registers it 7. SSH disconnect destroys SSH context and switches to local 8. File watcher events are re-wired on context switch - Global service variables replaced with ServiceContextRegistry in main/index.ts - All IPC domain handlers route through registry, not module-level service refs - SSH connect creates isolated context; SSH disconnect destroys it without affecting local - File watcher events forward from active context to renderer - reinitializeServiceHandlers removed (no longer needed) - No regressions in existing tests After completion, create `.planning/phases/02-service-infrastructure/02-02-SUMMARY.md`