import Electron, { contextBridge, ipcRenderer, webUtils } from 'electron'; import { Recipe } from './recipe'; import { GooseApp } from './api'; import type { Settings, SettingKey } from './utils/settings'; import { defaultSettings } from './utils/settings'; // Mapping from settings keys to their old localStorage keys for lazy migration const localStorageKeyMap: Partial> = { theme: 'theme', useSystemTheme: 'use_system_theme', responseStyle: 'response_style', showPricing: 'show_pricing', sessionSharing: 'session_sharing_config', seenAnnouncementIds: 'seenAnnouncementIds', }; // Parse localStorage value based on the setting key function parseLocalStorageValue( key: K, rawValue: string ): Settings[K] | null { try { switch (key) { case 'theme': return (rawValue === 'dark' || rawValue === 'light' ? rawValue : null) as Settings[K]; case 'useSystemTheme': return (rawValue === 'true') as unknown as Settings[K]; case 'responseStyle': return rawValue as Settings[K]; case 'showPricing': return (rawValue === 'true') as unknown as Settings[K]; case 'sessionSharing': return JSON.parse(rawValue) as Settings[K]; case 'seenAnnouncementIds': return JSON.parse(rawValue) as Settings[K]; default: return null; } } catch { return null; } } interface NotificationData { title: string; body: string; } interface MessageBoxOptions { type?: 'none' | 'info' | 'error' | 'question' | 'warning'; buttons?: string[]; defaultId?: number; title?: string; message: string; detail?: string; } interface MessageBoxResponse { response: number; checkboxChecked?: boolean; } interface SaveDialogOptions { title?: string; defaultPath?: string; buttonLabel?: string; filters?: Array<{ name: string; extensions: string[] }>; message?: string; nameFieldLabel?: string; showsTagField?: boolean; } interface SaveDialogResponse { canceled: boolean; filePath?: string; } interface FileResponse { file: string; filePath: string; error: string | null; found: boolean; } const config = JSON.parse(process.argv.find((arg) => arg.startsWith('{')) || '{}'); interface UpdaterEvent { event: string; data?: unknown; } export interface CreateChatWindowOptions { query?: string; dir?: string; version?: string; resumeSessionId?: string; viewType?: string; recipeId?: string; } // Define the API types in a single place type ElectronAPI = { platform: string; reactReady: () => void; getConfig: () => Record; hideWindow: () => void; directoryChooser: () => Promise; createChatWindow: (options?: CreateChatWindowOptions) => void; logInfo: (txt: string) => void; showNotification: (data: NotificationData) => void; showMessageBox: (options: MessageBoxOptions) => Promise; showSaveDialog: (options: SaveDialogOptions) => Promise; openInChrome: (url: string) => void; fetchMetadata: (url: string) => Promise; reloadApp: () => void; checkForOllama: () => Promise; selectFileOrDirectory: (defaultPath?: string) => Promise; getBinaryPath: (binaryName: string) => Promise; readFile: (directory: string) => Promise; writeFile: (directory: string, content: string) => Promise; ensureDirectory: (dirPath: string) => Promise; listFiles: (dirPath: string, extension?: string) => Promise; getAllowedExtensions: () => Promise; getPathForFile: (file: File) => string; setMenuBarIcon: (show: boolean) => Promise; getMenuBarIconState: () => Promise; setDockIcon: (show: boolean) => Promise; getDockIconState: () => Promise; getSetting: (key: K) => Promise; setSetting: (key: K, value: Settings[K]) => Promise; getSecretKey: () => Promise; getGoosedHostPort: () => Promise; setWakelock: (enable: boolean) => Promise; getWakelockState: () => Promise; setSpellcheck: (enable: boolean) => Promise; getSpellcheckState: () => Promise; openNotificationsSettings: () => Promise; onMouseBackButtonClicked: (callback: () => void) => void; offMouseBackButtonClicked: (callback: () => void) => void; on: ( channel: string, callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void ) => void; off: ( channel: string, callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void ) => void; emit: (channel: string, ...args: unknown[]) => void; broadcastThemeChange: (themeData: { mode: string; useSystemTheme: boolean; theme: string; }) => void; openExternal: (url: string) => Promise; // Update-related functions getVersion: () => string; checkForUpdates: () => Promise<{ updateInfo: unknown; error: string | null }>; downloadUpdate: () => Promise<{ success: boolean; error: string | null }>; installUpdate: () => void; restartApp: () => void; onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => void; getUpdateState: () => Promise<{ updateAvailable: boolean; latestVersion?: string } | null>; isUsingGitHubFallback: () => Promise; // Recipe warning functions closeWindow: () => void; hasAcceptedRecipeBefore: (recipe: Recipe) => Promise; recordRecipeHash: (recipe: Recipe) => Promise; openDirectoryInExplorer: (directoryPath: string) => Promise; launchApp: (app: GooseApp) => Promise; refreshApp: (app: GooseApp) => Promise; closeApp: (appName: string) => Promise; addRecentDir: (dir: string) => Promise; }; type AppConfigAPI = { get: (key: string) => unknown; getAll: () => Record; }; const electronAPI: ElectronAPI = { platform: process.platform, reactReady: () => ipcRenderer.send('react-ready'), getConfig: () => { if (!config || Object.keys(config).length === 0) { console.warn( 'No config provided by main process. This may indicate an initialization issue.' ); } return config; }, hideWindow: () => ipcRenderer.send('hide-window'), directoryChooser: () => ipcRenderer.invoke('directory-chooser'), createChatWindow: (options?: CreateChatWindowOptions) => ipcRenderer.send('create-chat-window', options || {}), logInfo: (txt: string) => ipcRenderer.send('logInfo', txt), showNotification: (data: NotificationData) => ipcRenderer.send('notify', data), showMessageBox: (options: MessageBoxOptions) => ipcRenderer.invoke('show-message-box', options), showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke('show-save-dialog', options), openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url), fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url), reloadApp: () => ipcRenderer.send('reload-app'), checkForOllama: () => ipcRenderer.invoke('check-ollama'), selectFileOrDirectory: (defaultPath?: string) => ipcRenderer.invoke('select-file-or-directory', defaultPath), getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName), readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath), writeFile: (filePath: string, content: string) => ipcRenderer.invoke('write-file', filePath, content), ensureDirectory: (dirPath: string) => ipcRenderer.invoke('ensure-directory', dirPath), listFiles: (dirPath: string, extension?: string) => ipcRenderer.invoke('list-files', dirPath, extension), getPathForFile: (file: File) => webUtils.getPathForFile(file), getAllowedExtensions: () => ipcRenderer.invoke('get-allowed-extensions'), setMenuBarIcon: (show: boolean) => ipcRenderer.invoke('set-menu-bar-icon', show), getMenuBarIconState: () => ipcRenderer.invoke('get-menu-bar-icon-state'), setDockIcon: (show: boolean) => ipcRenderer.invoke('set-dock-icon', show), getDockIconState: () => ipcRenderer.invoke('get-dock-icon-state'), getSetting: async (key: K): Promise => { try { // Check for localStorage value first (lazy migration) const localStorageKey = localStorageKeyMap[key]; if (localStorageKey) { const rawValue = localStorage.getItem(localStorageKey); if (rawValue !== null) { const parsed = parseLocalStorageValue(key, rawValue); if (parsed !== null) { return parsed; } } } return await ipcRenderer.invoke('get-setting', key); } catch (error) { console.error(`Failed to get setting '${key}', using default`, error); return defaultSettings[key]; } }, setSetting: async (key: K, value: Settings[K]): Promise => { // Clear any localStorage version when writing const localStorageKey = localStorageKeyMap[key]; if (localStorageKey) { localStorage.removeItem(localStorageKey); } return ipcRenderer.invoke('set-setting', key, value); }, getSecretKey: () => ipcRenderer.invoke('get-secret-key'), getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'), setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable), getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'), setSpellcheck: (enable: boolean) => ipcRenderer.invoke('set-spellcheck', enable), getSpellcheckState: () => ipcRenderer.invoke('get-spellcheck-state'), openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'), onMouseBackButtonClicked: (callback: () => void) => { // Wrapper that ignores the event parameter. const wrappedCallback = (_event: Electron.IpcRendererEvent) => callback(); ipcRenderer.on('mouse-back-button-clicked', wrappedCallback); return wrappedCallback; }, offMouseBackButtonClicked: (callback: () => void) => { ipcRenderer.removeListener('mouse-back-button-clicked', callback); }, on: ( channel: string, callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void ) => { ipcRenderer.on(channel, callback); }, off: ( channel: string, callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void ) => { ipcRenderer.off(channel, callback); }, emit: (channel: string, ...args: unknown[]) => { ipcRenderer.emit(channel, ...args); }, broadcastThemeChange: (themeData: { mode: string; useSystemTheme: boolean; theme: string }) => { ipcRenderer.send('broadcast-theme-change', themeData); }, openExternal: (url: string): Promise => { return ipcRenderer.invoke('open-external', url); }, getVersion: (): string => { return config.GOOSE_VERSION || ipcRenderer.sendSync('get-app-version') || ''; }, checkForUpdates: (): Promise<{ updateInfo: unknown; error: string | null }> => { return ipcRenderer.invoke('check-for-updates'); }, downloadUpdate: (): Promise<{ success: boolean; error: string | null }> => { return ipcRenderer.invoke('download-update'); }, installUpdate: (): void => { ipcRenderer.invoke('install-update'); }, restartApp: (): void => { ipcRenderer.send('restart-app'); }, onUpdaterEvent: (callback: (event: UpdaterEvent) => void): void => { ipcRenderer.on('updater-event', (_event, data) => callback(data)); }, getUpdateState: (): Promise<{ updateAvailable: boolean; latestVersion?: string } | null> => { return ipcRenderer.invoke('get-update-state'); }, isUsingGitHubFallback: (): Promise => { return ipcRenderer.invoke('is-using-github-fallback'); }, closeWindow: () => ipcRenderer.send('close-window'), hasAcceptedRecipeBefore: (recipe: Recipe) => ipcRenderer.invoke('has-accepted-recipe-before', recipe), recordRecipeHash: (recipe: Recipe) => ipcRenderer.invoke('record-recipe-hash', recipe), openDirectoryInExplorer: (directoryPath: string) => ipcRenderer.invoke('open-directory-in-explorer', directoryPath), launchApp: (app: GooseApp) => ipcRenderer.invoke('launch-app', app), refreshApp: (app: GooseApp) => ipcRenderer.invoke('refresh-app', app), closeApp: (appName: string) => ipcRenderer.invoke('close-app', appName), addRecentDir: (dir: string) => ipcRenderer.invoke('add-recent-dir', dir), }; const appConfigAPI: AppConfigAPI = { get: (key: string) => config[key], getAll: () => config, }; // Expose the APIs contextBridge.exposeInMainWorld('electron', electronAPI); contextBridge.exposeInMainWorld('appConfig', appConfigAPI); // Type declaration for TypeScript declare global { interface Window { electron: ElectronAPI; appConfig: AppConfigAPI; } }