agent-ecosystem/.planning/phases/02-service-infrastructure/02-03-PLAN.md
matt 85f34eb828 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>
2026-02-12 00:49:19 +00:00

13 KiB

phase plan type wave depends_on files_modified autonomous must_haves
02-service-infrastructure 03 execute 3
02-02
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
true
truths artifacts key_links
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
path provides exports
src/main/ipc/context.ts Context management IPC handlers
initializeContextHandlers
registerContextHandlers
removeContextHandlers
path provides contains
src/preload/constants/ipcChannels.ts Context IPC channel constants CONTEXT_
path provides contains
src/preload/index.ts Context API exposed to renderer context:
path provides contains
src/shared/types/api.ts Context API type definitions on ElectronAPI context
from to via pattern
src/preload/index.ts src/main/ipc/context.ts IPC invoke for context operations ipcRenderer.invoke.*CONTEXT_
from to via pattern
src/main/ipc/context.ts src/main/services/infrastructure/ServiceContextRegistry.ts calls registry methods registry.(list|switch|getActiveContextId)
from to via pattern
src/main/services/infrastructure/ConfigManager.ts src/shared/types/api.ts connection profiles stored in config sshProfiles
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.

<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 @.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 Task 1: Create context IPC handler and channel constants src/main/ipc/context.ts src/main/ipc/handlers.ts src/preload/constants/ipcChannels.ts **src/preload/constants/ipcChannels.ts** - Add context channel constants:
// 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:

    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:

      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:
    export function initializeIpcHandlers(
      registry: ServiceContextRegistry,
      updater: UpdaterService,
      sshManager: SshConnectionManager,
      onContextSwitched: (context: ServiceContext) => void,
    ): void
    
  3. Register: registerContextHandlers(ipcMain);
  4. Remove: removeContextHandlers(ipcMain); in removeIpcHandlers() 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. 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.
Task 2: Expose context API in preload bridge and add connection profiles to ConfigManager src/preload/index.ts src/shared/types/api.ts src/main/services/infrastructure/ConfigManager.ts **src/shared/types/api.ts** - Add context API types to ElectronAPI:
  1. Define ContextInfo type:

    export interface ContextInfo {
      id: string;
      type: 'local' | 'ssh';
    }
    
  2. Define SshConnectionProfile type (for saved profiles):

    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:

    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.):

    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):

    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:

    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).

  6. Run pnpm typecheck — must pass with zero errors

  7. Run pnpm test — all existing tests pass (especially config-related tests)

  8. Verify preload/index.ts has context namespace with list, getActive, switch, onChanged methods

  9. Verify ConfigManager has ssh.profiles and ssh.lastActiveContextId with defaults

  10. Verify ElectronAPI type includes context property 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.

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

<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>
After completion, create `.planning/phases/02-service-infrastructure/02-03-SUMMARY.md`