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>
13 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-service-infrastructure | 03 | execute | 3 |
|
|
true |
|
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):
-
Module state:
let registry: ServiceContextRegistry; -
initializeContextHandlers(reg: ServiceContextRegistry):- Store registry reference
-
registerContextHandlers(ipcMain: IpcMain):-
CONTEXT_LISThandler: Returnsregistry.list()— array of{ id: string; type: 'local' | 'ssh' }. Wrap in try/catch, return{ success: true, data: [...] }. -
CONTEXT_GET_ACTIVEhandler: Returnsregistry.getActiveContextId(). Wrap in try/catch, return{ success: true, data: contextId }. -
CONTEXT_SWITCHhandler: TakescontextId: stringargument. Callsregistry.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
onContextSwitchedcallback 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
onContextSwitchedcallback ininitializeContextHandlers(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 } };
-
-
removeContextHandlers(ipcMain: IpcMain):- Remove all three handlers
src/main/ipc/handlers.ts - Register the new context handler module:
- Import:
import { initializeContextHandlers, registerContextHandlers, removeContextHandlers } from './context'; - In
initializeIpcHandlers(), add:initializeContextHandlers(registry, onContextSwitched)— theonContextSwitchedcallback needs to be passed through. Update theinitializeIpcHandlerssignature to accept it:export function initializeIpcHandlers( registry: ServiceContextRegistry, updater: UpdaterService, sshManager: SshConnectionManager, onContextSwitched: (context: ServiceContext) => void, ): void - Register:
registerContextHandlers(ipcMain); - Remove:
removeContextHandlers(ipcMain);inremoveIpcHandlers()Runpnpm 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.
-
Define
ContextInfotype:export interface ContextInfo { id: string; type: 'local' | 'ssh'; } -
Define
SshConnectionProfiletype (for saved profiles):export interface SshConnectionProfile { id: string; name: string; host: string; port: number; username: string; authMethod: 'password' | 'privateKey' | 'agent' | 'auto'; privateKeyPath?: string; } -
Add
contextproperty toElectronAPIinterface: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
ElectronAPItype definition insrc/shared/types/api.tsto 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 fromcontextBridge.exposeInMainWorld), then just update preload/index.ts and the type will follow.
src/preload/index.ts - Add context methods to electronAPI:
-
Import new channel constants:
CONTEXT_LIST,CONTEXT_GET_ACTIVE,CONTEXT_SWITCH,CONTEXT_CHANGED -
Add
contextnamespace to the electronAPI object (alongside existingssh,config,notificationsetc.):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); }; }, }, -
Import
ContextInfotype if needed for type safety (may need to add to the imports from@shared/types).
src/main/services/infrastructure/ConfigManager.ts - Add connection profiles:
-
Add
SshConnectionProfileinterface (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; } -
Find the SSH config section type in ConfigManager. Currently there's
ssh.lastConnection. Extend to addssh.profiles:ssh: { lastConnection: SshLastConnection | null; profiles: SshConnectionProfile[]; lastActiveContextId: string; // Which context was active when app closed } -
Add defaults in the default config:
profiles: [],lastActiveContextId: 'local' -
Add profile management methods to ConfigManager:
addSshProfile(profile: SshConnectionProfile): void— adds to profiles array, savesremoveSshProfile(profileId: string): void— removes by id, savesupdateSshProfile(profileId: string, updates: Partial<SshConnectionProfile>): void— updates, savesgetSshProfiles(): SshConnectionProfile[]— returns profiles arraysetLastActiveContextId(contextId: string): void— persists for restore on restart
-
Ensure the config migration handles existing configs that don't have
profilesorlastActiveContextIdfields (add them with defaults in the config loading/merge logic). -
Run
pnpm typecheck— must pass with zero errors -
Run
pnpm test— all existing tests pass (especially config-related tests) -
Verify preload/index.ts has
contextnamespace with list, getActive, switch, onChanged methods -
Verify ConfigManager has ssh.profiles and ssh.lastActiveContextId with defaults
-
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.
<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>