import { contextBridge, ipcRenderer, webFrame, webUtils } from 'electron' // Which translucency the OS can back. Asked synchronously because the renderer // needs it before its first paint, and answered by main because deciding it // needs `os.release()` — a sandboxed preload may only require electron, events, // timers and url, so importing node:os here throws before contextBridge runs // and takes the ENTIRE bridge down with it (window.hermesDesktop undefined => // "Desktop IPC bridge is unavailable"). No reply means no glass, which degrades // to an ordinary opaque window rather than a page thinned over nothing. const translucencySupport = ipcRenderer.sendSync('hermes:translucency:support') const hudWindowing = ipcRenderer.sendSync('hermes:hud:windowing') const hudNativeDrag = hudWindowing?.nativeDrag === true const launchFlags = ipcRenderer.sendSync('hermes:launch-flags') contextBridge.exposeInMainWorld('hermesDesktop', { glassSupported: translucencySupport?.glass === true, translucencySupported: translucencySupport?.translucency === true, // Launch-flag fact: the app was started with --local, so the renderer may // show the local-models surfaces. Static for the window's lifetime. localModelsEnabled: launchFlags?.localModels === true, getConnection: profile => ipcRenderer.invoke('hermes:connection', profile), // Registry-scoped backend resolution: { connectionId, profile } → descriptor. getConnectionFor: payload => ipcRenderer.invoke('hermes:connection:for', payload), getProfileRoutes: profiles => ipcRenderer.invoke('hermes:plugin-profile-routes', profiles), revalidateConnection: () => ipcRenderer.invoke('hermes:connection:revalidate'), touchBackend: profile => ipcRenderer.invoke('hermes:backend:touch', profile), getPoolLimits: () => ipcRenderer.invoke('hermes:pool-limits:get'), setPoolLimits: limits => ipcRenderer.invoke('hermes:pool-limits:set', limits), getGatewayWsUrl: profile => ipcRenderer.invoke('hermes:gateway:ws-url', profile), // Registry-scoped fresh WS URL: { connectionId, profile } → result shape of // getGatewayWsUrl, minted against that connection's backend. getGatewayWsUrlFor: payload => ipcRenderer.invoke('hermes:gateway:ws-url-for', payload), // Union agent roster across every registered connection. getAgentRoster: () => ipcRenderer.invoke('hermes:agents:roster'), openSessionWindow: (sessionId, opts) => ipcRenderer.invoke('hermes:window:openSession', sessionId, opts), openSessionInTerminal: (sessionId, opts) => ipcRenderer.invoke('hermes:window:openInTerminal', sessionId, opts), openWindow: () => ipcRenderer.invoke('hermes:window:openInstance'), openBrowserWindow: tabId => ipcRenderer.invoke('hermes:window:openBrowser', tabId), onBrowserPopoutClosed: callback => { const listener = (_event, tabId) => callback(tabId) ipcRenderer.on('hermes:browser-popout:closed', listener) return () => ipcRenderer.removeListener('hermes:browser-popout:closed', listener) }, claimAmbientCue: key => ipcRenderer.invoke('hermes:ambient:claim', key), wakeIndicator: { getState: () => ipcRenderer.invoke('hermes:wake-indicator:get'), setState: state => ipcRenderer.send('hermes:wake-indicator:set', state), onState: callback => { const listener = (_event, state) => callback(state) ipcRenderer.on('hermes:wake-indicator:state', listener) return () => ipcRenderer.removeListener('hermes:wake-indicator:state', listener) } }, petOverlay: { // Main renderer → main process: window lifecycle + drag. `request` is // `{ bounds, screen }`; resolves with the screen bounds it actually used. open: request => ipcRenderer.invoke('hermes:pet-overlay:open', request), close: () => ipcRenderer.invoke('hermes:pet-overlay:close'), setBounds: bounds => ipcRenderer.send('hermes:pet-overlay:set-bounds', bounds), setIgnoreMouse: ignore => ipcRenderer.send('hermes:pet-overlay:ignore-mouse', ignore), // Flip the overlay focusable (and focus it) while the composer needs keys. setFocusable: focusable => ipcRenderer.send('hermes:pet-overlay:set-focusable', focusable), // Main renderer → overlay (forwarded by main): push the latest pet state. pushState: payload => ipcRenderer.send('hermes:pet-overlay:state', payload), // Overlay → main renderer (forwarded by main): pop back in / composer submit. control: payload => ipcRenderer.send('hermes:pet-overlay:control', payload), // Overlay subscribes to state pushes. onState: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:pet-overlay:state', listener) return () => ipcRenderer.removeListener('hermes:pet-overlay:state', listener) }, // Main renderer subscribes to overlay control messages. onControl: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:pet-overlay:control', listener) return () => ipcRenderer.removeListener('hermes:pet-overlay:control', listener) } }, // HUD mode: the chrome-free floating chat. A full app renderer (own gateway) // sized as a floating bar, so it mounts the real composer. Main owns the // window; `onChanged` keeps every window's toggle truthful. hud: { nativeDrag: hudNativeDrag, windowing: { clientPlacement: hudWindowing?.clientPlacement !== false, controlDrag: hudWindowing?.controlDrag === true, nativeDrag: hudNativeDrag, solid: hudWindowing?.solid === true, workspaceTransfer: hudWindowing?.workspaceTransfer === true }, open: request => ipcRenderer.invoke('hermes:hud:open', request), close: () => ipcRenderer.invoke('hermes:hud:close'), setIgnoreMouse: ignore => ipcRenderer.send('hermes:hud:ignore-mouse', ignore), beginMove: () => ipcRenderer.send('hermes:hud:begin-move'), endMove: () => ipcRenderer.send('hermes:hud:end-move'), moveBy: delta => ipcRenderer.send('hermes:hud:move-by', delta), setWorkspaceTransfer: transferring => ipcRenderer.send('hermes:hud:workspace-transfer', transferring), setBounds: bounds => ipcRenderer.send('hermes:hud:set-bounds', bounds), resetLayout: () => ipcRenderer.invoke('hermes:hud:reset-layout'), // Whether the band covers the window below the bar. Main pairs it with the // user's translucency setting to decide the native frost (macOS vibrancy / // Windows 11 DWM backdrop) — see hudFrostFor. setFrost: showing => ipcRenderer.invoke('hermes:hud:frost', showing), // The HUD tells main which session it is on; main hands that back to the // app window when the HUD closes, so the app can re-home onto it. setSession: sessionId => ipcRenderer.send('hermes:hud:session', sessionId), onGoto: callback => { const listener = (_event, sessionId) => callback(sessionId) ipcRenderer.on('hermes:hud:goto', listener) return () => ipcRenderer.removeListener('hermes:hud:goto', listener) }, onChanged: callback => { const listener = (_event, state) => callback(state) ipcRenderer.on('hermes:hud:changed', listener) return () => ipcRenderer.removeListener('hermes:hud:changed', listener) }, // Linux only, and silent elsewhere: where the cursor is, in page // coordinates, or null when it has left the window. Stands in for the // mousemove that `setIgnoreMouseEvents(true, { forward: true })` delivers on // macOS and Windows but not here. onCursor: callback => { const listener = (_event, point) => callback(point) ipcRenderer.on('hermes:hud:cursor', listener) return () => ipcRenderer.removeListener('hermes:hud:cursor', listener) }, // Main's game-overlay watch: whether a fullscreen app (a game) is under // the HUD, so the renderer can step back to the low-opacity overlay // treatment while one owns the screen. onGameOverlay: callback => { const listener = (_event, state) => callback(state) ipcRenderer.on('hermes:hud:game-overlay', listener) return () => ipcRenderer.removeListener('hermes:hud:game-overlay', listener) } }, // Quick Entry: the global-hotkey mini composer window. Main owns the OS // shortcut + the persisted preference; the quick window only captures text // and hands it back, and the primary renderer submits it through the normal // prompt path. quickEntry: { getSettings: () => ipcRenderer.invoke('hermes:quick-entry:settings:get'), setSettings: patch => ipcRenderer.invoke('hermes:quick-entry:settings:set', patch), submit: payload => ipcRenderer.send('hermes:quick-entry:submit', payload), dismiss: () => ipcRenderer.send('hermes:quick-entry:dismiss'), // Primary renderer → main → quick window: gateway connection state + the // recent-session options the target picker offers. Main caches the latest // payload so a freshly spawned quick window starts from truth. pushState: payload => ipcRenderer.send('hermes:quick-entry:state', payload), // Quick window subscribes to those pushes. onState: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:quick-entry:state', listener) return () => ipcRenderer.removeListener('hermes:quick-entry:state', listener) }, // Main → primary renderer: a submit captured by the quick window. onSubmit: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:quick-entry:submit', listener) return () => ipcRenderer.removeListener('hermes:quick-entry:submit', listener) }, // Main → quick window: you were just summoned (reset draft + refocus). onShown: callback => { const listener = () => callback() ipcRenderer.on('hermes:quick-entry:shown', listener) return () => ipcRenderer.removeListener('hermes:quick-entry:shown', listener) } }, getBootProgress: () => ipcRenderer.invoke('hermes:boot-progress:get'), getConnectionConfig: profile => ipcRenderer.invoke('hermes:connection-config:get', profile), saveConnectionConfig: payload => ipcRenderer.invoke('hermes:connection-config:save', payload), applyConnectionConfig: payload => ipcRenderer.invoke('hermes:connection-config:apply', payload), testConnectionConfig: payload => ipcRenderer.invoke('hermes:connection-config:test', payload), // Opt-in OS-keychain encryption for stored gateway secrets (default off — // see secret-storage-policy.ts). get never touches the OS keychain. getSecretStorageEncryption: () => ipcRenderer.invoke('hermes:secret-storage:get'), setSecretStorageEncryption: (on: boolean) => ipcRenderer.invoke('hermes:secret-storage:set', on), // v2 multi-connection registry: named agent sources (local / remote / cloud / ssh). connections: { list: () => ipcRenderer.invoke('hermes:connections:list'), save: payload => ipcRenderer.invoke('hermes:connections:save', payload), remove: id => ipcRenderer.invoke('hermes:connections:remove', id), setPrimary: id => ipcRenderer.invoke('hermes:connections:set-primary', id), setLaunchMode: mode => ipcRenderer.invoke('hermes:connections:set-launch-mode', mode), setLastUsed: id => ipcRenderer.invoke('hermes:connections:set-last-used', id), test: id => ipcRenderer.invoke('hermes:connections:test', id), updateManaged: id => ipcRenderer.invoke('hermes:connections:update-managed', id), // Fan out `hermes update` to every eligible registered connection. // Optional excludeIds skips rows the caller updates through another path. updateAll: options => ipcRenderer.invoke('hermes:connections:update-all', options), // Registry lifecycle push (main → renderer): a connection was removed or // materially edited, so secondaries scoped to it must be disposed (and, // for edits, re-dialed at the new target). onChanged: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:connections:changed', listener) return () => ipcRenderer.removeListener('hermes:connections:changed', listener) } }, sshConfigHosts: () => ipcRenderer.invoke('hermes:ssh-config:hosts'), sshResolveHost: host => ipcRenderer.invoke('hermes:ssh-config:resolve', host), probeConnectionConfig: remoteUrl => ipcRenderer.invoke('hermes:connection-config:probe', remoteUrl), oauthLoginConnectionConfig: remoteUrl => ipcRenderer.invoke('hermes:connection-config:oauth-login', remoteUrl), oauthLogoutConnectionConfig: remoteUrl => ipcRenderer.invoke('hermes:connection-config:oauth-logout', remoteUrl), // Hermes Cloud: one portal login powers discovery + silent per-agent sign-in // (cloud-auto-discovery Phase 3). cloud: { status: () => ipcRenderer.invoke('hermes:cloud:status'), login: () => ipcRenderer.invoke('hermes:cloud:login'), logout: () => ipcRenderer.invoke('hermes:cloud:logout'), discover: org => ipcRenderer.invoke('hermes:cloud:discover', org), agentSignIn: dashboardUrl => ipcRenderer.invoke('hermes:cloud:agent-sign-in', dashboardUrl) }, profile: { get: () => ipcRenderer.invoke('hermes:profile:get'), remember: name => ipcRenderer.invoke('hermes:profile:remember', name), set: name => ipcRenderer.invoke('hermes:profile:set', name) }, api: request => ipcRenderer.invoke('hermes:api', request), notify: payload => ipcRenderer.invoke('hermes:notify', payload), requestMicrophoneAccess: () => ipcRenderer.invoke('hermes:requestMicrophoneAccess'), readWindowBelow: () => ipcRenderer.invoke('hermes:window:readBelow'), readFileDataUrl: filePath => ipcRenderer.invoke('hermes:readFileDataUrl', filePath), readFileDataUrlForAttach: filePath => ipcRenderer.invoke('hermes:readFileDataUrlForAttach', filePath), dataUrlReadMax: { get: () => ipcRenderer.invoke('hermes:data-url-read-max:get'), set: maxMb => ipcRenderer.invoke('hermes:data-url-read-max:set', maxMb) }, readFileText: filePath => ipcRenderer.invoke('hermes:readFileText', filePath), readPluginSource: (filePath: string) => ipcRenderer.invoke('hermes:readPluginSource', filePath), selectPaths: options => ipcRenderer.invoke('hermes:selectPaths', options), selectSavePath: options => ipcRenderer.invoke('hermes:selectSavePath', options), writeClipboard: text => ipcRenderer.invoke('hermes:writeClipboard', text), readClipboard: () => ipcRenderer.invoke('hermes:readClipboard'), saveGatewayFile: payload => ipcRenderer.invoke('hermes:saveGatewayFile', payload), saveImageFromUrl: url => ipcRenderer.invoke('hermes:saveImageFromUrl', url), contextMenuEdit: command => ipcRenderer.invoke('hermes:context-menu:edit', command), contextMenuCopyImage: () => ipcRenderer.invoke('hermes:context-menu:copy-image'), contextMenuSpellcheck: action => ipcRenderer.invoke('hermes:context-menu:spellcheck', action), contextMenuGuestAddWord: payload => ipcRenderer.invoke('hermes:context-menu:guest-add-word', payload), onContextMenuSpellcheck: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:context-menu-spellcheck', listener) return () => ipcRenderer.removeListener('hermes:context-menu-spellcheck', listener) }, saveImageBuffer: (data, ext, name) => ipcRenderer.invoke('hermes:saveImageBuffer', { data, ext, name }), capturePreview: payload => ipcRenderer.invoke('hermes:capturePreview', payload), saveClipboardImage: () => ipcRenderer.invoke('hermes:saveClipboardImage'), getPathForFile: file => { try { return webUtils.getPathForFile(file) || '' } catch { return '' } }, normalizePreviewTarget: (target, baseDir) => ipcRenderer.invoke('hermes:normalizePreviewTarget', target, baseDir), watchPreviewFile: url => ipcRenderer.invoke('hermes:watchPreviewFile', url), watchDirectory: dir => ipcRenderer.invoke('hermes:watchDirectory', dir), stopPreviewFileWatch: id => ipcRenderer.invoke('hermes:stopPreviewFileWatch', id), setActiveWork: payload => ipcRenderer.send('hermes:active-work', payload), setTitleBarTheme: payload => ipcRenderer.send('hermes:titlebar-theme', payload), setNativeTheme: mode => ipcRenderer.send('hermes:native-theme', mode), setTranslucency: payload => ipcRenderer.send('hermes:translucency', payload), setKeepAwake: on => ipcRenderer.send('hermes:keep-awake', on), setDisableF12: blocked => ipcRenderer.send('hermes:devtools:disable-f12', blocked), setPreviewShortcutActive: active => ipcRenderer.send('hermes:previewShortcutActive', Boolean(active)), openExternal: url => ipcRenderer.invoke('hermes:openExternal', url), mcpOauth: { // One-shot loopback listener for MCP OAuth against remote backends: bind // on this machine, hand redirectUri to mcp.servers.oauth.start, then wait // for the provider redirect and relay code/state via oauth.callback. listen: () => ipcRenderer.invoke('hermes:mcp-oauth:listen'), wait: (id, timeoutMs) => ipcRenderer.invoke('hermes:mcp-oauth:wait', id, timeoutMs), cancel: id => ipcRenderer.invoke('hermes:mcp-oauth:cancel', id) }, openPreviewInBrowser: url => ipcRenderer.invoke('hermes:openPreviewInBrowser', url), reachPreviewUrl: url => ipcRenderer.invoke('hermes:preview:reach', url), setActiveConnectionRoute: route => ipcRenderer.send('hermes:connection:active-route', route), fetchLinkTitle: url => ipcRenderer.invoke('hermes:fetchLinkTitle', url), resolveFavicon: url => ipcRenderer.invoke('hermes:resolveFavicon', url), sanitizeWorkspaceCwd: cwd => ipcRenderer.invoke('hermes:workspace:sanitize', cwd), settings: { getDefaultProjectDir: () => ipcRenderer.invoke('hermes:setting:defaultProjectDir:get'), setDefaultProjectDir: dir => ipcRenderer.invoke('hermes:setting:defaultProjectDir:set', dir), pickDefaultProjectDir: () => ipcRenderer.invoke('hermes:setting:defaultProjectDir:pick') }, zoom: { // Current zoom of this window, as { level, percent }. get: () => ipcRenderer.invoke('hermes:zoom:get'), // Synchronous zoom factor (1 = 100%). Coordinate math needs it in the // same tick as the event it converts, so no IPC round-trip here. factor: () => webFrame.getZoomFactor(), setPercent: percent => ipcRenderer.send('hermes:zoom:set-percent', percent), // Fires on every zoom change, including the Ctrl/Cmd +/-/0 shortcuts, // so the settings UI can stay in sync with the keyboard. onChanged: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:zoom:changed', listener) return () => ipcRenderer.removeListener('hermes:zoom:changed', listener) } }, revealLogs: () => ipcRenderer.invoke('hermes:logs:reveal'), getRecentLogs: () => ipcRenderer.invoke('hermes:logs:recent'), // Fire-and-forget: persists a renderer error-boundary catch (with component // stack) to desktop.log so crashes survive the window (#79428). reportRendererError: report => ipcRenderer.send('hermes:logs:renderer-error', report), readDir: dirPath => ipcRenderer.invoke('hermes:fs:readDir', dirPath), gitRoot: startPath => ipcRenderer.invoke('hermes:fs:gitRoot', startPath), revealPath: targetPath => ipcRenderer.invoke('hermes:fs:reveal', targetPath), openDir: dirPath => ipcRenderer.invoke('hermes:fs:openDir', dirPath), desktopPluginsRoot: () => ipcRenderer.invoke('hermes:fs:desktopPluginsRoot'), logsRoot: () => ipcRenderer.invoke('hermes:fs:logsRoot'), agentPluginsRoot: () => ipcRenderer.invoke('hermes:fs:agentPluginsRoot'), renamePath: (targetPath, newName) => ipcRenderer.invoke('hermes:fs:rename', targetPath, newName), writeTextFile: (filePath, content) => ipcRenderer.invoke('hermes:fs:writeText', filePath, content), trashPath: targetPath => ipcRenderer.invoke('hermes:fs:trash', targetPath), git: { worktreeList: repoPath => ipcRenderer.invoke('hermes:git:worktreeList', repoPath), worktreeAdd: (repoPath, options) => ipcRenderer.invoke('hermes:git:worktreeAdd', repoPath, options), worktreeRemove: (repoPath, worktreePath, options) => ipcRenderer.invoke('hermes:git:worktreeRemove', repoPath, worktreePath, options), branchSwitch: (repoPath, branch) => ipcRenderer.invoke('hermes:git:branchSwitch', repoPath, branch), branchList: repoPath => ipcRenderer.invoke('hermes:git:branchList', repoPath), baseBranchList: repoPath => ipcRenderer.invoke('hermes:git:baseBranchList', repoPath), repoStatus: repoPath => ipcRenderer.invoke('hermes:git:repoStatus', repoPath), fileDiff: (repoPath, filePath) => ipcRenderer.invoke('hermes:git:fileDiff', repoPath, filePath), scanRepos: (roots, options) => ipcRenderer.invoke('hermes:git:scanRepos', roots, options), review: { list: (repoPath, scope, baseRef) => ipcRenderer.invoke('hermes:git:review:list', repoPath, scope, baseRef), diff: (repoPath, filePath, scope, baseRef, staged) => ipcRenderer.invoke('hermes:git:review:diff', repoPath, filePath, scope, baseRef, staged), stage: (repoPath, filePath) => ipcRenderer.invoke('hermes:git:review:stage', repoPath, filePath), unstage: (repoPath, filePath) => ipcRenderer.invoke('hermes:git:review:unstage', repoPath, filePath), revert: (repoPath, filePath) => ipcRenderer.invoke('hermes:git:review:revert', repoPath, filePath), revParse: (repoPath, ref) => ipcRenderer.invoke('hermes:git:review:revParse', repoPath, ref), commit: (repoPath, message, push) => ipcRenderer.invoke('hermes:git:review:commit', repoPath, message, push), commitContext: repoPath => ipcRenderer.invoke('hermes:git:review:commitContext', repoPath), push: repoPath => ipcRenderer.invoke('hermes:git:review:push', repoPath), shipInfo: repoPath => ipcRenderer.invoke('hermes:git:review:shipInfo', repoPath), prList: (repoPath, branches, numbers) => ipcRenderer.invoke('hermes:git:review:prList', repoPath, branches, numbers), fetchPrComment: (repoPath, url) => ipcRenderer.invoke('hermes:git:review:fetchPrComment', repoPath, url), createPr: repoPath => ipcRenderer.invoke('hermes:git:review:createPr', repoPath) } }, terminal: { attach: id => ipcRenderer.invoke('hermes:terminal:attach', id), cwd: id => ipcRenderer.invoke('hermes:terminal:cwd', id), dispose: id => ipcRenderer.invoke('hermes:terminal:dispose', id), resize: (id, size) => ipcRenderer.invoke('hermes:terminal:resize', id, size), start: options => ipcRenderer.invoke('hermes:terminal:start', options), write: (id, data) => ipcRenderer.invoke('hermes:terminal:write', id, data), onData: (id, callback) => { const channel = `hermes:terminal:${id}:data` const listener = (_event, payload) => callback(payload) ipcRenderer.on(channel, listener) return () => ipcRenderer.removeListener(channel, listener) }, onExit: (id, callback) => { const channel = `hermes:terminal:${id}:exit` const listener = (_event, payload) => callback(payload) ipcRenderer.on(channel, listener) return () => ipcRenderer.removeListener(channel, listener) } }, onClosePreviewRequested: callback => { const listener = () => callback() ipcRenderer.on('hermes:close-preview-requested', listener) return () => ipcRenderer.removeListener('hermes:close-preview-requested', listener) }, onPreviewNav: callback => { const listener = (_event, command) => callback(command) ipcRenderer.on('hermes:preview-nav', listener) return () => ipcRenderer.removeListener('hermes:preview-nav', listener) }, onOpenFolderRequested: callback => { const listener = () => callback() ipcRenderer.on('hermes:open-folder-requested', listener) return () => ipcRenderer.removeListener('hermes:open-folder-requested', listener) }, onOpenUpdatesRequested: callback => { const listener = () => callback() ipcRenderer.on('hermes:open-updates', listener) return () => ipcRenderer.removeListener('hermes:open-updates', listener) }, onDeepLink: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:deep-link', listener) return () => ipcRenderer.removeListener('hermes:deep-link', listener) }, signalDeepLinkReady: () => ipcRenderer.invoke('hermes:deep-link-ready'), probePluginRepo: payload => ipcRenderer.invoke('hermes:plugin:probe', payload), installDesktopPlugin: payload => ipcRenderer.invoke('hermes:plugin:installDesktop', payload), onWindowStateChanged: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:window-state-changed', listener) return () => ipcRenderer.removeListener('hermes:window-state-changed', listener) }, onFocusSession: callback => { const listener = (_event, sessionId) => callback(sessionId) ipcRenderer.on('hermes:focus-session', listener) return () => ipcRenderer.removeListener('hermes:focus-session', listener) }, onNotificationAction: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:notification-action', listener) return () => ipcRenderer.removeListener('hermes:notification-action', listener) }, onNotificationActivate: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:notification-activate', listener) return () => ipcRenderer.removeListener('hermes:notification-activate', listener) }, onPreviewFileChanged: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:preview-file-changed', listener) return () => ipcRenderer.removeListener('hermes:preview-file-changed', listener) }, onBackendExit: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:backend-exit', listener) return () => ipcRenderer.removeListener('hermes:backend-exit', listener) }, // Soft gateway-mode apply finished tearing down the primary backend. Renderer // should wipe session lists + re-dial without a window reload. onConnectionApplied: callback => { const listener = () => callback() ipcRenderer.on('hermes:connection:applied', listener) return () => ipcRenderer.removeListener('hermes:connection:applied', listener) }, onPowerResume: callback => { const listener = () => callback() ipcRenderer.on('hermes:power-resume', listener) return () => ipcRenderer.removeListener('hermes:power-resume', listener) }, // AC ↔ battery transitions; renderers slow their backstop polls on battery. getOnBattery: () => ipcRenderer.invoke('hermes:power-battery:get'), onBatteryChanged: callback => { const listener = (_event, onBattery) => callback(Boolean(onBattery)) ipcRenderer.on('hermes:power-battery', listener) return () => ipcRenderer.removeListener('hermes:power-battery', listener) }, onBootProgress: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:boot-progress', listener) return () => ipcRenderer.removeListener('hermes:boot-progress', listener) }, // First-launch bootstrap progress -- emitted by the install.ps1 stage // runner in main.ts (apps/desktop/electron/bootstrap-runner.ts). // Renderer's install overlay subscribes to live events and queries the // current snapshot via getBootstrapState() to recover after a devtools // reload mid-bootstrap. getBootstrapState: () => ipcRenderer.invoke('hermes:bootstrap:get'), continueBootstrapLocal: () => ipcRenderer.invoke('hermes:bootstrap:continue-local'), recycleBackend: profile => ipcRenderer.invoke('hermes:backend:recycle', profile), resetBootstrap: () => ipcRenderer.invoke('hermes:bootstrap:reset'), repairBootstrap: () => ipcRenderer.invoke('hermes:bootstrap:repair'), cancelBootstrap: () => ipcRenderer.invoke('hermes:bootstrap:cancel'), onBootstrapEvent: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:bootstrap:event', listener) return () => ipcRenderer.removeListener('hermes:bootstrap:event', listener) }, getVersion: () => ipcRenderer.invoke('hermes:version'), relaunchApp: () => ipcRenderer.invoke('hermes:app:relaunch'), getRemoteDisplayReason: () => ipcRenderer.invoke('hermes:get-remote-display-reason'), uninstall: { summary: () => ipcRenderer.invoke('hermes:uninstall:summary'), run: mode => ipcRenderer.invoke('hermes:uninstall:run', { mode }) }, updates: { check: () => ipcRenderer.invoke('hermes:updates:check'), apply: opts => ipcRenderer.invoke('hermes:updates:apply', opts), getBranch: () => ipcRenderer.invoke('hermes:updates:branch:get'), setBranch: name => ipcRenderer.invoke('hermes:updates:branch:set', name), onProgress: callback => { const listener = (_event, payload) => callback(payload) ipcRenderer.on('hermes:updates:progress', listener) return () => ipcRenderer.removeListener('hermes:updates:progress', listener) } }, themes: { fetchMarketplace: id => ipcRenderer.invoke('hermes:vscode-theme:fetch', id), searchMarketplace: query => ipcRenderer.invoke('hermes:vscode-theme:search', query) }, // Find-in-page (Ctrl/Cmd+F): delegates to Electron's // webContents.findInPage on the IPC sender's window so a Cmd+F pressed // in a secondary session window searches THAT window, not the primary. // `onFoundInPage` returns the unsubscribe fn; the renderer wires it via // `initFindInPageListener` in store/find-in-page.ts and tears it down // when the FindBar unmounts. findInPage: (query, options) => ipcRenderer.invoke('hermes:find-in-page', query, options), stopFindInPage: () => ipcRenderer.invoke('hermes:stop-find-in-page'), onFoundInPage: callback => { const listener = (_event, result) => callback(result) ipcRenderer.on('hermes:found-in-page', listener) return () => ipcRenderer.removeListener('hermes:found-in-page', listener) }, // Main-process `before-input-event` forwards Ctrl/Cmd+F here so renderer // can open the FindBar even when the GTK compositor has already grabbed // the chord at the windowing layer (#81727). onOpenFindBarRequested: callback => { const listener = () => callback() ipcRenderer.on('hermes:open-find-bar', listener) return () => ipcRenderer.removeListener('hermes:open-find-bar', listener) } })