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>
311 lines
15 KiB
Markdown
311 lines
15 KiB
Markdown
---
|
|
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)"
|
|
---
|
|
|
|
<objective>
|
|
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().
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@.planning/phases/02-service-infrastructure/02-RESEARCH.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Refactor main/index.ts to use ServiceContextRegistry</name>
|
|
<files>
|
|
src/main/index.ts
|
|
src/main/ipc/handlers.ts
|
|
</files>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
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.
|
|
</verify>
|
|
<done>
|
|
main/index.ts creates ServiceContextRegistry at startup with local context registered. handlers.ts accepts ServiceContextRegistry. reinitializeServiceHandlers is removed. Type checking passes.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Update domain IPC handlers to route via registry</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
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
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-service-infrastructure/02-02-SUMMARY.md`
|
|
</output>
|