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
This commit is contained in:
matt 2026-02-12 01:04:29 +00:00
parent 5bf41c6ed8
commit 24051acac8
5 changed files with 100 additions and 70 deletions

View file

@ -14,18 +14,18 @@ import { type Project, type RepositoryGroup, type Session } from '../types';
import { validateProjectId } from './guards'; import { validateProjectId } from './guards';
import type { ProjectScanner } from '../services'; import type { ServiceContextRegistry } from '../services';
const logger = createLogger('IPC:projects'); const logger = createLogger('IPC:projects');
// Service instance - set via initialize // Service registry - set via initialize
let projectScanner: ProjectScanner; let registry: ServiceContextRegistry;
/** /**
* Initializes project handlers with service instance. * Initializes project handlers with service registry.
*/ */
export function initializeProjectHandlers(scanner: ProjectScanner): void { export function initializeProjectHandlers(contextRegistry: ServiceContextRegistry): void {
projectScanner = scanner; registry = contextRegistry;
} }
/** /**
@ -60,6 +60,7 @@ export function removeProjectHandlers(ipcMain: IpcMain): void {
*/ */
async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]> { async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]> {
try { try {
const { projectScanner } = registry.getActive();
const projects = await projectScanner.scan(); const projects = await projectScanner.scan();
return projects; return projects;
} catch (error) { } catch (error) {
@ -75,6 +76,7 @@ async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]>
*/ */
async function handleGetRepositoryGroups(_event: IpcMainInvokeEvent): Promise<RepositoryGroup[]> { async function handleGetRepositoryGroups(_event: IpcMainInvokeEvent): Promise<RepositoryGroup[]> {
try { try {
const { projectScanner } = registry.getActive();
const groups = await projectScanner.scanWithWorktreeGrouping(); const groups = await projectScanner.scanWithWorktreeGrouping();
return groups; return groups;
} catch (error) { } catch (error) {
@ -100,6 +102,7 @@ async function handleGetWorktreeSessions(
return []; return [];
} }
const { projectScanner } = registry.getActive();
const sessions = await projectScanner.listWorktreeSessions(validatedProject.value!); const sessions = await projectScanner.listWorktreeSessions(validatedProject.value!);
return sessions; return sessions;
} catch (error) { } catch (error) {

View file

@ -14,16 +14,16 @@ import { coerceSearchMaxResults, validateProjectId, validateSearchQuery } from '
const logger = createLogger('IPC:search'); const logger = createLogger('IPC:search');
import type { ProjectScanner } from '../services'; import type { ServiceContextRegistry } from '../services';
// Service instance - set via initialize // Service registry - set via initialize
let projectScanner: ProjectScanner; let registry: ServiceContextRegistry;
/** /**
* Initializes search handlers with service instance. * Initializes search handlers with service registry.
*/ */
export function initializeSearchHandlers(scanner: ProjectScanner): void { export function initializeSearchHandlers(contextRegistry: ServiceContextRegistry): void {
projectScanner = scanner; registry = contextRegistry;
} }
/** /**
@ -68,6 +68,7 @@ async function handleSearchSessions(
return { results: [], totalMatches: 0, sessionsSearched: 0, query }; return { results: [], totalMatches: 0, sessionsSearched: 0, query };
} }
const { projectScanner } = registry.getActive();
const safeMaxResults = coerceSearchMaxResults(maxResults, 50); const safeMaxResults = coerceSearchMaxResults(maxResults, 50);
const result = await projectScanner.searchSessions( const result = await projectScanner.searchSessions(
validatedProject.value!, validatedProject.value!,

View file

@ -24,33 +24,19 @@ import {
import { coercePageLimit, validateProjectId, validateSessionId } from './guards'; 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'; import type { WaterfallData } from '@shared/types';
const logger = createLogger('IPC:sessions'); const logger = createLogger('IPC:sessions');
// Service instances - set via initialize // Service registry - set via initialize
let projectScanner: ProjectScanner; let registry: ServiceContextRegistry;
let sessionParser: SessionParser;
let subagentResolver: SubagentResolver;
let chunkBuilder: ChunkBuilder;
let dataCache: DataCache;
/** /**
* Initializes session handlers with service instances. * Initializes session handlers with service registry.
*/ */
export function initializeSessionHandlers( export function initializeSessionHandlers(contextRegistry: ServiceContextRegistry): void {
scanner: ProjectScanner, registry = contextRegistry;
parser: SessionParser,
resolver: SubagentResolver,
builder: ChunkBuilder,
cache: DataCache
): void {
projectScanner = scanner;
sessionParser = parser;
subagentResolver = resolver;
chunkBuilder = builder;
dataCache = cache;
} }
/** /**
@ -100,6 +86,7 @@ async function handleGetSessions(
return []; return [];
} }
const { projectScanner } = registry.getActive();
const sessions = await projectScanner.listSessions(validatedProject.value!); const sessions = await projectScanner.listSessions(validatedProject.value!);
return sessions; return sessions;
} catch (error) { } catch (error) {
@ -128,6 +115,7 @@ async function handleGetSessionsPaginated(
return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 }; return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };
} }
const { projectScanner } = registry.getActive();
const safeLimit = coercePageLimit(limit, 20); const safeLimit = coercePageLimit(limit, 20);
const result = await projectScanner.listSessionsPaginated( const result = await projectScanner.listSessionsPaginated(
validatedProject.value!, validatedProject.value!,
@ -162,6 +150,9 @@ async function handleGetSessionDetail(
return null; return null;
} }
const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } =
registry.getActive();
const safeProjectId = validatedProject.value!; const safeProjectId = validatedProject.value!;
const safeSessionId = validatedSession.value!; const safeSessionId = validatedSession.value!;
const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId); const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
@ -223,6 +214,7 @@ async function handleGetSessionGroups(
); );
return []; return [];
} }
const { sessionParser, subagentResolver, chunkBuilder } = registry.getActive();
const safeProjectId = validatedProject.value!; const safeProjectId = validatedProject.value!;
const safeSessionId = validatedSession.value!; const safeSessionId = validatedSession.value!;
@ -262,6 +254,7 @@ async function handleGetSessionMetrics(
if (!validatedProject.valid || !validatedSession.valid) { if (!validatedProject.valid || !validatedSession.valid) {
return null; return null;
} }
const { sessionParser, dataCache } = registry.getActive();
const safeProjectId = validatedProject.value!; const safeProjectId = validatedProject.value!;
const safeSessionId = validatedSession.value!; const safeSessionId = validatedSession.value!;
@ -297,6 +290,7 @@ async function handleGetWaterfallData(
return null; return null;
} }
const { chunkBuilder } = registry.getActive();
return chunkBuilder.buildWaterfallData(detail.chunks, detail.processes); return chunkBuilder.buildWaterfallData(detail.chunks, detail.processes);
} catch (error) { } catch (error) {
logger.error(`Error in get-waterfall-data for ${projectId}/${sessionId}:`, error); logger.error(`Error in get-waterfall-data for ${projectId}/${sessionId}:`, error);

View file

@ -2,8 +2,8 @@
* SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests. * SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests.
* *
* Channels: * Channels:
* - ssh:connect - Connect to SSH host, switch to remote mode * - ssh:connect - Connect to SSH host, create new context
* - ssh:disconnect - Disconnect and switch back to local mode * - ssh:disconnect - Disconnect and switch back to local context
* - ssh:getState - Get current connection state * - ssh:getState - Get current connection state
* - ssh:test - Test connection without switching * - ssh:test - Test connection without switching
*/ */
@ -20,13 +20,14 @@ import {
} from '@preload/constants/ipcChannels'; } from '@preload/constants/ipcChannels';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { configManager } from '../services'; import { configManager, ServiceContext } from '../services';
import type { import type {
ServiceContextRegistry,
SshConnectionConfig, SshConnectionConfig,
SshConnectionManager, SshConnectionManager,
SshConnectionStatus, SshConnectionStatus,
} from '../services/infrastructure/SshConnectionManager'; } from '../services';
import type { SshLastConnection } from '@shared/types'; import type { SshLastConnection } from '@shared/types';
import type { IpcMain } from 'electron'; import type { IpcMain } from 'electron';
@ -37,7 +38,7 @@ const logger = createLogger('IPC:ssh');
// ============================================================================= // =============================================================================
let connectionManager: SshConnectionManager; let connectionManager: SshConnectionManager;
let onModeSwitch: ((mode: 'local' | 'ssh') => Promise<void>) | null = null; let registry: ServiceContextRegistry;
// ============================================================================= // =============================================================================
// Initialization // Initialization
@ -46,14 +47,14 @@ let onModeSwitch: ((mode: 'local' | 'ssh') => Promise<void>) | null = null;
/** /**
* Initialize SSH handlers with required services. * Initialize SSH handlers with required services.
* @param manager - The SSH connection manager instance * @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( export function initializeSshHandlers(
manager: SshConnectionManager, manager: SshConnectionManager,
modeSwitchCallback: (mode: 'local' | 'ssh') => Promise<void> contextRegistry: ServiceContextRegistry
): void { ): void {
connectionManager = manager; connectionManager = manager;
onModeSwitch = modeSwitchCallback; registry = contextRegistry;
} }
// ============================================================================= // =============================================================================
@ -63,10 +64,41 @@ export function initializeSshHandlers(
export function registerSshHandlers(ipcMain: IpcMain): void { export function registerSshHandlers(ipcMain: IpcMain): void {
ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => { ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => {
try { try {
// Connect to SSH host
await connectionManager.connect(config); 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() }; return { success: true, data: connectionManager.getStatus() };
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(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 () => { ipcMain.handle(SSH_DISCONNECT, async () => {
try { try {
// Get current SSH context ID before disconnecting
const currentContextId = registry.getActiveContextId();
const isSshContext = currentContextId.startsWith('ssh-');
// Disconnect from SSH
connectionManager.disconnect(); 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() }; return { success: true, data: connectionManager.getStatus() };
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);

View file

@ -12,38 +12,18 @@ import { type SubagentDetail } from '../types';
import { validateProjectId, validateSessionId, validateSubagentId } from './guards'; import { validateProjectId, validateSessionId, validateSubagentId } from './guards';
import type { import type { ServiceContextRegistry } from '../services';
ChunkBuilder,
DataCache,
ProjectScanner,
SessionParser,
SubagentResolver,
} from '../services';
const logger = createLogger('IPC:subagents'); const logger = createLogger('IPC:subagents');
// Service instances - set via initialize // Service registry - set via initialize
let chunkBuilder: ChunkBuilder; let registry: ServiceContextRegistry;
let dataCache: DataCache;
let sessionParser: SessionParser;
let subagentResolver: SubagentResolver;
let projectScanner: ProjectScanner;
/** /**
* Initializes subagent handlers with service instances. * Initializes subagent handlers with service registry.
*/ */
export function initializeSubagentHandlers( export function initializeSubagentHandlers(contextRegistry: ServiceContextRegistry): void {
builder: ChunkBuilder, registry = contextRegistry;
cache: DataCache,
parser: SessionParser,
resolver: SubagentResolver,
scanner: ProjectScanner
): void {
chunkBuilder = builder;
dataCache = cache;
sessionParser = parser;
subagentResolver = resolver;
projectScanner = scanner;
} }
/** /**
@ -97,6 +77,9 @@ async function handleGetSubagentDetail(
const safeSessionId = validatedSession.value!; const safeSessionId = validatedSession.value!;
const safeSubagentId = validatedSubagent.value!; const safeSubagentId = validatedSubagent.value!;
const { chunkBuilder, sessionParser, subagentResolver, projectScanner, dataCache } =
registry.getActive();
const cacheKey = `subagent-${safeProjectId}-${safeSessionId}-${safeSubagentId}`; const cacheKey = `subagent-${safeProjectId}-${safeSessionId}-${safeSubagentId}`;
// Check cache first // Check cache first