28c729e68f
Removing a connection stopped its pooled backends and ssh tunnels but never told the renderer: for remote/cloud sources there is no local process to die, so the removed connection's WebSocket stayed open and kept streaming ghost events into the UI until page reload. If the socket did drop, openSecondary -> getConnectionFor threw 'No connection with id' and scheduleReconnect retried forever (backoff caps at 15s, entry never evicted). - main now broadcasts 'hermes:connections:changed' on removal (and on material edits); preload exposes connections.onChanged. - use-gateway-boot subscribes and calls the new disposeSecondariesForConnection(), which disposes + evicts every secondary scoped to the connection id (redialing on edits). - reconnectSecondary fail-stops: when the Electron main reports the connection no longer exists, the entry is disposed and evicted instead of retrying forever; ordinary transport errors keep the existing backoff behavior.
438 lines
24 KiB
TypeScript
438 lines
24 KiB
TypeScript
import { contextBridge, ipcRenderer, webUtils } from 'electron'
|
|
|
|
contextBridge.exposeInMainWorld('hermesDesktop', {
|
|
getConnection: profile => ipcRenderer.invoke('hermes:connection', profile),
|
|
// Registry-scoped backend resolution: { connectionId, profile } → descriptor.
|
|
getConnectionFor: payload => ipcRenderer.invoke('hermes:connection:for', payload),
|
|
revalidateConnection: () => ipcRenderer.invoke('hermes:connection:revalidate'),
|
|
touchBackend: profile => ipcRenderer.invoke('hermes:backend:touch', profile),
|
|
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'),
|
|
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: {
|
|
open: request => ipcRenderer.invoke('hermes:hud:open', request),
|
|
close: () => ipcRenderer.invoke('hermes:hud:close'),
|
|
setIgnoreMouse: ignore => ipcRenderer.send('hermes:hud:ignore-mouse', ignore),
|
|
moveBy: delta => ipcRenderer.send('hermes:hud:move-by', delta),
|
|
setBounds: bounds => ipcRenderer.send('hermes:hud:set-bounds', bounds),
|
|
setVibrancy: on => ipcRenderer.invoke('hermes:hud:vibrancy', on),
|
|
// 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)
|
|
}
|
|
},
|
|
// 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),
|
|
// 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),
|
|
test: id => ipcRenderer.invoke('hermes:connections:test', id),
|
|
// Fan out `hermes update` to every eligible registered connection.
|
|
updateAll: () => ipcRenderer.invoke('hermes:connections:update-all'),
|
|
// 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'),
|
|
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),
|
|
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),
|
|
saveImageBuffer: (data, ext) => ipcRenderer.invoke('hermes:saveImageBuffer', { data, ext }),
|
|
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),
|
|
openPreviewInBrowser: url => ipcRenderer.invoke('hermes:openPreviewInBrowser', url),
|
|
fetchLinkTitle: url => ipcRenderer.invoke('hermes:fetchLinkTitle', 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'),
|
|
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'),
|
|
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: {
|
|
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)
|
|
},
|
|
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'),
|
|
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)
|
|
},
|
|
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'),
|
|
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'),
|
|
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)
|
|
}
|
|
})
|