docs(02-service-infrastructure): create phase plan (3 plans, 3 waves)
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>
This commit is contained in:
parent
ae4833e310
commit
85f34eb828
4 changed files with 824 additions and 6 deletions
|
|
@ -42,12 +42,12 @@ Plans:
|
|||
3. Switching between contexts routes IPC requests to the correct service context
|
||||
4. Disconnecting SSH and reconnecting later restores the same SSH context (not recreated from scratch)
|
||||
5. File watcher events only fire for the active context (no cross-context pollution)
|
||||
**Plans**: 2-3 plans
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 02-01: ServiceContextRegistry with context lifecycle management
|
||||
- [ ] 02-02: IPC context API and preload bridge
|
||||
- [ ] 02-03: Connection profiles in ConfigManager
|
||||
- [ ] 02-01-PLAN.md — ServiceContext bundle class, ServiceContextRegistry coordinator, and dispose() methods for FileWatcher/DataCache
|
||||
- [ ] 02-02-PLAN.md — Wire registry into main/index.ts and update all IPC handlers to route via registry
|
||||
- [ ] 02-03-PLAN.md — Context management IPC channels, preload bridge, and connection profiles in ConfigManager
|
||||
|
||||
### Phase 3: State Management
|
||||
**Goal**: Context switching preserves exact UI state per workspace with instant restoration
|
||||
|
|
@ -89,10 +89,10 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
|
|||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Provider Plumbing | 1/1 | ✓ Complete | 2026-02-12 |
|
||||
| 2. Service Infrastructure | 0/2-3 | Not started | - |
|
||||
| 2. Service Infrastructure | 0/3 | Not started | - |
|
||||
| 3. State Management | 0/1-2 | Not started | - |
|
||||
| 4. Workspace UI | 0/1-2 | Not started | - |
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-02-12*
|
||||
*Last updated: 2026-02-12 after Phase 1 execution complete*
|
||||
*Last updated: 2026-02-12 after Phase 2 planning complete*
|
||||
|
|
|
|||
208
.planning/phases/02-service-infrastructure/02-01-PLAN.md
Normal file
208
.planning/phases/02-service-infrastructure/02-01-PLAN.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
---
|
||||
phase: 02-service-infrastructure
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/main/services/infrastructure/ServiceContext.ts
|
||||
- src/main/services/infrastructure/ServiceContextRegistry.ts
|
||||
- src/main/services/infrastructure/index.ts
|
||||
- src/main/services/infrastructure/FileWatcher.ts
|
||||
- src/main/services/infrastructure/DataCache.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "ServiceContext bundles all session-data services for a single workspace context"
|
||||
- "ServiceContextRegistry manages local + N SSH contexts with create/switch/destroy lifecycle"
|
||||
- "Local context is always alive and cannot be destroyed"
|
||||
- "All EventEmitter-based services have comprehensive dispose() methods that prevent memory leaks"
|
||||
- "FileWatcher dispose() clears all timers, watchers, tracking maps, and listeners"
|
||||
artifacts:
|
||||
- path: "src/main/services/infrastructure/ServiceContext.ts"
|
||||
provides: "Service bundle class for a single workspace context"
|
||||
exports: ["ServiceContext", "ServiceContextConfig"]
|
||||
- path: "src/main/services/infrastructure/ServiceContextRegistry.ts"
|
||||
provides: "Registry coordinator for all contexts"
|
||||
exports: ["ServiceContextRegistry"]
|
||||
- path: "src/main/services/infrastructure/FileWatcher.ts"
|
||||
provides: "dispose() method on FileWatcher"
|
||||
contains: "dispose()"
|
||||
- path: "src/main/services/infrastructure/DataCache.ts"
|
||||
provides: "dispose() method on DataCache"
|
||||
contains: "dispose()"
|
||||
key_links:
|
||||
- from: "src/main/services/infrastructure/ServiceContextRegistry.ts"
|
||||
to: "src/main/services/infrastructure/ServiceContext.ts"
|
||||
via: "creates ServiceContext instances"
|
||||
pattern: "new ServiceContext"
|
||||
- from: "src/main/services/infrastructure/ServiceContext.ts"
|
||||
to: "src/main/services/infrastructure/FileWatcher.ts"
|
||||
via: "creates and owns FileWatcher instance"
|
||||
pattern: "new FileWatcher"
|
||||
- from: "src/main/services/infrastructure/ServiceContext.ts"
|
||||
to: "src/main/services/infrastructure/DataCache.ts"
|
||||
via: "creates and owns DataCache instance"
|
||||
pattern: "new DataCache"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the ServiceContext bundle class and ServiceContextRegistry coordinator, plus add comprehensive dispose() methods to all EventEmitter-based services.
|
||||
|
||||
Purpose: These are the foundational types for multi-context support. ServiceContext encapsulates all session-data services for a single workspace (local or SSH). ServiceContextRegistry manages the Map of contexts, tracks the active context, and enforces lifecycle rules (local never destroyed, proper cleanup on destroy).
|
||||
|
||||
Output: Two new TypeScript files (ServiceContext.ts, ServiceContextRegistry.ts), updated dispose methods on FileWatcher and DataCache, updated barrel exports.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@.planning/phases/02-service-infrastructure/02-RESEARCH.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/01-provider-plumbing/01-01-SUMMARY.md
|
||||
@src/main/services/infrastructure/FileSystemProvider.ts
|
||||
@src/main/services/infrastructure/FileWatcher.ts
|
||||
@src/main/services/infrastructure/DataCache.ts
|
||||
@src/main/services/infrastructure/LocalFileSystemProvider.ts
|
||||
@src/main/services/discovery/ProjectScanner.ts
|
||||
@src/main/services/parsing/SessionParser.ts
|
||||
@src/main/services/discovery/SubagentResolver.ts
|
||||
@src/main/services/analysis/ChunkBuilder.ts
|
||||
@src/main/services/infrastructure/index.ts
|
||||
@src/main/index.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create ServiceContext and ServiceContextRegistry</name>
|
||||
<files>
|
||||
src/main/services/infrastructure/ServiceContext.ts
|
||||
src/main/services/infrastructure/ServiceContextRegistry.ts
|
||||
src/main/services/infrastructure/index.ts
|
||||
</files>
|
||||
<action>
|
||||
**ServiceContext.ts** - Create a service bundle class:
|
||||
|
||||
1. Define `ServiceContextConfig` interface:
|
||||
- `id: string` (e.g., 'local', 'ssh-myserver')
|
||||
- `type: 'local' | 'ssh'`
|
||||
- `fsProvider: FileSystemProvider`
|
||||
- `projectsDir?: string` (defaults to getProjectsBasePath())
|
||||
- `todosDir?: string` (defaults to getTodosBasePath())
|
||||
|
||||
2. Define `ServiceContext` class with:
|
||||
- Readonly properties: `id`, `type`
|
||||
- Service instances (all readonly): `projectScanner: ProjectScanner`, `sessionParser: SessionParser`, `subagentResolver: SubagentResolver`, `chunkBuilder: ChunkBuilder`, `dataCache: DataCache`, `fileWatcher: FileWatcher`
|
||||
- `fsProvider: FileSystemProvider` (readonly, stored for reference)
|
||||
- Constructor that creates all services with correct dependency chain:
|
||||
- `projectScanner = new ProjectScanner(config.projectsDir, config.todosDir, config.fsProvider)`
|
||||
- `sessionParser = new SessionParser(this.projectScanner)`
|
||||
- `subagentResolver = new SubagentResolver(this.projectScanner)`
|
||||
- `chunkBuilder = new ChunkBuilder()`
|
||||
- `dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache)` where `disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1'`
|
||||
- `fileWatcher = new FileWatcher(this.dataCache)` — do NOT call setFileSystemProvider here; FileWatcher uses local fs by default and the setFileSystemProvider pattern is used externally. Actually, looking at the code, FileWatcher stores an fsProvider internally. Check its constructor — if it accepts a provider, pass it. If not, call `setFileSystemProvider(config.fsProvider)` after construction.
|
||||
- `start()` method: calls `fileWatcher.start()` and `dataCache.startAutoCleanup(CACHE_CLEANUP_INTERVAL_MINUTES)`, stores the cleanup interval handle
|
||||
- `stopFileWatcher()` method: calls `fileWatcher.stop()` (for pausing on context switch)
|
||||
- `startFileWatcher()` method: calls `fileWatcher.start()` (for resuming on context switch)
|
||||
- `dispose()` method: calls `fileWatcher.dispose()`, `dataCache.dispose()`, clears cleanup interval. Does NOT dispose fsProvider (ownership belongs to SshConnectionManager per research).
|
||||
|
||||
Import constants from `@shared/constants`: `MAX_CACHE_SESSIONS`, `CACHE_TTL_MINUTES`, `CACHE_CLEANUP_INTERVAL_MINUTES`.
|
||||
|
||||
**ServiceContextRegistry.ts** - Create the registry coordinator:
|
||||
|
||||
1. Define `ServiceContextRegistry` class:
|
||||
- `private contexts = new Map<string, ServiceContext>()`
|
||||
- `private activeContextId: string = 'local'`
|
||||
- Constructor takes NO arguments. It does NOT create the local context internally (that happens in index.ts where the mainWindow and notification manager wiring exists).
|
||||
- `registerContext(context: ServiceContext): void` — adds to map, throws if id already exists
|
||||
- `getActive(): ServiceContext` — returns context for activeContextId, throws if not found
|
||||
- `get(contextId: string): ServiceContext | undefined` — returns context by id
|
||||
- `has(contextId: string): boolean` — check existence
|
||||
- `switch(contextId: string): { previous: ServiceContext; current: ServiceContext }` — validates contextId exists, stops file watcher on previous context, sets activeContextId, starts file watcher on new context, returns both contexts for caller to do IPC re-init
|
||||
- `destroy(contextId: string): void` — throws if 'local', calls context.dispose(), removes from map, if activeContextId was this context switch to 'local'
|
||||
- `list(): Array<{ id: string; type: 'local' | 'ssh' }>` — returns metadata for all contexts
|
||||
- `getActiveContextId(): string` — getter
|
||||
- `dispose(): void` — disposes ALL contexts (including local), clears map. Used only on app shutdown.
|
||||
|
||||
Use `createLogger('Infrastructure:ServiceContextRegistry')` for logging.
|
||||
|
||||
**index.ts** - Add exports:
|
||||
- Add `export * from './ServiceContext';`
|
||||
- Add `export * from './ServiceContextRegistry';`
|
||||
</action>
|
||||
<verify>
|
||||
Run `pnpm typecheck` — must pass with zero errors. Verify the new files exist and export the expected classes.
|
||||
</verify>
|
||||
<done>
|
||||
ServiceContext.ts exports ServiceContext class and ServiceContextConfig interface. ServiceContextRegistry.ts exports ServiceContextRegistry class. Both are type-safe and compile without errors. Barrel export updated.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add comprehensive dispose() methods to FileWatcher and DataCache</name>
|
||||
<files>
|
||||
src/main/services/infrastructure/FileWatcher.ts
|
||||
src/main/services/infrastructure/DataCache.ts
|
||||
</files>
|
||||
<action>
|
||||
**FileWatcher.ts** - Add a `dispose()` method (separate from existing `stop()`):
|
||||
|
||||
The existing `stop()` method closes watchers but does NOT clear all internal state or remove EventEmitter listeners. Add a `dispose()` method that performs complete cleanup:
|
||||
|
||||
1. Call `this.stop()` first (closes fs.watch watchers)
|
||||
2. Clear retry timer: `if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = null; }`
|
||||
3. Clear all debounce timers: `for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear();`
|
||||
4. Clear catch-up timer: `if (this.catchUpTimer) { clearInterval(this.catchUpTimer); this.catchUpTimer = null; }`
|
||||
5. Clear polling timer: `if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; }`
|
||||
6. Clear all tracking maps: `this.lastProcessedLineCount.clear()`, `this.lastProcessedSize.clear()`, `this.activeSessionFiles.clear()`, `this.polledFileSizes.clear()`, `this.processingInProgress.clear()`
|
||||
7. Also clear `this.pendingReprocess` if it exists (check the class for this field)
|
||||
8. LAST: `this.removeAllListeners()` — MUST be last to prevent firing events during cleanup
|
||||
|
||||
Add a `private disposed = false` flag. Set it in dispose(). Check it in `start()` to prevent restarting a disposed watcher (throw error or log warning and return early).
|
||||
|
||||
**DataCache.ts** - Add a `dispose()` method:
|
||||
|
||||
1. Call `this.cache.clear()`
|
||||
2. Set `this.enabled = false`
|
||||
3. Add a `private disposed = false` flag, set it in dispose
|
||||
4. If DataCache has a `startAutoCleanup()` method that returns an interval handle, the caller (ServiceContext) manages that interval. But also add internal cleanup: check if there's an internal auto-cleanup timer stored as a class field. If `startAutoCleanup()` only returns the interval for the caller to manage, just clear the cache and disable.
|
||||
|
||||
Look at the actual DataCache code to see if it stores the cleanup interval internally. If it only returns it, the caller is responsible. If it stores it, clear it in dispose().
|
||||
</action>
|
||||
<verify>
|
||||
Run `pnpm typecheck` — must pass. Run `pnpm test` — all existing tests must still pass. Verify FileWatcher.dispose() exists and calls removeAllListeners(). Verify DataCache.dispose() exists and clears cache.
|
||||
</verify>
|
||||
<done>
|
||||
FileWatcher has a dispose() method that: stops watchers, clears ALL timers (retry, debounce, catch-up, polling), clears ALL tracking maps, and calls removeAllListeners() LAST. DataCache has a dispose() method that clears the cache and disables it. Both have a `disposed` flag to prevent reuse after disposal. All existing tests pass.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `pnpm typecheck` passes with zero errors
|
||||
2. `pnpm test` — all existing tests pass (no regressions)
|
||||
3. New files exist at expected paths: `src/main/services/infrastructure/ServiceContext.ts`, `src/main/services/infrastructure/ServiceContextRegistry.ts`
|
||||
4. ServiceContext constructor creates all 6 services correctly
|
||||
5. ServiceContextRegistry.switch() stops old FileWatcher and starts new one
|
||||
6. ServiceContextRegistry.destroy('local') throws an error
|
||||
7. FileWatcher.dispose() calls removeAllListeners()
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- ServiceContext class creates isolated service bundles with correct dependency chain
|
||||
- ServiceContextRegistry manages Map of contexts with active tracking
|
||||
- Local context cannot be destroyed (throws on attempt)
|
||||
- FileWatcher.dispose() performs comprehensive cleanup (timers, maps, listeners)
|
||||
- DataCache.dispose() clears cache and disables
|
||||
- All existing tests pass with no regressions
|
||||
- Type checking passes
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-service-infrastructure/02-01-SUMMARY.md`
|
||||
</output>
|
||||
311
.planning/phases/02-service-infrastructure/02-02-PLAN.md
Normal file
311
.planning/phases/02-service-infrastructure/02-02-PLAN.md
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
---
|
||||
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>
|
||||
299
.planning/phases/02-service-infrastructure/02-03-PLAN.md
Normal file
299
.planning/phases/02-service-infrastructure/02-03-PLAN.md
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
---
|
||||
phase: 02-service-infrastructure
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["02-02"]
|
||||
files_modified:
|
||||
- src/main/ipc/context.ts
|
||||
- src/main/ipc/handlers.ts
|
||||
- src/preload/constants/ipcChannels.ts
|
||||
- src/preload/index.ts
|
||||
- src/shared/types/api.ts
|
||||
- src/main/services/infrastructure/ConfigManager.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Renderer can list all available contexts (local + SSH workspaces)"
|
||||
- "Renderer can request a context switch and receive confirmation"
|
||||
- "Renderer can get the current active context ID"
|
||||
- "Context change events propagate to renderer via IPC"
|
||||
- "SSH connection profiles are persisted in ConfigManager for reconnection"
|
||||
artifacts:
|
||||
- path: "src/main/ipc/context.ts"
|
||||
provides: "Context management IPC handlers"
|
||||
exports: ["initializeContextHandlers", "registerContextHandlers", "removeContextHandlers"]
|
||||
- path: "src/preload/constants/ipcChannels.ts"
|
||||
provides: "Context IPC channel constants"
|
||||
contains: "CONTEXT_"
|
||||
- path: "src/preload/index.ts"
|
||||
provides: "Context API exposed to renderer"
|
||||
contains: "context:"
|
||||
- path: "src/shared/types/api.ts"
|
||||
provides: "Context API type definitions on ElectronAPI"
|
||||
contains: "context"
|
||||
key_links:
|
||||
- from: "src/preload/index.ts"
|
||||
to: "src/main/ipc/context.ts"
|
||||
via: "IPC invoke for context operations"
|
||||
pattern: "ipcRenderer\\.invoke.*CONTEXT_"
|
||||
- from: "src/main/ipc/context.ts"
|
||||
to: "src/main/services/infrastructure/ServiceContextRegistry.ts"
|
||||
via: "calls registry methods"
|
||||
pattern: "registry\\.(list|switch|getActiveContextId)"
|
||||
- from: "src/main/services/infrastructure/ConfigManager.ts"
|
||||
to: "src/shared/types/api.ts"
|
||||
via: "connection profiles stored in config"
|
||||
pattern: "sshProfiles"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the context management IPC channels, preload bridge, and connection profile persistence so the renderer can query, switch, and manage workspace contexts.
|
||||
|
||||
Purpose: This completes the service infrastructure by giving the renderer process the ability to interact with the context system. Without this, the registry exists but the UI has no way to list contexts, trigger switches, or restore saved connections. Connection profiles in ConfigManager enable reconnection without re-entering credentials (Success Criterion 4).
|
||||
|
||||
Output: New context IPC handler module, updated preload bridge with context API, connection profiles in ConfigManager config schema.
|
||||
</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
|
||||
@.planning/phases/02-service-infrastructure/02-02-PLAN.md
|
||||
@src/main/ipc/handlers.ts
|
||||
@src/main/ipc/ssh.ts
|
||||
@src/preload/index.ts
|
||||
@src/preload/constants/ipcChannels.ts
|
||||
@src/shared/types/api.ts
|
||||
@src/main/services/infrastructure/ConfigManager.ts
|
||||
@src/main/services/infrastructure/ServiceContextRegistry.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create context IPC handler and channel constants</name>
|
||||
<files>
|
||||
src/main/ipc/context.ts
|
||||
src/main/ipc/handlers.ts
|
||||
src/preload/constants/ipcChannels.ts
|
||||
</files>
|
||||
<action>
|
||||
**src/preload/constants/ipcChannels.ts** - Add context channel constants:
|
||||
|
||||
```typescript
|
||||
// Context API Channels
|
||||
export const CONTEXT_LIST = 'context:list';
|
||||
export const CONTEXT_GET_ACTIVE = 'context:getActive';
|
||||
export const CONTEXT_SWITCH = 'context:switch';
|
||||
export const CONTEXT_CHANGED = 'context:changed'; // main -> renderer event
|
||||
```
|
||||
|
||||
Note: context:connect-ssh and context:disconnect-ssh are NOT needed here — those operations are handled by the existing SSH IPC handlers (ssh:connect, ssh:disconnect) which now internally create/destroy contexts via the registry (done in Plan 02). The context IPC is for listing and switching only.
|
||||
|
||||
**src/main/ipc/context.ts** - Create context management handler module:
|
||||
|
||||
Follow the existing handler pattern (initialize, register, remove exports):
|
||||
|
||||
1. Module state:
|
||||
```typescript
|
||||
let registry: ServiceContextRegistry;
|
||||
```
|
||||
|
||||
2. `initializeContextHandlers(reg: ServiceContextRegistry)`:
|
||||
- Store registry reference
|
||||
|
||||
3. `registerContextHandlers(ipcMain: IpcMain)`:
|
||||
|
||||
- `CONTEXT_LIST` handler: Returns `registry.list()` — array of `{ id: string; type: 'local' | 'ssh' }`. Wrap in try/catch, return `{ success: true, data: [...] }`.
|
||||
|
||||
- `CONTEXT_GET_ACTIVE` handler: Returns `registry.getActiveContextId()`. Wrap in try/catch, return `{ success: true, data: contextId }`.
|
||||
|
||||
- `CONTEXT_SWITCH` handler: Takes `contextId: string` argument. Calls `registry.switch(contextId)`. On success return `{ success: true, data: { contextId } }`. On error (context doesn't exist), return `{ success: false, error: message }`.
|
||||
|
||||
Important: After switching, the caller (or this handler) should also trigger the `onContextSwitched` callback to re-wire FileWatcher events. Two approaches:
|
||||
- Store the onContextSwitched callback in this module too
|
||||
- Have the switch handler emit the CONTEXT_CHANGED event to renderer
|
||||
|
||||
Approach: Accept an `onContextSwitched` callback in `initializeContextHandlers` (same pattern as SSH handler). After successful switch, call it with the new active context, and also send CONTEXT_CHANGED event to renderer via mainWindow.
|
||||
|
||||
Updated init signature: `initializeContextHandlers(reg: ServiceContextRegistry, onSwitched: (context: ServiceContext) => void)`
|
||||
|
||||
In the CONTEXT_SWITCH handler:
|
||||
```typescript
|
||||
const { current } = registry.switch(contextId);
|
||||
onSwitched(current);
|
||||
// mainWindow notification happens inside onSwitched callback
|
||||
return { success: true, data: { contextId } };
|
||||
```
|
||||
|
||||
4. `removeContextHandlers(ipcMain: IpcMain)`:
|
||||
- Remove all three handlers
|
||||
|
||||
**src/main/ipc/handlers.ts** - Register the new context handler module:
|
||||
|
||||
1. Import: `import { initializeContextHandlers, registerContextHandlers, removeContextHandlers } from './context';`
|
||||
2. In `initializeIpcHandlers()`, add: `initializeContextHandlers(registry, onContextSwitched)` — the `onContextSwitched` callback needs to be passed through. Update the `initializeIpcHandlers` signature to accept it:
|
||||
```typescript
|
||||
export function initializeIpcHandlers(
|
||||
registry: ServiceContextRegistry,
|
||||
updater: UpdaterService,
|
||||
sshManager: SshConnectionManager,
|
||||
onContextSwitched: (context: ServiceContext) => void,
|
||||
): void
|
||||
```
|
||||
3. Register: `registerContextHandlers(ipcMain);`
|
||||
4. Remove: `removeContextHandlers(ipcMain);` in `removeIpcHandlers()`
|
||||
</action>
|
||||
<verify>
|
||||
Run `pnpm typecheck` — must pass. Verify context.ts exports the three standard functions. Verify ipcChannels.ts has CONTEXT_LIST, CONTEXT_GET_ACTIVE, CONTEXT_SWITCH, CONTEXT_CHANGED constants.
|
||||
</verify>
|
||||
<done>
|
||||
Context IPC handler created with list, getActive, and switch operations. Channel constants defined. Handler registered in handlers.ts init/register/remove flow. Context switching triggers onContextSwitched callback for event re-wiring.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Expose context API in preload bridge and add connection profiles to ConfigManager</name>
|
||||
<files>
|
||||
src/preload/index.ts
|
||||
src/shared/types/api.ts
|
||||
src/main/services/infrastructure/ConfigManager.ts
|
||||
</files>
|
||||
<action>
|
||||
**src/shared/types/api.ts** - Add context API types to ElectronAPI:
|
||||
|
||||
1. Define `ContextInfo` type:
|
||||
```typescript
|
||||
export interface ContextInfo {
|
||||
id: string;
|
||||
type: 'local' | 'ssh';
|
||||
}
|
||||
```
|
||||
|
||||
2. Define `SshConnectionProfile` type (for saved profiles):
|
||||
```typescript
|
||||
export interface SshConnectionProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
authMethod: 'password' | 'privateKey' | 'agent' | 'auto';
|
||||
privateKeyPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
3. Add `context` property to `ElectronAPI` interface:
|
||||
```typescript
|
||||
context: {
|
||||
list: () => Promise<ContextInfo[]>;
|
||||
getActive: () => Promise<string>;
|
||||
switch: (contextId: string) => Promise<{ contextId: string }>;
|
||||
onChanged: (callback: (event: unknown, data: ContextInfo) => void) => () => void;
|
||||
};
|
||||
```
|
||||
|
||||
Note: Check the existing `ElectronAPI` type definition in `src/shared/types/api.ts` to see the exact structure and follow its conventions. The type may be defined there or it may be inferred from the preload implementation. If it's explicitly defined, add the context property. If it's not (i.e., type is inferred from `contextBridge.exposeInMainWorld`), then just update preload/index.ts and the type will follow.
|
||||
|
||||
**src/preload/index.ts** - Add context methods to electronAPI:
|
||||
|
||||
1. Import new channel constants: `CONTEXT_LIST`, `CONTEXT_GET_ACTIVE`, `CONTEXT_SWITCH`, `CONTEXT_CHANGED`
|
||||
|
||||
2. Add `context` namespace to the electronAPI object (alongside existing `ssh`, `config`, `notifications` etc.):
|
||||
```typescript
|
||||
context: {
|
||||
list: async () => {
|
||||
return invokeIpcWithResult<ContextInfo[]>(CONTEXT_LIST);
|
||||
},
|
||||
getActive: async () => {
|
||||
return invokeIpcWithResult<string>(CONTEXT_GET_ACTIVE);
|
||||
},
|
||||
switch: async (contextId: string) => {
|
||||
return invokeIpcWithResult<{ contextId: string }>(CONTEXT_SWITCH, contextId);
|
||||
},
|
||||
onChanged: (callback: (event: unknown, data: { id: string; type: string }) => void) => {
|
||||
ipcRenderer.on(CONTEXT_CHANGED, callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void);
|
||||
return () => {
|
||||
ipcRenderer.removeListener(CONTEXT_CHANGED, callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void);
|
||||
};
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
3. Import `ContextInfo` type if needed for type safety (may need to add to the imports from `@shared/types`).
|
||||
|
||||
**src/main/services/infrastructure/ConfigManager.ts** - Add connection profiles:
|
||||
|
||||
1. Add `SshConnectionProfile` interface (or import from shared types if defined there):
|
||||
```typescript
|
||||
export interface SshConnectionProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
authMethod: SshAuthMethod;
|
||||
privateKeyPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
2. Find the SSH config section type in ConfigManager. Currently there's `ssh.lastConnection`. Extend to add `ssh.profiles`:
|
||||
```typescript
|
||||
ssh: {
|
||||
lastConnection: SshLastConnection | null;
|
||||
profiles: SshConnectionProfile[];
|
||||
lastActiveContextId: string; // Which context was active when app closed
|
||||
}
|
||||
```
|
||||
|
||||
3. Add defaults in the default config: `profiles: []`, `lastActiveContextId: 'local'`
|
||||
|
||||
4. Add profile management methods to ConfigManager:
|
||||
- `addSshProfile(profile: SshConnectionProfile): void` — adds to profiles array, saves
|
||||
- `removeSshProfile(profileId: string): void` — removes by id, saves
|
||||
- `updateSshProfile(profileId: string, updates: Partial<SshConnectionProfile>): void` — updates, saves
|
||||
- `getSshProfiles(): SshConnectionProfile[]` — returns profiles array
|
||||
- `setLastActiveContextId(contextId: string): void` — persists for restore on restart
|
||||
|
||||
5. Ensure the config migration handles existing configs that don't have `profiles` or `lastActiveContextId` fields (add them with defaults in the config loading/merge logic).
|
||||
</action>
|
||||
<verify>
|
||||
1. Run `pnpm typecheck` — must pass with zero errors
|
||||
2. Run `pnpm test` — all existing tests pass (especially config-related tests)
|
||||
3. Verify preload/index.ts has `context` namespace with list, getActive, switch, onChanged methods
|
||||
4. Verify ConfigManager has ssh.profiles and ssh.lastActiveContextId with defaults
|
||||
5. Verify ElectronAPI type includes context property
|
||||
</verify>
|
||||
<done>
|
||||
Renderer can call window.electronAPI.context.list(), context.getActive(), context.switch(id), and context.onChanged(callback). ConfigManager stores SSH connection profiles array and last active context ID for reconnection on restart. All types are properly shared between processes.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `pnpm typecheck` passes with zero errors
|
||||
2. `pnpm test` — all existing tests pass
|
||||
3. Context IPC channels exist: context:list, context:getActive, context:switch, context:changed
|
||||
4. Preload exposes window.electronAPI.context with 4 methods
|
||||
5. ConfigManager config includes ssh.profiles (array) and ssh.lastActiveContextId (string)
|
||||
6. ElectronAPI type includes context property definition
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Renderer process can list all contexts, get active context, switch contexts, and listen for context changes
|
||||
- SSH connection profiles persisted in ConfigManager for quick reconnection
|
||||
- Last active context ID persisted for app restart restoration
|
||||
- All IPC channels follow existing naming and error handling patterns
|
||||
- No regressions in existing tests or type checking
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-service-infrastructure/02-03-SUMMARY.md`
|
||||
</output>
|
||||
Loading…
Reference in a new issue