/** * Preload Script * Exposes safe APIs to the renderer process via contextBridge */ import { contextBridge, ipcRenderer } from 'electron'; const isDevelopment = process.env.NODE_ENV === 'development' || Boolean(process.env.VITE_DEV_SERVER_URL); const imageWorkspaceMode = process.env.NIANCODE_IMAGE_WORKSPACE_MODE?.trim().toLowerCase(); const imageWorkspaceLocalDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL) && (imageWorkspaceMode === 'local' || !imageWorkspaceMode); const validInvokeChannels = [ 'hostapi:fetch', 'hostapi:token', 'hostapi:base-url', 'opencode:status', 'opencode:start', 'opencode:stop', 'opencode:restart', 'opencode:health', 'shell:openExternal', 'shell:showItemInFolder', 'shell:openPath', 'dialog:open', 'dialog:save', 'dialog:message', 'transcript:save', 'app:version', 'app:name', 'app:getPath', 'app:platform', 'app:quit', 'app:relaunch', 'app:request', 'window:minimize', 'window:maximize', 'window:close', 'window:isMaximized', 'settings:get', 'settings:set', 'settings:setMany', 'settings:getAll', 'settings:reset', 'provider:list', 'provider:get', 'provider:save', 'provider:delete', 'provider:setApiKey', 'provider:updateWithKey', 'provider:deleteApiKey', 'provider:hasApiKey', 'provider:getApiKey', 'provider:setDefault', 'provider:getDefault', 'provider:validateKey', 'update:status', 'update:version', 'update:check', 'update:download', 'update:install', 'update:setChannel', 'update:setAutoDownload', 'update:cancelAutoInstall', ]; const validEventChannels = [ 'navigate', 'opencode:status', 'opencode:stderr', 'update:status-changed', 'update:checking', 'update:available', 'update:not-available', 'update:progress', 'update:downloaded', 'update:error', 'update:auto-install-countdown', 'oauth:code', 'oauth:success', 'oauth:error', 'agent-browser:show', 'agent-browser:state', ]; /** * IPC renderer methods exposed to the renderer process */ const electronAPI = { /** * IPC invoke (request-response pattern) */ ipcRenderer: { invoke: (channel: string, ...args: unknown[]) => { if (validInvokeChannels.includes(channel)) { return ipcRenderer.invoke(channel, ...args); } throw new Error(`Invalid IPC channel: ${channel}`); }, /** * Listen for events from main process */ on: (channel: string, callback: (...args: unknown[]) => void) => { if (validEventChannels.includes(channel) || channel.startsWith('ext:')) { const subscription = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => { callback(...args); }; ipcRenderer.on(channel, subscription); // Return unsubscribe function return () => { ipcRenderer.removeListener(channel, subscription); }; } throw new Error(`Invalid IPC channel: ${channel}`); }, /** * Listen for a single event from main process */ once: (channel: string, callback: (...args: unknown[]) => void) => { if (validEventChannels.includes(channel) || channel.startsWith('ext:')) { ipcRenderer.once(channel, (_event, ...args) => callback(...args)); return; } throw new Error(`Invalid IPC channel: ${channel}`); }, /** * Remove all listeners for a channel */ off: (channel: string, callback?: (...args: unknown[]) => void) => { if (callback) { // eslint-disable-next-line @typescript-eslint/no-explicit-any ipcRenderer.removeListener(channel, callback as any); } else { ipcRenderer.removeAllListeners(channel); } }, }, /** * Open external URL in default browser */ openExternal: (url: string) => { return ipcRenderer.invoke('shell:openExternal', url); }, /** * Get current platform */ platform: process.platform, /** * Check if running in development */ isDev: isDevelopment, /** * Narrow anonymous-route exception for the explicit unpackaged image workspace adapter. */ imageWorkspaceLocalDevelopment, }; // Expose the API to the renderer process contextBridge.exposeInMainWorld('electron', electronAPI); // Type declarations for the renderer process export type ElectronAPI = typeof electronAPI;