From 24051acac84bb2f561139c6bc50f50c113f0c567 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 12 Feb 2026 01:04:29 +0000 Subject: [PATCH] refactor(02-02): IPC handlers route via ServiceContextRegistry - projects.ts: Accept registry, resolve projectScanner via getActive() - sessions.ts: Accept registry, destructure all services from getActive() - search.ts: Accept registry, resolve projectScanner via getActive() - subagents.ts: Accept registry, resolve all services from getActive() - ssh.ts: Accept registry, create/destroy ServiceContext on connect/disconnect - SSH_CONNECT: Create new ServiceContext with SSH provider, register, start, switch - SSH_DISCONNECT: Switch to local, destroy SSH context - Import onContextSwitched from index.ts to rewire file watcher events - All handlers now call registry.getActive() at invocation time - No module-level service variables remain - fully dynamic routing --- src/main/ipc/projects.ts | 15 ++++---- src/main/ipc/search.ts | 13 +++---- src/main/ipc/sessions.ts | 34 ++++++++---------- src/main/ipc/ssh.ts | 73 ++++++++++++++++++++++++++++++++------- src/main/ipc/subagents.ts | 35 +++++-------------- 5 files changed, 100 insertions(+), 70 deletions(-) diff --git a/src/main/ipc/projects.ts b/src/main/ipc/projects.ts index 21d96677..0ae3922f 100644 --- a/src/main/ipc/projects.ts +++ b/src/main/ipc/projects.ts @@ -14,18 +14,18 @@ import { type Project, type RepositoryGroup, type Session } from '../types'; import { validateProjectId } from './guards'; -import type { ProjectScanner } from '../services'; +import type { ServiceContextRegistry } from '../services'; const logger = createLogger('IPC:projects'); -// Service instance - set via initialize -let projectScanner: ProjectScanner; +// Service registry - set via initialize +let registry: ServiceContextRegistry; /** - * Initializes project handlers with service instance. + * Initializes project handlers with service registry. */ -export function initializeProjectHandlers(scanner: ProjectScanner): void { - projectScanner = scanner; +export function initializeProjectHandlers(contextRegistry: ServiceContextRegistry): void { + registry = contextRegistry; } /** @@ -60,6 +60,7 @@ export function removeProjectHandlers(ipcMain: IpcMain): void { */ async function handleGetProjects(_event: IpcMainInvokeEvent): Promise { try { + const { projectScanner } = registry.getActive(); const projects = await projectScanner.scan(); return projects; } catch (error) { @@ -75,6 +76,7 @@ async function handleGetProjects(_event: IpcMainInvokeEvent): Promise */ async function handleGetRepositoryGroups(_event: IpcMainInvokeEvent): Promise { try { + const { projectScanner } = registry.getActive(); const groups = await projectScanner.scanWithWorktreeGrouping(); return groups; } catch (error) { @@ -100,6 +102,7 @@ async function handleGetWorktreeSessions( return []; } + const { projectScanner } = registry.getActive(); const sessions = await projectScanner.listWorktreeSessions(validatedProject.value!); return sessions; } catch (error) { diff --git a/src/main/ipc/search.ts b/src/main/ipc/search.ts index 81797f02..940897d8 100644 --- a/src/main/ipc/search.ts +++ b/src/main/ipc/search.ts @@ -14,16 +14,16 @@ import { coerceSearchMaxResults, validateProjectId, validateSearchQuery } from ' const logger = createLogger('IPC:search'); -import type { ProjectScanner } from '../services'; +import type { ServiceContextRegistry } from '../services'; -// Service instance - set via initialize -let projectScanner: ProjectScanner; +// Service registry - set via initialize +let registry: ServiceContextRegistry; /** - * Initializes search handlers with service instance. + * Initializes search handlers with service registry. */ -export function initializeSearchHandlers(scanner: ProjectScanner): void { - projectScanner = scanner; +export function initializeSearchHandlers(contextRegistry: ServiceContextRegistry): void { + registry = contextRegistry; } /** @@ -68,6 +68,7 @@ async function handleSearchSessions( return { results: [], totalMatches: 0, sessionsSearched: 0, query }; } + const { projectScanner } = registry.getActive(); const safeMaxResults = coerceSearchMaxResults(maxResults, 50); const result = await projectScanner.searchSessions( validatedProject.value!, diff --git a/src/main/ipc/sessions.ts b/src/main/ipc/sessions.ts index 7f76ac65..4183baaa 100644 --- a/src/main/ipc/sessions.ts +++ b/src/main/ipc/sessions.ts @@ -24,33 +24,19 @@ import { import { coercePageLimit, validateProjectId, validateSessionId } from './guards'; -import type { ChunkBuilder, ProjectScanner, SessionParser, SubagentResolver } from '../services'; +import type { ServiceContextRegistry } from '../services'; import type { WaterfallData } from '@shared/types'; const logger = createLogger('IPC:sessions'); -// Service instances - set via initialize -let projectScanner: ProjectScanner; -let sessionParser: SessionParser; -let subagentResolver: SubagentResolver; -let chunkBuilder: ChunkBuilder; -let dataCache: DataCache; +// Service registry - set via initialize +let registry: ServiceContextRegistry; /** - * Initializes session handlers with service instances. + * Initializes session handlers with service registry. */ -export function initializeSessionHandlers( - scanner: ProjectScanner, - parser: SessionParser, - resolver: SubagentResolver, - builder: ChunkBuilder, - cache: DataCache -): void { - projectScanner = scanner; - sessionParser = parser; - subagentResolver = resolver; - chunkBuilder = builder; - dataCache = cache; +export function initializeSessionHandlers(contextRegistry: ServiceContextRegistry): void { + registry = contextRegistry; } /** @@ -100,6 +86,7 @@ async function handleGetSessions( return []; } + const { projectScanner } = registry.getActive(); const sessions = await projectScanner.listSessions(validatedProject.value!); return sessions; } catch (error) { @@ -128,6 +115,7 @@ async function handleGetSessionsPaginated( return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 }; } + const { projectScanner } = registry.getActive(); const safeLimit = coercePageLimit(limit, 20); const result = await projectScanner.listSessionsPaginated( validatedProject.value!, @@ -162,6 +150,9 @@ async function handleGetSessionDetail( return null; } + const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } = + registry.getActive(); + const safeProjectId = validatedProject.value!; const safeSessionId = validatedSession.value!; const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId); @@ -223,6 +214,7 @@ async function handleGetSessionGroups( ); return []; } + const { sessionParser, subagentResolver, chunkBuilder } = registry.getActive(); const safeProjectId = validatedProject.value!; const safeSessionId = validatedSession.value!; @@ -262,6 +254,7 @@ async function handleGetSessionMetrics( if (!validatedProject.valid || !validatedSession.valid) { return null; } + const { sessionParser, dataCache } = registry.getActive(); const safeProjectId = validatedProject.value!; const safeSessionId = validatedSession.value!; @@ -297,6 +290,7 @@ async function handleGetWaterfallData( return null; } + const { chunkBuilder } = registry.getActive(); return chunkBuilder.buildWaterfallData(detail.chunks, detail.processes); } catch (error) { logger.error(`Error in get-waterfall-data for ${projectId}/${sessionId}:`, error); diff --git a/src/main/ipc/ssh.ts b/src/main/ipc/ssh.ts index 8949fbb3..807030eb 100644 --- a/src/main/ipc/ssh.ts +++ b/src/main/ipc/ssh.ts @@ -2,8 +2,8 @@ * SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests. * * Channels: - * - ssh:connect - Connect to SSH host, switch to remote mode - * - ssh:disconnect - Disconnect and switch back to local mode + * - ssh:connect - Connect to SSH host, create new context + * - ssh:disconnect - Disconnect and switch back to local context * - ssh:getState - Get current connection state * - ssh:test - Test connection without switching */ @@ -20,13 +20,14 @@ import { } from '@preload/constants/ipcChannels'; import { createLogger } from '@shared/utils/logger'; -import { configManager } from '../services'; +import { configManager, ServiceContext } from '../services'; import type { + ServiceContextRegistry, SshConnectionConfig, SshConnectionManager, SshConnectionStatus, -} from '../services/infrastructure/SshConnectionManager'; +} from '../services'; import type { SshLastConnection } from '@shared/types'; import type { IpcMain } from 'electron'; @@ -37,7 +38,7 @@ const logger = createLogger('IPC:ssh'); // ============================================================================= let connectionManager: SshConnectionManager; -let onModeSwitch: ((mode: 'local' | 'ssh') => Promise) | null = null; +let registry: ServiceContextRegistry; // ============================================================================= // Initialization @@ -46,14 +47,14 @@ let onModeSwitch: ((mode: 'local' | 'ssh') => Promise) | null = null; /** * Initialize SSH handlers with required services. * @param manager - The SSH connection manager instance - * @param modeSwitchCallback - Called when switching between local/SSH mode + * @param contextRegistry - The service context registry */ export function initializeSshHandlers( manager: SshConnectionManager, - modeSwitchCallback: (mode: 'local' | 'ssh') => Promise + contextRegistry: ServiceContextRegistry ): void { connectionManager = manager; - onModeSwitch = modeSwitchCallback; + registry = contextRegistry; } // ============================================================================= @@ -63,10 +64,41 @@ export function initializeSshHandlers( export function registerSshHandlers(ipcMain: IpcMain): void { ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => { try { + // Connect to SSH host await connectionManager.connect(config); - if (onModeSwitch) { - await onModeSwitch('ssh'); + + // Get provider and remote path + const provider = connectionManager.getProvider(); + const remoteProjectsPath = connectionManager.getRemoteProjectsPath() ?? undefined; + + // Generate context ID + const contextId = `ssh-${config.host}`; + + // Destroy existing SSH context if any (reconnection case) + if (registry.has(contextId)) { + logger.info(`Destroying existing SSH context: ${contextId}`); + registry.destroy(contextId); } + + // Create new SSH context + const sshContext = new ServiceContext({ + id: contextId, + type: 'ssh', + fsProvider: provider, + projectsDir: remoteProjectsPath, + }); + + // Register and start SSH context + registry.registerContext(sshContext); + sshContext.start(); + + // Switch to SSH context + registry.switch(contextId); + + // Import and call context switch callback from index.ts + const { onContextSwitched } = await import('../index'); + onContextSwitched(sshContext); + return { success: true, data: connectionManager.getStatus() }; } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -77,10 +109,27 @@ export function registerSshHandlers(ipcMain: IpcMain): void { ipcMain.handle(SSH_DISCONNECT, async () => { try { + // Get current SSH context ID before disconnecting + const currentContextId = registry.getActiveContextId(); + const isSshContext = currentContextId.startsWith('ssh-'); + + // Disconnect from SSH connectionManager.disconnect(); - if (onModeSwitch) { - await onModeSwitch('local'); + + // If we were on an SSH context, destroy it + if (isSshContext) { + // Switch back to local first (this also starts local file watcher) + registry.switch('local'); + + // Destroy the SSH context + registry.destroy(currentContextId); + + // Call context switch callback to rewire events + const localContext = registry.getActive(); + const { onContextSwitched } = await import('../index'); + onContextSwitched(localContext); } + return { success: true, data: connectionManager.getStatus() }; } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/src/main/ipc/subagents.ts b/src/main/ipc/subagents.ts index 916a0286..747605d3 100644 --- a/src/main/ipc/subagents.ts +++ b/src/main/ipc/subagents.ts @@ -12,38 +12,18 @@ import { type SubagentDetail } from '../types'; import { validateProjectId, validateSessionId, validateSubagentId } from './guards'; -import type { - ChunkBuilder, - DataCache, - ProjectScanner, - SessionParser, - SubagentResolver, -} from '../services'; +import type { ServiceContextRegistry } from '../services'; const logger = createLogger('IPC:subagents'); -// Service instances - set via initialize -let chunkBuilder: ChunkBuilder; -let dataCache: DataCache; -let sessionParser: SessionParser; -let subagentResolver: SubagentResolver; -let projectScanner: ProjectScanner; +// Service registry - set via initialize +let registry: ServiceContextRegistry; /** - * Initializes subagent handlers with service instances. + * Initializes subagent handlers with service registry. */ -export function initializeSubagentHandlers( - builder: ChunkBuilder, - cache: DataCache, - parser: SessionParser, - resolver: SubagentResolver, - scanner: ProjectScanner -): void { - chunkBuilder = builder; - dataCache = cache; - sessionParser = parser; - subagentResolver = resolver; - projectScanner = scanner; +export function initializeSubagentHandlers(contextRegistry: ServiceContextRegistry): void { + registry = contextRegistry; } /** @@ -97,6 +77,9 @@ async function handleGetSubagentDetail( const safeSessionId = validatedSession.value!; const safeSubagentId = validatedSubagent.value!; + const { chunkBuilder, sessionParser, subagentResolver, projectScanner, dataCache } = + registry.getActive(); + const cacheKey = `subagent-${safeProjectId}-${safeSessionId}-${safeSubagentId}`; // Check cache first