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 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<Project[]> {
try {
const { projectScanner } = registry.getActive();
const projects = await projectScanner.scan();
return projects;
} catch (error) {
@ -75,6 +76,7 @@ async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]>
*/
async function handleGetRepositoryGroups(_event: IpcMainInvokeEvent): Promise<RepositoryGroup[]> {
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) {

View file

@ -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!,

View file

@ -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);

View file

@ -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<void>) | null = null;
let registry: ServiceContextRegistry;
// =============================================================================
// Initialization
@ -46,14 +47,14 @@ let onModeSwitch: ((mode: 'local' | 'ssh') => Promise<void>) | 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<void>
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);

View file

@ -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