mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 08:32:09 +00:00
* feat(desktop): per-profile remote gateway hosts Profile switching silently failed whenever the desktop was connected to a remote backend: the rail routed non-active profiles to a local pool backend, but spawnPoolBackend hard-threw "Profiles are unavailable when connected to a remote Hermes backend", and the renderer swallowed the error into an infinite reconnect backoff while still marking the profile active. Remote was also a single app-global setting, so there was no way to give a profile its own host. Add per-profile remote hosts so each profile can point at its own backend: - connection.json gains a validated `profiles` map; profileRemoteOverride() (pure, unit-tested) selects an explicit per-profile remote. - resolveRemoteBackend(profile) precedence: per-profile override → env override → global remote → local spawn. spawnPoolBackend now connects to a profile's remote (no local child) instead of throwing; startHermes resolves the primary profile's remote. - coerce/sanitize connection config are scope-aware (global vs named profile) and preserve each other's entries; IPC get/save/apply/test thread an optional profile. Per-profile apply drops only that profile's pool backend. - Settings → Gateway adds an "Applies to" scope selector reusing the existing URL/token/OAuth/test UX per profile. Tests: connection-config pure suite (+6) and desktop platform suite pass; tsc/eslint/vitest clean. * refactor(desktop): DRY per-profile remote helpers Share connectionScopeKey + normAuthMode from connection-config.cjs (drop the main.cjs copy), collapse the scope/auth ternaries, route the env remote through buildRemoteConnection, and fold the duplicated remote-block validation into buildRemoteBlock. No behavior change; pure suite + live E2E still green.
131 lines
7.1 KiB
JavaScript
131 lines
7.1 KiB
JavaScript
const { contextBridge, ipcRenderer, webUtils } = require('electron')
|
|
|
|
contextBridge.exposeInMainWorld('hermesDesktop', {
|
|
getConnection: profile => ipcRenderer.invoke('hermes:connection', profile),
|
|
touchBackend: profile => ipcRenderer.invoke('hermes:backend:touch', profile),
|
|
getGatewayWsUrl: profile => ipcRenderer.invoke('hermes:gateway:ws-url', profile),
|
|
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),
|
|
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),
|
|
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'),
|
|
readFileDataUrl: filePath => ipcRenderer.invoke('hermes:readFileDataUrl', filePath),
|
|
readFileText: filePath => ipcRenderer.invoke('hermes:readFileText', filePath),
|
|
selectPaths: options => ipcRenderer.invoke('hermes:selectPaths', options),
|
|
writeClipboard: text => ipcRenderer.invoke('hermes:writeClipboard', text),
|
|
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),
|
|
stopPreviewFileWatch: id => ipcRenderer.invoke('hermes:stopPreviewFileWatch', id),
|
|
setTitleBarTheme: payload => ipcRenderer.send('hermes:titlebar-theme', payload),
|
|
setPreviewShortcutActive: active => ipcRenderer.send('hermes:previewShortcutActive', Boolean(active)),
|
|
openExternal: url => ipcRenderer.invoke('hermes:openExternal', url),
|
|
fetchLinkTitle: url => ipcRenderer.invoke('hermes:fetchLinkTitle', url),
|
|
settings: {
|
|
getDefaultProjectDir: () => ipcRenderer.invoke('hermes:setting:defaultProjectDir:get'),
|
|
setDefaultProjectDir: dir => ipcRenderer.invoke('hermes:setting:defaultProjectDir:set', dir),
|
|
pickDefaultProjectDir: () => ipcRenderer.invoke('hermes:setting:defaultProjectDir:pick')
|
|
},
|
|
revealLogs: () => ipcRenderer.invoke('hermes:logs:reveal'),
|
|
getRecentLogs: () => ipcRenderer.invoke('hermes:logs:recent'),
|
|
readDir: dirPath => ipcRenderer.invoke('hermes:fs:readDir', dirPath),
|
|
gitRoot: startPath => ipcRenderer.invoke('hermes:fs:gitRoot', startPath),
|
|
terminal: {
|
|
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)
|
|
},
|
|
onOpenUpdatesRequested: callback => {
|
|
const listener = () => callback()
|
|
ipcRenderer.on('hermes:open-updates', listener)
|
|
return () => ipcRenderer.removeListener('hermes:open-updates', listener)
|
|
},
|
|
onWindowStateChanged: callback => {
|
|
const listener = (_event, payload) => callback(payload)
|
|
ipcRenderer.on('hermes:window-state-changed', listener)
|
|
return () => ipcRenderer.removeListener('hermes:window-state-changed', 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)
|
|
},
|
|
onPowerResume: callback => {
|
|
const listener = () => callback()
|
|
ipcRenderer.on('hermes:power-resume', listener)
|
|
return () => ipcRenderer.removeListener('hermes:power-resume', 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.cjs (apps/desktop/electron/bootstrap-runner.cjs).
|
|
// 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'),
|
|
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'),
|
|
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)
|
|
}
|
|
}
|
|
})
|