From b93b6b0e4de3f7fa5c3c4d928ac1929492ec01d1 Mon Sep 17 00:00:00 2001 From: ParsaKhaz Date: Thu, 14 May 2026 12:52:37 -0700 Subject: [PATCH] feat: route renderer daemon commands through bridge --- frontend/src/types/electron.d.ts | 5 +- main/src/ipc/daemon.test.ts | 61 ++++++ main/src/ipc/daemon.ts | 23 ++ main/src/ipc/index.ts | 2 + main/src/preload.ts | 355 ++++++++++++++++--------------- 5 files changed, 272 insertions(+), 174 deletions(-) create mode 100644 main/src/ipc/daemon.test.ts create mode 100644 main/src/ipc/daemon.ts diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index 068680c..11f81d0 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -39,7 +39,8 @@ interface IPCResponse { } interface ElectronAPI { - // Generic invoke method for direct IPC calls + // Generic invoke method. Daemon-owned channels route through the main-process + // daemon bridge while adapter-only channels stay on direct Electron IPC. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic IPC bridge that returns different types based on channel invoke: (channel: string, ...args: unknown[]) => Promise; @@ -460,6 +461,8 @@ interface CloudVmState { // Additional electron interface for IPC event listeners interface ElectronInterface { openExternal: (url: string) => Promise; + // Generic invoke method. Daemon-owned channels route through the main-process + // daemon bridge while adapter-only channels stay on direct Electron IPC. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic IPC bridge that returns different types based on channel invoke: (channel: string, ...args: unknown[]) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic IPC event callback that receives different argument types diff --git a/main/src/ipc/daemon.test.ts b/main/src/ipc/daemon.test.ts new file mode 100644 index 0000000..4504ee8 --- /dev/null +++ b/main/src/ipc/daemon.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from 'vitest'; +import { PaneCommandRegistry } from '../daemon/commandRegistry'; +import { registerDaemonBridgeHandlers } from './daemon'; + +interface IpcMainStub { + handlers: Map Promise>; + handle(channel: string, listener: (_event: unknown, ...args: unknown[]) => Promise): void; +} + +function createIpcMainStub(): IpcMainStub { + const handlers = new Map Promise>(); + + return { + handlers, + handle(channel, listener) { + handlers.set(channel, listener); + }, + }; +} + +describe('daemon IPC bridge', () => { + it('forwards daemon-owned channels into the shared command registry', async () => { + const registry = new PaneCommandRegistry(); + const ipcMain = createIpcMainStub(); + const handler = vi.fn(async (sessionId: string) => ({ success: true, data: sessionId })); + + registry.register('sessions:get', handler); + registerDaemonBridgeHandlers(ipcMain, registry); + + const bridge = ipcMain.handlers.get('daemon:invoke'); + expect(bridge).toBeDefined(); + + await expect(bridge?.({}, 'sessions:get', 'session-1')).resolves.toEqual({ + success: true, + data: 'session-1', + }); + expect(handler).toHaveBeenCalledWith('session-1'); + }); + + it('rejects adapter-only channels at the bridge boundary', async () => { + const registry = new PaneCommandRegistry(); + const ipcMain = createIpcMainStub(); + + registerDaemonBridgeHandlers(ipcMain, registry); + + const bridge = ipcMain.handlers.get('daemon:invoke'); + await expect(bridge?.({}, 'sessions:open-ide', 'session-1')).rejects.toThrow( + 'Channel "sessions:open-ide" is not daemon-owned', + ); + }); + + it('rejects malformed bridge requests before reaching the registry', async () => { + const registry = new PaneCommandRegistry(); + const ipcMain = createIpcMainStub(); + + registerDaemonBridgeHandlers(ipcMain, registry); + + const bridge = ipcMain.handlers.get('daemon:invoke'); + await expect(bridge?.({}, 123)).rejects.toThrow('Pane daemon bridge requires a string channel'); + }); +}); diff --git a/main/src/ipc/daemon.ts b/main/src/ipc/daemon.ts new file mode 100644 index 0000000..d9e7f7d --- /dev/null +++ b/main/src/ipc/daemon.ts @@ -0,0 +1,23 @@ +import { isDaemonOwnedChannel } from '../../../shared/types/daemon'; +import type { PaneCommandRegistry } from '../daemon/commandRegistry'; + +interface IpcMainHandleLike { + handle(channel: string, listener: (_event: unknown, ...args: unknown[]) => Promise): void; +} + +export function registerDaemonBridgeHandlers( + ipcMain: IpcMainHandleLike, + commandRegistry: PaneCommandRegistry, +): void { + ipcMain.handle('daemon:invoke', async (_event, channel: unknown, ...args: unknown[]) => { + if (typeof channel !== 'string') { + throw new Error('Pane daemon bridge requires a string channel'); + } + + if (!isDaemonOwnedChannel(channel)) { + throw new Error(`Channel "${channel}" is not daemon-owned`); + } + + return commandRegistry.invoke(channel, args); + }); +} diff --git a/main/src/ipc/index.ts b/main/src/ipc/index.ts index 6868f03..f66ae19 100644 --- a/main/src/ipc/index.ts +++ b/main/src/ipc/index.ts @@ -22,6 +22,7 @@ import { registerCloudHandlers } from './cloud'; import { registerClipboardHandlers } from './clipboard'; import { registerResourceMonitorHandlers } from './resourceMonitor'; import { registerOnboardingHandlers } from './onboarding'; +import { registerDaemonBridgeHandlers } from './daemon'; import { PaneCommandRegistry } from '../daemon/commandRegistry'; @@ -50,6 +51,7 @@ export function registerIpcHandlers(services: AppServices): PaneCommandRegistry registerClipboardHandlers(ipcMain, services); registerResourceMonitorHandlers(ipcMain, services, commandRegistry); registerOnboardingHandlers(ipcMain, services); + registerDaemonBridgeHandlers(ipcMain, commandRegistry); return commandRegistry; } diff --git a/main/src/preload.ts b/main/src/preload.ts index c3d3f31..a7198ab 100644 --- a/main/src/preload.ts +++ b/main/src/preload.ts @@ -3,6 +3,7 @@ import type { CreateSessionRequest, Session } from './types/session'; import type { AppConfig, UpdateConfigRequest } from './types/config'; import type { CreateProjectRequest, UpdateProjectRequest, Project } from '../../frontend/src/types/project'; import type { ToolPanel } from '../../shared/types/panels'; +import { isDaemonOwnedChannel } from '../../shared/types/daemon'; interface LogEntry { timestamp: string; @@ -271,7 +272,7 @@ if (process.env.NODE_ENV !== 'production') { // Send to main process for file logging try { - ipcRenderer.invoke('console:log', { + invokeIpc('console:log', { level, args: args.map(arg => { if (typeof arg === 'object') { @@ -301,28 +302,36 @@ interface IPCResponse { error?: string; } +function invokeIpc(channel: string, ...args: unknown[]) { + if (isDaemonOwnedChannel(channel)) { + return ipcRenderer.invoke('daemon:invoke', channel, ...args); + } + + return ipcRenderer.invoke(channel, ...args); +} + contextBridge.exposeInMainWorld('electronAPI', { // Generic invoke method for direct IPC calls - invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args), + invoke: (channel: string, ...args: unknown[]) => invokeIpc(channel, ...args), // Basic app info - getAppVersion: () => ipcRenderer.invoke('get-app-version'), - getPlatform: () => ipcRenderer.invoke('get-platform'), - isPackaged: () => ipcRenderer.invoke('is-packaged'), + getAppVersion: () => invokeIpc('get-app-version'), + getPlatform: () => invokeIpc('get-platform'), + isPackaged: () => invokeIpc('is-packaged'), // Version checking - checkForUpdates: (): Promise => ipcRenderer.invoke('version:check-for-updates'), - getVersionInfo: (): Promise => ipcRenderer.invoke('version:get-info'), + checkForUpdates: (): Promise => invokeIpc('version:check-for-updates'), + getVersionInfo: (): Promise => invokeIpc('version:get-info'), // Auto-updater updater: { - checkAndDownload: (): Promise => ipcRenderer.invoke('updater:check-and-download'), - downloadUpdate: (): Promise => ipcRenderer.invoke('updater:download-update'), - installUpdate: (): Promise => ipcRenderer.invoke('updater:install-update'), + checkAndDownload: (): Promise => invokeIpc('updater:check-and-download'), + downloadUpdate: (): Promise => invokeIpc('updater:download-update'), + installUpdate: (): Promise => invokeIpc('updater:install-update'), }, // System utilities - openExternal: (url: string): Promise => ipcRenderer.invoke('openExternal', url), + openExternal: (url: string): Promise => invokeIpc('openExternal', url), diagnostics: { rendererFatal: (payload: { @@ -333,121 +342,121 @@ contextBridge.exposeInMainWorld('electronAPI', { url?: string; line?: number; column?: number; - }): Promise => ipcRenderer.invoke('diagnostics:renderer-fatal', payload), + }): Promise => invokeIpc('diagnostics:renderer-fatal', payload), }, // Session management sessions: { - getAll: (): Promise => ipcRenderer.invoke('sessions:get-all'), - getAllWithProjects: (): Promise => ipcRenderer.invoke('sessions:get-all-with-projects'), - getArchivedWithProjects: (): Promise => ipcRenderer.invoke('sessions:get-archived-with-projects'), - restore: (sessionId: string): Promise => ipcRenderer.invoke('sessions:restore', sessionId), - get: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get', sessionId), - create: (request: CreateSessionRequest): Promise => ipcRenderer.invoke('sessions:create', request), - delete: (sessionId: string): Promise => ipcRenderer.invoke('sessions:delete', sessionId), - sendInput: (sessionId: string, input: string): Promise => ipcRenderer.invoke('sessions:input', sessionId, input), - continue: (sessionId: string, prompt?: string, model?: string): Promise => ipcRenderer.invoke('sessions:continue', sessionId, prompt, model), - getOutput: (sessionId: string, limit?: number): Promise => ipcRenderer.invoke('sessions:get-output', sessionId, limit), - getJsonMessages: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-json-messages', sessionId), - getStatistics: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-statistics', sessionId), - getConversation: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-conversation', sessionId), - getConversationMessages: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-conversation-messages', sessionId), - getConversationMessageCount: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-conversation-message-count', sessionId), - generateCompactedContext: (sessionId: string): Promise => ipcRenderer.invoke('sessions:generate-compacted-context', sessionId), - markViewed: (sessionId: string): Promise => ipcRenderer.invoke('sessions:mark-viewed', sessionId), - stop: (sessionId: string): Promise => ipcRenderer.invoke('sessions:stop', sessionId), + getAll: (): Promise => invokeIpc('sessions:get-all'), + getAllWithProjects: (): Promise => invokeIpc('sessions:get-all-with-projects'), + getArchivedWithProjects: (): Promise => invokeIpc('sessions:get-archived-with-projects'), + restore: (sessionId: string): Promise => invokeIpc('sessions:restore', sessionId), + get: (sessionId: string): Promise => invokeIpc('sessions:get', sessionId), + create: (request: CreateSessionRequest): Promise => invokeIpc('sessions:create', request), + delete: (sessionId: string): Promise => invokeIpc('sessions:delete', sessionId), + sendInput: (sessionId: string, input: string): Promise => invokeIpc('sessions:input', sessionId, input), + continue: (sessionId: string, prompt?: string, model?: string): Promise => invokeIpc('sessions:continue', sessionId, prompt, model), + getOutput: (sessionId: string, limit?: number): Promise => invokeIpc('sessions:get-output', sessionId, limit), + getJsonMessages: (sessionId: string): Promise => invokeIpc('sessions:get-json-messages', sessionId), + getStatistics: (sessionId: string): Promise => invokeIpc('sessions:get-statistics', sessionId), + getConversation: (sessionId: string): Promise => invokeIpc('sessions:get-conversation', sessionId), + getConversationMessages: (sessionId: string): Promise => invokeIpc('sessions:get-conversation-messages', sessionId), + getConversationMessageCount: (sessionId: string): Promise => invokeIpc('sessions:get-conversation-message-count', sessionId), + generateCompactedContext: (sessionId: string): Promise => invokeIpc('sessions:generate-compacted-context', sessionId), + markViewed: (sessionId: string): Promise => invokeIpc('sessions:mark-viewed', sessionId), + stop: (sessionId: string): Promise => invokeIpc('sessions:stop', sessionId), // Execution and Git operations - getExecutions: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-executions', sessionId), - getExecutionDiff: (sessionId: string, executionId: string): Promise => ipcRenderer.invoke('sessions:get-execution-diff', sessionId, executionId), - gitCommit: (sessionId: string, message: string): Promise => ipcRenderer.invoke('sessions:git-commit', sessionId, message), - gitDiff: (sessionId: string): Promise => ipcRenderer.invoke('sessions:git-diff', sessionId), - getCombinedDiff: (sessionId: string, executionIds?: number[]): Promise => ipcRenderer.invoke('sessions:get-combined-diff', sessionId, executionIds), - getCommitDiffByHash: (sessionId: string, commitHash: string): Promise => ipcRenderer.invoke('sessions:get-commit-diff-by-hash', sessionId, commitHash), + getExecutions: (sessionId: string): Promise => invokeIpc('sessions:get-executions', sessionId), + getExecutionDiff: (sessionId: string, executionId: string): Promise => invokeIpc('sessions:get-execution-diff', sessionId, executionId), + gitCommit: (sessionId: string, message: string): Promise => invokeIpc('sessions:git-commit', sessionId, message), + gitDiff: (sessionId: string): Promise => invokeIpc('sessions:git-diff', sessionId), + getCombinedDiff: (sessionId: string, executionIds?: number[]): Promise => invokeIpc('sessions:get-combined-diff', sessionId, executionIds), + getCommitDiffByHash: (sessionId: string, commitHash: string): Promise => invokeIpc('sessions:get-commit-diff-by-hash', sessionId, commitHash), // Main repo session - getOrCreateMainRepoSession: (projectId: number): Promise => ipcRenderer.invoke('sessions:get-or-create-main-repo', projectId), + getOrCreateMainRepoSession: (projectId: number): Promise => invokeIpc('sessions:get-or-create-main-repo', projectId), // Script operations - hasRunScript: (sessionId: string): Promise => ipcRenderer.invoke('sessions:has-run-script', sessionId), - getRunningSession: (): Promise => ipcRenderer.invoke('sessions:get-running-session'), - runScript: (sessionId: string): Promise => ipcRenderer.invoke('sessions:run-script', sessionId), - stopScript: (sessionId?: string): Promise => ipcRenderer.invoke('sessions:stop-script', sessionId), - runTerminalCommand: (sessionId: string, command: string): Promise => ipcRenderer.invoke('sessions:run-terminal-command', sessionId, command), - sendTerminalInput: (sessionId: string, data: string): Promise => ipcRenderer.invoke('sessions:send-terminal-input', sessionId, data), - preCreateTerminal: (sessionId: string): Promise => ipcRenderer.invoke('sessions:pre-create-terminal', sessionId), - resizeTerminal: (sessionId: string, cols: number, rows: number): Promise => ipcRenderer.invoke('sessions:resize-terminal', sessionId, cols, rows), + hasRunScript: (sessionId: string): Promise => invokeIpc('sessions:has-run-script', sessionId), + getRunningSession: (): Promise => invokeIpc('sessions:get-running-session'), + runScript: (sessionId: string): Promise => invokeIpc('sessions:run-script', sessionId), + stopScript: (sessionId?: string): Promise => invokeIpc('sessions:stop-script', sessionId), + runTerminalCommand: (sessionId: string, command: string): Promise => invokeIpc('sessions:run-terminal-command', sessionId, command), + sendTerminalInput: (sessionId: string, data: string): Promise => invokeIpc('sessions:send-terminal-input', sessionId, data), + preCreateTerminal: (sessionId: string): Promise => invokeIpc('sessions:pre-create-terminal', sessionId), + resizeTerminal: (sessionId: string, cols: number, rows: number): Promise => invokeIpc('sessions:resize-terminal', sessionId, cols, rows), // Prompt operations - getPrompts: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-prompts', sessionId), + getPrompts: (sessionId: string): Promise => invokeIpc('sessions:get-prompts', sessionId), // Git rebase operations - rebaseMainIntoWorktree: (sessionId: string): Promise => ipcRenderer.invoke('sessions:rebase-main-into-worktree', sessionId), - abortRebaseAndUseClaude: (sessionId: string): Promise => ipcRenderer.invoke('sessions:abort-rebase-and-use-claude', sessionId), - squashAndRebaseToMain: (sessionId: string, commitMessage: string): Promise => ipcRenderer.invoke('sessions:squash-and-rebase-to-main', sessionId, commitMessage), - rebaseToMain: (sessionId: string): Promise => ipcRenderer.invoke('sessions:rebase-to-main', sessionId), + rebaseMainIntoWorktree: (sessionId: string): Promise => invokeIpc('sessions:rebase-main-into-worktree', sessionId), + abortRebaseAndUseClaude: (sessionId: string): Promise => invokeIpc('sessions:abort-rebase-and-use-claude', sessionId), + squashAndRebaseToMain: (sessionId: string, commitMessage: string): Promise => invokeIpc('sessions:squash-and-rebase-to-main', sessionId, commitMessage), + rebaseToMain: (sessionId: string): Promise => invokeIpc('sessions:rebase-to-main', sessionId), // Git pull/push operations - gitPull: (sessionId: string): Promise => ipcRenderer.invoke('sessions:git-pull', sessionId), - gitPush: (sessionId: string): Promise => ipcRenderer.invoke('sessions:git-push', sessionId), - gitFetch: (sessionId: string): Promise => ipcRenderer.invoke('sessions:git-fetch', sessionId), - gitStash: (sessionId: string, message?: string): Promise => ipcRenderer.invoke('sessions:git-stash', sessionId, message), - gitStashPop: (sessionId: string): Promise => ipcRenderer.invoke('sessions:git-stash-pop', sessionId), - gitSoftReset: (sessionId: string): Promise => ipcRenderer.invoke('sessions:git-soft-reset', sessionId), - gitStageAndCommit: (sessionId: string, message: string): Promise => ipcRenderer.invoke('sessions:git-stage-and-commit', sessionId, message), - hasStash: (sessionId: string): Promise => ipcRenderer.invoke('sessions:has-stash', sessionId), - setUpstream: (sessionId: string, remoteBranch: string): Promise => ipcRenderer.invoke('sessions:set-upstream', sessionId, remoteBranch), - getUpstream: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-upstream', sessionId), - getRemoteBranches: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-remote-branches', sessionId), - getGitStatus: (sessionId: string, nonBlocking?: boolean, isInitialLoad?: boolean): Promise => ipcRenderer.invoke('sessions:get-git-status', sessionId, nonBlocking, isInitialLoad), - getLastCommits: (sessionId: string, count: number): Promise => ipcRenderer.invoke('sessions:get-last-commits', sessionId, count), - getGitGraph: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-git-graph', sessionId), + gitPull: (sessionId: string): Promise => invokeIpc('sessions:git-pull', sessionId), + gitPush: (sessionId: string): Promise => invokeIpc('sessions:git-push', sessionId), + gitFetch: (sessionId: string): Promise => invokeIpc('sessions:git-fetch', sessionId), + gitStash: (sessionId: string, message?: string): Promise => invokeIpc('sessions:git-stash', sessionId, message), + gitStashPop: (sessionId: string): Promise => invokeIpc('sessions:git-stash-pop', sessionId), + gitSoftReset: (sessionId: string): Promise => invokeIpc('sessions:git-soft-reset', sessionId), + gitStageAndCommit: (sessionId: string, message: string): Promise => invokeIpc('sessions:git-stage-and-commit', sessionId, message), + hasStash: (sessionId: string): Promise => invokeIpc('sessions:has-stash', sessionId), + setUpstream: (sessionId: string, remoteBranch: string): Promise => invokeIpc('sessions:set-upstream', sessionId, remoteBranch), + getUpstream: (sessionId: string): Promise => invokeIpc('sessions:get-upstream', sessionId), + getRemoteBranches: (sessionId: string): Promise => invokeIpc('sessions:get-remote-branches', sessionId), + getGitStatus: (sessionId: string, nonBlocking?: boolean, isInitialLoad?: boolean): Promise => invokeIpc('sessions:get-git-status', sessionId, nonBlocking, isInitialLoad), + getLastCommits: (sessionId: string, count: number): Promise => invokeIpc('sessions:get-last-commits', sessionId, count), + getGitGraph: (sessionId: string): Promise => invokeIpc('sessions:get-git-graph', sessionId), // Git operation helpers - hasChangesToRebase: (sessionId: string): Promise => ipcRenderer.invoke('sessions:has-changes-to-rebase', sessionId), - getGitCommands: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-git-commands', sessionId), - generateName: (prompt: string): Promise => ipcRenderer.invoke('sessions:generate-name', prompt), - rename: (sessionId: string, newName: string): Promise => ipcRenderer.invoke('sessions:rename', sessionId, newName), - toggleFavorite: (sessionId: string): Promise => ipcRenderer.invoke('sessions:toggle-favorite', sessionId), + hasChangesToRebase: (sessionId: string): Promise => invokeIpc('sessions:has-changes-to-rebase', sessionId), + getGitCommands: (sessionId: string): Promise => invokeIpc('sessions:get-git-commands', sessionId), + generateName: (prompt: string): Promise => invokeIpc('sessions:generate-name', prompt), + rename: (sessionId: string, newName: string): Promise => invokeIpc('sessions:rename', sessionId, newName), + toggleFavorite: (sessionId: string): Promise => invokeIpc('sessions:toggle-favorite', sessionId), // Resume session operations - getResumable: (): Promise => ipcRenderer.invoke('sessions:get-resumable'), - resumeInterrupted: (sessionIds: string[]): Promise => ipcRenderer.invoke('sessions:resume-interrupted', sessionIds), - dismissInterrupted: (sessionIds: string[]): Promise => ipcRenderer.invoke('sessions:dismiss-interrupted', sessionIds), + getResumable: (): Promise => invokeIpc('sessions:get-resumable'), + resumeInterrupted: (sessionIds: string[]): Promise => invokeIpc('sessions:resume-interrupted', sessionIds), + dismissInterrupted: (sessionIds: string[]): Promise => invokeIpc('sessions:dismiss-interrupted', sessionIds), // IDE operations - openIDE: (sessionId: string, ideKey?: string): Promise => ipcRenderer.invoke('sessions:open-ide', sessionId, ideKey), + openIDE: (sessionId: string, ideKey?: string): Promise => invokeIpc('sessions:open-ide', sessionId, ideKey), // Reorder operations - reorder: (sessionOrders: Array<{ id: string; displayOrder: number }>): Promise => ipcRenderer.invoke('sessions:reorder', sessionOrders), + reorder: (sessionOrders: Array<{ id: string; displayOrder: number }>): Promise => invokeIpc('sessions:reorder', sessionOrders), // Image operations - saveImages: (sessionId: string, images: Array<{ name: string; dataUrl: string; type: string }>): Promise => ipcRenderer.invoke('sessions:save-images', sessionId, images), + saveImages: (sessionId: string, images: Array<{ name: string; dataUrl: string; type: string }>): Promise => invokeIpc('sessions:save-images', sessionId, images), // Text file operations - saveLargeText: (sessionId: string, text: string): Promise => ipcRenderer.invoke('sessions:save-large-text', sessionId, text), + saveLargeText: (sessionId: string, text: string): Promise => invokeIpc('sessions:save-large-text', sessionId, text), // Log operations - getLogs: (sessionId: string): Promise => ipcRenderer.invoke('sessions:get-logs', sessionId), - clearLogs: (sessionId: string): Promise => ipcRenderer.invoke('sessions:clear-logs', sessionId), - addLog: (sessionId: string, entry: LogEntry): Promise => ipcRenderer.invoke('sessions:add-log', sessionId, entry), + getLogs: (sessionId: string): Promise => invokeIpc('sessions:get-logs', sessionId), + clearLogs: (sessionId: string): Promise => invokeIpc('sessions:clear-logs', sessionId), + addLog: (sessionId: string, entry: LogEntry): Promise => invokeIpc('sessions:add-log', sessionId, entry), }, // Project management projects: { - getAll: (): Promise => ipcRenderer.invoke('projects:get-all'), - getActive: (): Promise => ipcRenderer.invoke('projects:get-active'), - create: (projectData: CreateProjectRequest): Promise => ipcRenderer.invoke('projects:create', projectData), - activate: (projectId: string): Promise => ipcRenderer.invoke('projects:activate', projectId), - update: (projectId: string, updates: UpdateProjectRequest): Promise => ipcRenderer.invoke('projects:update', projectId, updates), - delete: (projectId: string): Promise => ipcRenderer.invoke('projects:delete', projectId), - detectBranch: (path: string): Promise => ipcRenderer.invoke('projects:detect-branch', path), - reorder: (projectOrders: Array<{ id: number; displayOrder: number }>): Promise => ipcRenderer.invoke('projects:reorder', projectOrders), - listBranches: (projectId: string): Promise => ipcRenderer.invoke('projects:list-branches', projectId), - refreshGitStatus: (projectId: number): Promise => ipcRenderer.invoke('projects:refresh-git-status', projectId), - runScript: (projectId: number): Promise => ipcRenderer.invoke('projects:run-script', projectId), - getRunningScript: (): Promise => ipcRenderer.invoke('projects:get-running-script'), - stopScript: (projectId?: number): Promise => ipcRenderer.invoke('projects:stop-script', projectId), + getAll: (): Promise => invokeIpc('projects:get-all'), + getActive: (): Promise => invokeIpc('projects:get-active'), + create: (projectData: CreateProjectRequest): Promise => invokeIpc('projects:create', projectData), + activate: (projectId: string): Promise => invokeIpc('projects:activate', projectId), + update: (projectId: string, updates: UpdateProjectRequest): Promise => invokeIpc('projects:update', projectId, updates), + delete: (projectId: string): Promise => invokeIpc('projects:delete', projectId), + detectBranch: (path: string): Promise => invokeIpc('projects:detect-branch', path), + reorder: (projectOrders: Array<{ id: number; displayOrder: number }>): Promise => invokeIpc('projects:reorder', projectOrders), + listBranches: (projectId: string): Promise => invokeIpc('projects:list-branches', projectId), + refreshGitStatus: (projectId: number): Promise => invokeIpc('projects:refresh-git-status', projectId), + runScript: (projectId: number): Promise => invokeIpc('projects:run-script', projectId), + getRunningScript: (): Promise => invokeIpc('projects:get-running-script'), + stopScript: (projectId?: number): Promise => invokeIpc('projects:stop-script', projectId), /** * Detects the project's config file (pane.json, conductor.json, .gitpod.yml, or * devcontainer.json) and returns a `DetectedProjectConfig` with `setup`, `run`, @@ -455,7 +464,7 @@ contextBridge.exposeInMainWorld('electronAPI', { * Used by ProjectSettings to show "From " badges on script fields. * Reads from the project's main working directory, not a session worktree. */ - detectConfig: (projectId: string): Promise => ipcRenderer.invoke('projects:detect-config', projectId), + detectConfig: (projectId: string): Promise => invokeIpc('projects:detect-config', projectId), /** * Resolves which run script should execute for a specific session. * Checks (in order): DB `project.run_script`, then config-file detection from the @@ -464,78 +473,78 @@ contextBridge.exposeInMainWorld('electronAPI', { * Returns `{ command, source }` or null if nothing is configured. * Used by `PanelTabBar` to start/stop the dev server for a session. */ - resolveRunScript: (sessionId: string): Promise => ipcRenderer.invoke('projects:resolve-run-script', sessionId), + resolveRunScript: (sessionId: string): Promise => invokeIpc('projects:resolve-run-script', sessionId), }, // Git operations git: { - detectBranch: (path: string): Promise> => ipcRenderer.invoke('projects:detect-branch', path), - cancelStatusForProject: (projectId: number): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('git:cancel-status-for-project', projectId), - executeProject: (projectId: number, args: string[]): Promise => ipcRenderer.invoke('git:execute-project', { projectId, args }), - cloneRepo: (url: string, destDir: string): Promise => ipcRenderer.invoke('git:clone-repo', url, destDir), + detectBranch: (path: string): Promise> => invokeIpc('projects:detect-branch', path), + cancelStatusForProject: (projectId: number): Promise<{ success: boolean; error?: string }> => invokeIpc('git:cancel-status-for-project', projectId), + executeProject: (projectId: number, args: string[]): Promise => invokeIpc('git:execute-project', { projectId, args }), + cloneRepo: (url: string, destDir: string): Promise => invokeIpc('git:clone-repo', url, destDir), }, // Folders folders: { - getByProject: (projectId: number): Promise => ipcRenderer.invoke('folders:get-by-project', projectId), - create: (name: string, projectId: number, parentFolderId?: string | null): Promise => ipcRenderer.invoke('folders:create', name, projectId, parentFolderId), - update: (folderId: string, updates: { name?: string; display_order?: number; parent_folder_id?: string | null }): Promise => ipcRenderer.invoke('folders:update', folderId, updates), - delete: (folderId: string): Promise => ipcRenderer.invoke('folders:delete', folderId), - reorder: (projectId: number, folderOrders: Array<{ id: string; displayOrder: number }>): Promise => ipcRenderer.invoke('folders:reorder', projectId, folderOrders), - moveSession: (sessionId: string, folderId: string | null): Promise => ipcRenderer.invoke('folders:move-session', sessionId, folderId), - move: (folderId: string, parentFolderId: string | null): Promise => ipcRenderer.invoke('folders:move', folderId, parentFolderId), + getByProject: (projectId: number): Promise => invokeIpc('folders:get-by-project', projectId), + create: (name: string, projectId: number, parentFolderId?: string | null): Promise => invokeIpc('folders:create', name, projectId, parentFolderId), + update: (folderId: string, updates: { name?: string; display_order?: number; parent_folder_id?: string | null }): Promise => invokeIpc('folders:update', folderId, updates), + delete: (folderId: string): Promise => invokeIpc('folders:delete', folderId), + reorder: (projectId: number, folderOrders: Array<{ id: string; displayOrder: number }>): Promise => invokeIpc('folders:reorder', projectId, folderOrders), + moveSession: (sessionId: string, folderId: string | null): Promise => invokeIpc('folders:move-session', sessionId, folderId), + move: (folderId: string, parentFolderId: string | null): Promise => invokeIpc('folders:move', folderId, parentFolderId), }, // Configuration config: { - get: (): Promise => ipcRenderer.invoke('config:get'), - update: (updates: UpdateConfigRequest): Promise => ipcRenderer.invoke('config:update', updates), - getSessionPreferences: (): Promise => ipcRenderer.invoke('config:get-session-preferences'), - updateSessionPreferences: (preferences: AppConfig['sessionCreationPreferences']): Promise => ipcRenderer.invoke('config:update-session-preferences', preferences), - getAvailableShells: (): Promise => ipcRenderer.invoke('config:get-available-shells'), - getMonospaceFonts: (): Promise => ipcRenderer.invoke('config:get-monospace-fonts'), + get: (): Promise => invokeIpc('config:get'), + update: (updates: UpdateConfigRequest): Promise => invokeIpc('config:update', updates), + getSessionPreferences: (): Promise => invokeIpc('config:get-session-preferences'), + updateSessionPreferences: (preferences: AppConfig['sessionCreationPreferences']): Promise => invokeIpc('config:update-session-preferences', preferences), + getAvailableShells: (): Promise => invokeIpc('config:get-available-shells'), + getMonospaceFonts: (): Promise => invokeIpc('config:get-monospace-fonts'), }, // Prompts prompts: { - getAll: (): Promise => ipcRenderer.invoke('prompts:get-all'), - getByPromptId: (promptId: string): Promise => ipcRenderer.invoke('prompts:get-by-id', promptId), + getAll: (): Promise => invokeIpc('prompts:get-all'), + getByPromptId: (promptId: string): Promise => invokeIpc('prompts:get-by-id', promptId), }, // File operations file: { - listProject: (projectId: number, path?: string): Promise => ipcRenderer.invoke('file:list-project', { projectId, path }), - readProject: (projectId: number, filePath: string): Promise => ipcRenderer.invoke('file:read-project', { projectId, filePath }), - writeProject: (projectId: number, filePath: string, content: string): Promise => ipcRenderer.invoke('file:write-project', { projectId, filePath, content }), + listProject: (projectId: number, path?: string): Promise => invokeIpc('file:list-project', { projectId, path }), + readProject: (projectId: number, filePath: string): Promise => invokeIpc('file:read-project', { projectId, filePath }), + writeProject: (projectId: number, filePath: string, content: string): Promise => invokeIpc('file:write-project', { projectId, filePath, content }), }, // Dialog dialog: { - openFile: (options?: DialogOptions): Promise> => ipcRenderer.invoke('dialog:open-file', options), - openDirectory: (options?: DialogOptions): Promise> => ipcRenderer.invoke('dialog:open-directory', options), + openFile: (options?: DialogOptions): Promise> => invokeIpc('dialog:open-file', options), + openDirectory: (options?: DialogOptions): Promise> => invokeIpc('dialog:open-directory', options), }, // Permissions permissions: { - respond: (requestId: string, response: boolean | { approved: boolean; remember?: boolean }): Promise => ipcRenderer.invoke('permission:respond', requestId, response), - getPending: (): Promise => ipcRenderer.invoke('permission:getPending'), + respond: (requestId: string, response: boolean | { approved: boolean; remember?: boolean }): Promise => invokeIpc('permission:respond', requestId, response), + getPending: (): Promise => invokeIpc('permission:getPending'), }, // Stravu OAuth integration stravu: { - getConnectionStatus: (): Promise => ipcRenderer.invoke('stravu:get-connection-status'), - initiateAuth: (): Promise => ipcRenderer.invoke('stravu:initiate-auth'), - checkAuthStatus: (sessionId: string): Promise => ipcRenderer.invoke('stravu:check-auth-status', sessionId), - disconnect: (): Promise => ipcRenderer.invoke('stravu:disconnect'), - getNotebooks: (): Promise => ipcRenderer.invoke('stravu:get-notebooks'), - getNotebook: (notebookId: string): Promise => ipcRenderer.invoke('stravu:get-notebook', notebookId), - searchNotebooks: (query: string, limit?: number): Promise => ipcRenderer.invoke('stravu:search-notebooks', query, limit), + getConnectionStatus: (): Promise => invokeIpc('stravu:get-connection-status'), + initiateAuth: (): Promise => invokeIpc('stravu:initiate-auth'), + checkAuthStatus: (sessionId: string): Promise => invokeIpc('stravu:check-auth-status', sessionId), + disconnect: (): Promise => invokeIpc('stravu:disconnect'), + getNotebooks: (): Promise => invokeIpc('stravu:get-notebooks'), + getNotebook: (notebookId: string): Promise => invokeIpc('stravu:get-notebook', notebookId), + searchNotebooks: (query: string, limit?: number): Promise => invokeIpc('stravu:search-notebooks', query, limit), }, // Dashboard dashboard: { - getProjectStatus: (projectId: number): Promise => ipcRenderer.invoke('dashboard:get-project-status', projectId), - getProjectStatusProgressive: (projectId: number): Promise => ipcRenderer.invoke('dashboard:get-project-status-progressive', projectId), + getProjectStatus: (projectId: number): Promise => invokeIpc('dashboard:get-project-status', projectId), + getProjectStatusProgressive: (projectId: number): Promise => invokeIpc('dashboard:get-project-status-progressive', projectId), onUpdate: (callback: (data: DashboardUpdateData) => void) => { const subscription = (_event: Electron.IpcRendererEvent, data: DashboardUpdateData) => callback(data); ipcRenderer.on('dashboard:update', subscription); @@ -550,18 +559,18 @@ contextBridge.exposeInMainWorld('electronAPI', { // Onboarding onboarding: { - detectEnvironment: (): Promise => ipcRenderer.invoke('onboarding:detect-environment'), - setupDefaultRepo: (): Promise => ipcRenderer.invoke('onboarding:setup-default-repo'), - starRepo: (): Promise => ipcRenderer.invoke('onboarding:star-repo'), + detectEnvironment: (): Promise => invokeIpc('onboarding:detect-environment'), + setupDefaultRepo: (): Promise => invokeIpc('onboarding:setup-default-repo'), + starRepo: (): Promise => invokeIpc('onboarding:star-repo'), }, // UI State management uiState: { - getExpanded: (): Promise => ipcRenderer.invoke('ui-state:get-expanded'), - saveExpanded: (projectIds: number[], folderIds: string[]): Promise => ipcRenderer.invoke('ui-state:save-expanded', projectIds, folderIds), - saveExpandedProjects: (projectIds: number[]): Promise => ipcRenderer.invoke('ui-state:save-expanded-projects', projectIds), - saveExpandedFolders: (folderIds: string[]): Promise => ipcRenderer.invoke('ui-state:save-expanded-folders', folderIds), - saveSessionSortAscending: (ascending: boolean): Promise => ipcRenderer.invoke('ui-state:save-session-sort-ascending', ascending), + getExpanded: (): Promise => invokeIpc('ui-state:get-expanded'), + saveExpanded: (projectIds: number[], folderIds: string[]): Promise => invokeIpc('ui-state:save-expanded', projectIds, folderIds), + saveExpandedProjects: (projectIds: number[]): Promise => invokeIpc('ui-state:save-expanded-projects', projectIds), + saveExpandedFolders: (folderIds: string[]): Promise => invokeIpc('ui-state:save-expanded-folders', folderIds), + saveSessionSortAscending: (ascending: boolean): Promise => invokeIpc('ui-state:save-session-sort-ascending', ascending), }, // Event listeners for real-time updates @@ -806,42 +815,42 @@ contextBridge.exposeInMainWorld('electronAPI', { // Panels API for Claude panels and other panel types panels: { createPanel: (sessionId: string, type: string, name: string, config?: Record): Promise => - ipcRenderer.invoke('panels:create', { sessionId, type, title: name, initialState: config }), - getSessionPanels: (sessionId: string): Promise => ipcRenderer.invoke('panels:list', sessionId), - deletePanel: (panelId: string): Promise => ipcRenderer.invoke('panels:delete', panelId), - renamePanel: (panelId: string, name: string): Promise => ipcRenderer.invoke('panels:update', panelId, { name }), - setActivePanel: (sessionId: string, panelId: string): Promise => ipcRenderer.invoke('panels:set-active', sessionId, panelId), - resizeTerminal: (panelId: string, cols: number, rows: number): Promise => ipcRenderer.invoke('panels:resize-terminal', panelId, cols, rows), - sendTerminalInput: (panelId: string, data: string): Promise => ipcRenderer.invoke('panels:send-terminal-input', panelId, data), - getOutput: (panelId: string, limit?: number): Promise => ipcRenderer.invoke('panels:get-output', panelId, limit), - getConversationMessages: (panelId: string): Promise => ipcRenderer.invoke('panels:get-conversation-messages', panelId), - getJsonMessages: (panelId: string): Promise => ipcRenderer.invoke('panels:get-json-messages', panelId), - getPrompts: (panelId: string): Promise => ipcRenderer.invoke('panels:get-prompts', panelId), - sendInput: (panelId: string, input: string): Promise => ipcRenderer.invoke('panels:send-input', panelId, input), - continue: (panelId: string, input: string, model?: string): Promise => ipcRenderer.invoke('panels:continue', panelId, input, model), + invokeIpc('panels:create', { sessionId, type, title: name, initialState: config }), + getSessionPanels: (sessionId: string): Promise => invokeIpc('panels:list', sessionId), + deletePanel: (panelId: string): Promise => invokeIpc('panels:delete', panelId), + renamePanel: (panelId: string, name: string): Promise => invokeIpc('panels:update', panelId, { name }), + setActivePanel: (sessionId: string, panelId: string): Promise => invokeIpc('panels:set-active', sessionId, panelId), + resizeTerminal: (panelId: string, cols: number, rows: number): Promise => invokeIpc('panels:resize-terminal', panelId, cols, rows), + sendTerminalInput: (panelId: string, data: string): Promise => invokeIpc('panels:send-terminal-input', panelId, data), + getOutput: (panelId: string, limit?: number): Promise => invokeIpc('panels:get-output', panelId, limit), + getConversationMessages: (panelId: string): Promise => invokeIpc('panels:get-conversation-messages', panelId), + getJsonMessages: (panelId: string): Promise => invokeIpc('panels:get-json-messages', panelId), + getPrompts: (panelId: string): Promise => invokeIpc('panels:get-prompts', panelId), + sendInput: (panelId: string, input: string): Promise => invokeIpc('panels:send-input', panelId, input), + continue: (panelId: string, input: string, model?: string): Promise => invokeIpc('panels:continue', panelId, input, model), }, // Logs panel operations logs: { - runScript: (sessionId: string, command: string, cwd: string): Promise => ipcRenderer.invoke('logs:runScript', sessionId, command, cwd), - stopScript: (panelId: string): Promise => ipcRenderer.invoke('logs:stopScript', panelId), - isRunning: (sessionId: string): Promise => ipcRenderer.invoke('logs:isRunning', sessionId), + runScript: (sessionId: string, command: string, cwd: string): Promise => invokeIpc('logs:runScript', sessionId, command, cwd), + stopScript: (panelId: string): Promise => invokeIpc('logs:stopScript', panelId), + isRunning: (sessionId: string): Promise => invokeIpc('logs:isRunning', sessionId), }, // Debug utilities debug: { - getTableStructure: (tableName: 'folders' | 'sessions'): Promise => ipcRenderer.invoke('debug:get-table-structure', tableName), + getTableStructure: (tableName: 'folders' | 'sessions'): Promise => invokeIpc('debug:get-table-structure', tableName), }, // Nimbalyst integration nimbalyst: { - checkInstalled: (): Promise => ipcRenderer.invoke('nimbalyst:check-installed'), - openWorktree: (worktreePath: string): Promise => ipcRenderer.invoke('nimbalyst:open-worktree', worktreePath), + checkInstalled: (): Promise => invokeIpc('nimbalyst:check-installed'), + openWorktree: (worktreePath: string): Promise => invokeIpc('nimbalyst:open-worktree', worktreePath), }, // Analytics tracking analytics: { - getIdentity: () => ipcRenderer.invoke('analytics:get-identity'), + getIdentity: () => invokeIpc('analytics:get-identity'), onMainEvent: (callback: (event: { eventName: string; properties: Record }) => void) => { // Replay any events that arrived before this callback was registered for (const buffered of analyticsEventBuffer) { @@ -860,20 +869,20 @@ contextBridge.exposeInMainWorld('electronAPI', { // Spotlight spotlight: { - enable: (sessionId: string): Promise => ipcRenderer.invoke('spotlight:enable', sessionId), - disable: (sessionId: string): Promise => ipcRenderer.invoke('spotlight:disable', sessionId), - getStatus: (projectId: number): Promise => ipcRenderer.invoke('spotlight:get-status', projectId), + enable: (sessionId: string): Promise => invokeIpc('spotlight:enable', sessionId), + disable: (sessionId: string): Promise => invokeIpc('spotlight:disable', sessionId), + getStatus: (projectId: number): Promise => invokeIpc('spotlight:get-status', projectId), }, // Cloud VM management cloud: { - getState: (): Promise => ipcRenderer.invoke('cloud:get-state'), - startVm: (): Promise => ipcRenderer.invoke('cloud:start-vm'), - stopVm: (): Promise => ipcRenderer.invoke('cloud:stop-vm'), - startTunnel: (): Promise => ipcRenderer.invoke('cloud:start-tunnel'), - stopTunnel: (): Promise => ipcRenderer.invoke('cloud:stop-tunnel'), - startPolling: (): Promise => ipcRenderer.invoke('cloud:start-polling'), - stopPolling: (): Promise => ipcRenderer.invoke('cloud:stop-polling'), + getState: (): Promise => invokeIpc('cloud:get-state'), + startVm: (): Promise => invokeIpc('cloud:start-vm'), + stopVm: (): Promise => invokeIpc('cloud:stop-vm'), + startTunnel: (): Promise => invokeIpc('cloud:start-tunnel'), + stopTunnel: (): Promise => invokeIpc('cloud:stop-tunnel'), + startPolling: (): Promise => invokeIpc('cloud:start-polling'), + stopPolling: (): Promise => invokeIpc('cloud:stop-polling'), onStateChanged: (callback: (state: unknown) => void): (() => void) => { const wrappedCallback = (_event: unknown, state: unknown) => callback(state); ipcRenderer.on('cloud:state-changed', wrappedCallback); @@ -883,14 +892,14 @@ contextBridge.exposeInMainWorld('electronAPI', { // Resource monitor resourceMonitor: { - getSnapshot: (): Promise => ipcRenderer.invoke('resource-monitor:get-snapshot'), - startActive: (): Promise => ipcRenderer.invoke('resource-monitor:start-active'), - stopActive: (): Promise => ipcRenderer.invoke('resource-monitor:stop-active'), + getSnapshot: (): Promise => invokeIpc('resource-monitor:get-snapshot'), + startActive: (): Promise => invokeIpc('resource-monitor:start-active'), + stopActive: (): Promise => invokeIpc('resource-monitor:stop-active'), }, // Window state queries (invoke, not event subscriptions) window: { - isFocused: (): Promise => ipcRenderer.invoke('window:is-focused') as Promise, + isFocused: (): Promise => invokeIpc('window:is-focused') as Promise, }, // ptyHost: typed wrapper over the per-window MessagePort. The raw port is @@ -948,8 +957,8 @@ contextBridge.exposeInMainWorld('electronAPI', { // Expose electron event listeners and utilities for permission requests contextBridge.exposeInMainWorld('electron', { - openExternal: (url: string) => ipcRenderer.invoke('openExternal', url), - invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args), + openExternal: (url: string) => invokeIpc('openExternal', url), + invoke: (channel: string, ...args: unknown[]) => invokeIpc(channel, ...args), on: (channel: string, callback: (...args: unknown[]) => void) => { const validChannels = [ 'permission:request'