Plan 01: ServiceContext + ServiceContextRegistry + dispose() methods Plan 02: Wire registry into main/index.ts + update IPC routing Plan 03: Context management IPC + preload bridge + connection profiles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-service-infrastructure | 02 | execute | 2 |
|
|
true |
|
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().
<execution_context> @.planning/phases/02-service-infrastructure/02-RESEARCH.md </execution_context>
@.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:-
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;
-
Add:
let contextRegistry: ServiceContextRegistry; -
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
contextRegistryinstead of individual services. Also passsshConnectionManager. - Forward SSH state changes (unchanged)
- Create
-
Remove the
handleModeSwitchcallback entirely. SSH connect/disconnect will be handled by the SSH IPC handler using the registry directly (Plan 02 Task 2 below). -
Rewrite
shutdownServices():- Call
contextRegistry.dispose()(disposes all contexts including local) - Dispose
sshConnectionManager - Call
removeIpcHandlers()
- Call
-
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:
-
Change
initializeIpcHandlerssignature to acceptServiceContextRegistryinstead of individual services:export function initializeIpcHandlers( registry: ServiceContextRegistry, updater: UpdaterService, sshManager: SshConnectionManager, ): void -
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 callbackinitializeUpdaterHandlers(updater)— unchanged (not context-dependent)
-
Remove
reinitializeServiceHandlers()entirely — no longer needed because handlers always callregistry.getActive()to get current services. -
Keep
removeIpcHandlers()unchanged. Runpnpm 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.
src/main/ipc/projects.ts:
- Change
initializeProjectHandlers(scanner: ProjectScanner)toinitializeProjectHandlers(registry: ServiceContextRegistry) - Store registry as module-level variable:
let registry: ServiceContextRegistry; - Remove module-level
scannervariable - In each handler, resolve scanner from registry:
const { projectScanner } = registry.getActive(); - Update all references from
scanner.toprojectScanner.
src/main/ipc/sessions.ts:
- Change
initializeSessionHandlers(scanner, parser, resolver, builder, cache)toinitializeSessionHandlers(registry: ServiceContextRegistry) - Store registry as module-level variable
- Remove module-level scanner, parser, resolver, builder, cache variables
- In each handler, destructure from registry:
const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } = registry.getActive(); - Update all references to use destructured names
src/main/ipc/search.ts:
- Change
initializeSearchHandlers(scanner: ProjectScanner)toinitializeSearchHandlers(registry: ServiceContextRegistry) - Store registry, resolve from
registry.getActive()in handlers - Update references
src/main/ipc/subagents.ts:
- Change
initializeSubagentHandlers(builder, cache, parser, resolver, scanner)toinitializeSubagentHandlers(registry: ServiceContextRegistry) - Store registry, resolve from
registry.getActive()in handlers - In the handler, get
fsProviderandprojectsDirfromregistry.getActive().projectScanneras was done in Phase 1
src/main/ipc/ssh.ts:
- Change
initializeSshHandlers(manager, modeSwitchCallback)toinitializeSshHandlers(manager: SshConnectionManager, registry: ServiceContextRegistry) - Store registry as module-level variable, remove
onModeSwitchcallback - Rewrite
SSH_CONNECThandler:- Call
connectionManager.connect(config)(unchanged) - Get provider and remoteProjectsPath from connectionManager
- Generate contextId:
ssh-${config.host}(orssh-${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
- Call
- Rewrite
SSH_DISCONNECThandler:- 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
- 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
rewireFileWatcherEventsfrom index.ts (simple but circular dependency risk) - Option B: Pass a callback to
initializeSshHandlersthat the SSH handler calls after switching - Option C: Have
initializeSshHandlersalso acceptmainWindowreference 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:
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.
- Run
pnpm typecheck— must pass with zero errors - Run
pnpm test— all existing tests must pass (IPC handler tests may need mock updates) - Verify no remaining module-level service variables in sessions.ts, projects.ts, search.ts, subagents.ts (should all use
registry) - 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.
<success_criteria>
- 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 </success_criteria>