1450 lines
59 KiB
TypeScript
1450 lines
59 KiB
TypeScript
import type { GatewayWsUrlResult } from '@hermes/shared'
|
|
import type { TranslucencyState } from '@hermes/shared/translucency'
|
|
|
|
import type { PoolLimits } from '../electron/pool-limits'
|
|
|
|
import type { WakeIndicatorState } from './lib/wake-indicator'
|
|
import type {
|
|
PetOverlayBounds,
|
|
PetOverlayControl,
|
|
PetOverlayOpenRequest,
|
|
PetOverlayStatePayload
|
|
} from './store/pet-overlay'
|
|
import type { QuickEntryStatePush, QuickEntryStatus, QuickEntrySubmitPayload } from './store/quick-entry'
|
|
|
|
export {}
|
|
|
|
declare global {
|
|
interface Window {
|
|
hermesDesktop: {
|
|
setUiLanguage?: (locale: string) => void
|
|
// Resolve a backend connection. Omit `profile` (or pass the primary) for
|
|
// the window's backend; pass a named profile to lazily spawn/reuse that
|
|
// profile's backend from the pool.
|
|
getConnection: (profile?: string | null) => Promise<HermesConnection>
|
|
// Registry-scoped backend resolution: dial (connectionId, profile). An
|
|
// empty/local connectionId delegates to the legacy getConnection path.
|
|
getConnectionFor?: (payload: {
|
|
connectionId?: null | string
|
|
profile?: null | string
|
|
}) => Promise<HermesConnection>
|
|
// Registry-scoped fresh WS URL (same result contract as getGatewayWsUrl).
|
|
getGatewayWsUrlFor?: (payload: {
|
|
connectionId?: null | string
|
|
profile?: null | string
|
|
}) => Promise<GatewayWsUrlResult>
|
|
// Union agent roster across every registered connection.
|
|
getAgentRoster?: () => Promise<DesktopAgentRoster>
|
|
// Credential-free routes across the union connection registry. The
|
|
// optional profile list is used only by the single-local v1 fallback;
|
|
// endpoint and auth material never crosses the IPC boundary.
|
|
getProfileRoutes: (profiles: string[]) => Promise<DesktopPluginProfileRoute[]>
|
|
// Reconnect-after-wake recovery: liveness-probe the cached PRIMARY backend
|
|
// and drop it if a remote one has gone unreachable, so the next
|
|
// getConnection() rebuilds a reachable descriptor instead of the renderer
|
|
// re-dialing a dead remote forever. No-op for local backends (they
|
|
// self-heal via the child 'exit' handler). `rebuilt` is true when a stale
|
|
// remote cache was dropped.
|
|
revalidateConnection: () => Promise<{ ok: boolean; rebuilt: boolean }>
|
|
// Keepalive: mark a pool profile backend as recently used so the idle
|
|
// reaper spares it while its chat is active.
|
|
touchBackend: (profile?: string | null) => Promise<{ ok: boolean }>
|
|
// Pool sizing (Settings → Advanced): device-local, live-applied by the
|
|
// main process. get resolves the limits currently in force; set applies
|
|
// (and persists) new ones, evicting/reaping to converge immediately.
|
|
getPoolLimits: () => Promise<PoolLimits>
|
|
setPoolLimits: (limits: { maxBackends?: number; idleMs?: number }) => Promise<{
|
|
ok: boolean
|
|
limits: PoolLimits
|
|
}>
|
|
getGatewayWsUrl: (profile?: null | string) => Promise<GatewayWsUrlResult>
|
|
// Open (or focus) a standalone OS window for a single chat session so
|
|
// the user can work with multiple chats side by side. Returns ok:false
|
|
// with an error code when the sessionId is empty/invalid. `watch` opens
|
|
// a spectator window (lazy resume — no agent build) for live-streaming
|
|
// a running subagent's session.
|
|
openSessionWindow: (
|
|
sessionId: string,
|
|
opts?: { profile?: null | string; watch?: boolean }
|
|
) => Promise<{ ok: boolean; error?: string }>
|
|
// Resume this session in the user's own terminal emulator (`hermes --tui
|
|
// --resume <id>`) — the external terminal, not the in-app pane.
|
|
openSessionInTerminal: (
|
|
sessionId: string,
|
|
opts?: { cwd?: string; profile?: string }
|
|
) => Promise<{ ok: boolean; error?: string }>
|
|
// Open a new full-chrome app window — a peer instance of the primary that
|
|
// renders the complete app against the shared backend, so the user can run
|
|
// multiple GUI windows at once.
|
|
openWindow: () => Promise<{ ok: boolean; error?: string }>
|
|
// Pop the in-app Browser (webview + address bar) into its own OS window.
|
|
// `tabId` is the `$previewTabs` id; closing the window fires
|
|
// `onBrowserPopoutClosed` so the caller can dock the tab again.
|
|
openBrowserWindow: (tabId: string) => Promise<{ ok: boolean; error?: string }>
|
|
onBrowserPopoutClosed: (callback: (tabId: string) => void) => () => void
|
|
// Claim a one-shot cross-window ambient cue (turn-end sound / spoken
|
|
// reply). Resolves true for the first window to claim a key, false for
|
|
// peers — so N open windows don't all fire the same cue.
|
|
claimAmbientCue: (key: string) => Promise<boolean>
|
|
wakeIndicator?: {
|
|
getState: () => Promise<WakeIndicatorState>
|
|
setState: (state: WakeIndicatorState) => void
|
|
onState: (callback: (state: WakeIndicatorState) => void) => () => void
|
|
}
|
|
// The pop-out pet overlay: a transparent always-on-top window hosting only
|
|
// the mascot. The main renderer drives it (open/close/drag + state push);
|
|
// the overlay sends control messages back (pop-in, composer submit).
|
|
petOverlay: {
|
|
open: (request: PetOverlayOpenRequest) => Promise<{ ok: boolean; bounds?: PetOverlayBounds }>
|
|
close: () => Promise<{ ok: boolean }>
|
|
setBounds: (bounds: PetOverlayBounds) => void
|
|
setIgnoreMouse: (ignore: boolean) => void
|
|
setFocusable: (focusable: boolean) => void
|
|
pushState: (payload: PetOverlayStatePayload) => void
|
|
control: (payload: PetOverlayControl) => void
|
|
onState: (callback: (payload: PetOverlayStatePayload) => void) => () => void
|
|
onControl: (callback: (payload: PetOverlayControl) => void) => () => void
|
|
}
|
|
// HUD mode: the chrome-free floating chat. A FULL app renderer with its
|
|
// own gateway (like an instance window), sized and skinned as a floating
|
|
// bar — so it mounts the real composer rather than a lookalike. Main
|
|
// owns the window; `onChanged` keeps every window's toggle truthful.
|
|
hud?: {
|
|
nativeDrag: boolean
|
|
windowing?: {
|
|
clientPlacement: boolean
|
|
controlDrag: boolean
|
|
nativeDrag: boolean
|
|
solid: boolean
|
|
workspaceTransfer: boolean
|
|
}
|
|
open: (request?: { sessionId?: null | string; profile?: null | string }) => Promise<{ ok: boolean }>
|
|
close: () => Promise<{ ok: boolean }>
|
|
setIgnoreMouse: (ignore: boolean) => void
|
|
beginMove: () => void
|
|
endMove: () => void
|
|
moveBy: (delta: { width: number; height: number }) => void
|
|
setWorkspaceTransfer?: (transferring: boolean) => void
|
|
setBounds: (bounds: { x: number; y: number; width: number; height: number }) => void
|
|
resetLayout: () => Promise<{ ok: boolean }>
|
|
setFrost: (showing: boolean) => Promise<{ ok: boolean }>
|
|
setSession: (sessionId: null | string) => void
|
|
onGoto: (callback: (sessionId: string) => void) => () => void
|
|
onChanged: (callback: (state: { open: boolean; sessionId: null | string }) => void) => () => void
|
|
onCursor: (callback: (point: { x: number; y: number } | null) => void) => () => void
|
|
onGameOverlay: (callback: (state: { active: boolean; app: string }) => void) => () => void
|
|
}
|
|
// Quick Entry: a global-hotkey mini composer window. Main owns the OS
|
|
// shortcut registration + the persisted preference (it must restore the
|
|
// shortcut on a cold launch without the renderer visiting Settings), so
|
|
// the renderer reads/writes it here and adopts the authoritative reply.
|
|
quickEntry: {
|
|
getSettings: () => Promise<QuickEntryStatus>
|
|
// Returns the resulting state — including `registered: false` +
|
|
// `error: 'taken'` when another app already owns the chord, so a failed
|
|
// registration surfaces in Settings instead of failing silently.
|
|
setSettings: (patch: { enabled?: boolean; shortcut?: string }) => Promise<QuickEntryStatus>
|
|
// Quick window → main: send this payload (main forwards it to the
|
|
// primary renderer, which routes it to the target session and submits
|
|
// through the normal prompt path) and hide.
|
|
submit: (payload: QuickEntrySubmitPayload) => void
|
|
// Quick window → main: hide without sending (Escape / blur).
|
|
dismiss: () => void
|
|
// Primary renderer → main → quick window: gateway connection state +
|
|
// the recent-session options. Main caches the latest push and replays
|
|
// it to a quick window spawned later.
|
|
pushState: (payload: QuickEntryStatePush) => void
|
|
// Quick window subscribes to those pushes.
|
|
onState: (callback: (payload: QuickEntryStatePush) => void) => () => void
|
|
// Primary renderer subscribes to submits captured by the quick window.
|
|
onSubmit: (callback: (payload: QuickEntrySubmitPayload | string) => void) => () => void
|
|
// Quick window subscribes to "you were just summoned" so it can reset
|
|
// its draft and re-focus the input on every open.
|
|
onShown: (callback: () => void) => () => void
|
|
}
|
|
getBootProgress: () => Promise<DesktopBootProgress>
|
|
getConnectionConfig: (profile?: null | string) => Promise<DesktopConnectionConfig>
|
|
saveConnectionConfig: (payload: DesktopConnectionConfigInput) => Promise<DesktopConnectionConfig>
|
|
applyConnectionConfig: (payload: DesktopConnectionConfigInput) => Promise<DesktopConnectionConfig>
|
|
testConnectionConfig: (payload: DesktopConnectionConfigInput) => Promise<DesktopConnectionTestResult>
|
|
// Opt-in OS-keychain encryption for stored gateway secrets (default
|
|
// off). `get` never touches the OS keychain; `set` re-encodes stored
|
|
// secrets and can throw when the keychain is unusable.
|
|
getSecretStorageEncryption: () => Promise<{ on: boolean }>
|
|
setSecretStorageEncryption: (on: boolean) => Promise<{ on: boolean }>
|
|
// v2 multi-connection registry: named agent sources, all persisted
|
|
// together (local + any number of remote/cloud/ssh instances).
|
|
connections: {
|
|
list: () => Promise<DesktopConnectionsRegistry>
|
|
save: (
|
|
payload: DesktopRegistryConnectionInput
|
|
) => Promise<{ ok: boolean; connection: DesktopRegistryConnection; registry: DesktopConnectionsRegistry }>
|
|
remove: (id: string) => Promise<{ ok: boolean; registry: DesktopConnectionsRegistry }>
|
|
setPrimary: (id: string) => Promise<{ ok: boolean; registry: DesktopConnectionsRegistry }>
|
|
setLaunchMode?: (
|
|
mode: 'last-used' | 'primary'
|
|
) => Promise<{ ok: boolean; registry: DesktopConnectionsRegistry }>
|
|
setLastUsed?: (id: string) => Promise<{ ok: boolean; registry: DesktopConnectionsRegistry }>
|
|
test: (id: string) => Promise<DesktopConnectionTestResult>
|
|
// Drain/update/restore one Desktop-managed SSH install. External URL
|
|
// and cloud sources are refused without touching their processes.
|
|
updateManaged?: (id: string) => Promise<DesktopManagedConnectionUpdateResult>
|
|
// Fan out `hermes update` to every eligible registered connection;
|
|
// cloud entries are skipped (platform-managed), each row independent.
|
|
// excludeIds skips connections the caller updates through another
|
|
// path (the everything-update flow's active backend + local client).
|
|
updateAll?: (options?: {
|
|
excludeIds?: string[]
|
|
}) => Promise<{ ok: boolean; results: DesktopConnectionUpdateResult[] }>
|
|
// Registry lifecycle push: fired when a connection is removed or
|
|
// materially edited so the renderer can dispose (and re-dial) the
|
|
// secondary gateways scoped to it. Optional: older Electron mains
|
|
// don't emit it.
|
|
onChanged?: (
|
|
callback: (payload: { connectionId: string; reason: 'removed' | 'saved' | 'updated' }) => void
|
|
) => () => void
|
|
}
|
|
sshConfigHosts: () => Promise<DesktopSshHostsResult>
|
|
sshResolveHost: (host: string) => Promise<DesktopSshResolveResult>
|
|
probeConnectionConfig: (remoteUrl: string) => Promise<DesktopConnectionProbeResult>
|
|
oauthLoginConnectionConfig: (remoteUrl: string) => Promise<DesktopOauthLoginResult>
|
|
oauthLogoutConnectionConfig: (remoteUrl: string) => Promise<DesktopOauthLogoutResult>
|
|
// Hermes Cloud: one portal login powers discovery + silent per-agent
|
|
// sign-in (cloud-auto-discovery Phase 3).
|
|
cloud: {
|
|
status: () => Promise<DesktopCloudStatus>
|
|
login: () => Promise<DesktopCloudStatus & { ok: boolean }>
|
|
logout: () => Promise<DesktopCloudStatus & { ok: boolean }>
|
|
discover: (org?: string) => Promise<DesktopCloudDiscoverResult>
|
|
agentSignIn: (dashboardUrl: string) => Promise<DesktopCloudAgentSignInResult>
|
|
}
|
|
profile: {
|
|
get: () => Promise<DesktopActiveProfile>
|
|
// Persists the profile used on the next Desktop launch without
|
|
// interrupting the live gateway workspace switch.
|
|
remember: (name: string | null) => Promise<DesktopActiveProfile>
|
|
// Persists the desktop's profile choice and relaunches the local
|
|
// backend under the new HERMES_HOME (reloads the window). Pass null to
|
|
// clear the preference.
|
|
set: (name: string | null) => Promise<DesktopActiveProfile>
|
|
}
|
|
api: <T>(request: HermesApiRequest) => Promise<T>
|
|
notify: (payload: HermesNotification) => Promise<boolean>
|
|
requestMicrophoneAccess: () => Promise<boolean>
|
|
/** read_window_below tool: metadata for the OS window directly underneath this one (never pixels). */
|
|
readWindowBelow?: () => Promise<{
|
|
frontmost: { app: string; title: string } | null
|
|
note?: string
|
|
platform: string
|
|
window: {
|
|
app: string
|
|
bounds: { height: number; width: number; x: number; y: number }
|
|
id: number
|
|
title: string
|
|
} | null
|
|
} | null>
|
|
readFileDataUrl: (filePath: string) => Promise<string>
|
|
/** Remote non-image attach: higher dedicated cap than preview/Settings default. */
|
|
readFileDataUrlForAttach?: (filePath: string) => Promise<string>
|
|
/** Settings → Chat: max size for local files loaded as data URLs (attach/preview). */
|
|
dataUrlReadMax?: {
|
|
get: () => Promise<{ defaultMaxMb: number; maxBytes: number; maxMb: number }>
|
|
set: (maxMb: number) => Promise<{ defaultMaxMb: number; maxBytes: number; maxMb: number }>
|
|
}
|
|
readFileText: (filePath: string) => Promise<HermesReadFileTextResult>
|
|
/** Full-source read for runtime desktop plugins (readFileText truncates
|
|
* at the 512 KiB preview cap). Absent on older shells — callers fall
|
|
* back to readFileText and must reject a `truncated` result. */
|
|
readPluginSource?: (filePath: string) => Promise<HermesReadFileTextResult>
|
|
selectPaths: (options?: HermesSelectPathsOptions) => Promise<string[]>
|
|
/** Native save dialog; returns the chosen path or null on cancel. */
|
|
selectSavePath?: (options?: {
|
|
defaultPath?: string
|
|
filters?: Array<{ extensions: string[]; name: string }>
|
|
title?: string
|
|
}) => Promise<null | string>
|
|
writeClipboard: (text: string) => Promise<boolean>
|
|
readClipboard: () => Promise<string>
|
|
saveGatewayFile?: (payload: {
|
|
connectionId?: null | string
|
|
path: string
|
|
profile?: null | string
|
|
suggestedName?: string
|
|
}) => Promise<{
|
|
canceled?: boolean
|
|
path?: string
|
|
saved: boolean
|
|
}>
|
|
saveImageFromUrl: (url: string) => Promise<boolean>
|
|
/** Edit verb against the window's focused element (the custom context
|
|
* menu's Cut/Copy/Paste/Select all). */
|
|
contextMenuEdit?: (command: 'copy' | 'cut' | 'paste' | 'selectAll') => Promise<void>
|
|
/** Copy the image under the LAST context-menu gesture (Chromium tracks
|
|
* its coordinates on the main-process context-menu event). */
|
|
contextMenuCopyImage?: () => Promise<void>
|
|
/** Replace the misspelled word or add it to the dictionary. */
|
|
contextMenuSpellcheck?: (action: { kind: 'add' | 'replace'; word: string }) => Promise<void>
|
|
/** Add a word to the spell-check dictionary of a webview guest's
|
|
* session (the tag exposes no session API). */
|
|
contextMenuGuestAddWord?: (payload: { webContentsId: number; word: string }) => Promise<void>
|
|
/** Spell-check facts for the gesture that opened the current menu;
|
|
* fires shortly after the DOM contextmenu event. */
|
|
onContextMenuSpellcheck?: (
|
|
callback: (payload: { misspelledWord: string; suggestions: string[] }) => void
|
|
) => () => void
|
|
saveImageBuffer: (data: ArrayBuffer | Uint8Array, ext: string, name?: string) => Promise<string>
|
|
/** Crop the in-app browser guest. `rect` is CSS pixels in the page viewport. */
|
|
capturePreview?: (payload: {
|
|
rect?: { height: number; width: number; x: number; y: number }
|
|
viewport?: { height: number; width: number }
|
|
webContentsId: number
|
|
}) => Promise<string>
|
|
saveClipboardImage: () => Promise<string>
|
|
getPathForFile: (file: File) => string
|
|
normalizePreviewTarget: (target: string, baseDir?: string) => Promise<HermesPreviewTarget | null>
|
|
watchPreviewFile: (url: string) => Promise<HermesPreviewWatch>
|
|
/** Watch a directory for entry churn (disk-plugin door); same watcher
|
|
* registry + onPreviewFileChanged channel as watchPreviewFile. Optional:
|
|
* older Electron shells predate it and fall back to the readdir poll. */
|
|
watchDirectory?: (dir: string) => Promise<HermesPreviewWatch>
|
|
stopPreviewFileWatch: (id: string) => Promise<boolean>
|
|
setActiveWork?: (payload: HermesActiveWork) => void
|
|
setTitleBarTheme?: (payload: HermesTitleBarTheme) => void
|
|
setNativeTheme?: (mode: 'dark' | 'light' | 'system') => void
|
|
/** Main-process fact: this OS can back glass with a native material. */
|
|
glassSupported?: boolean
|
|
/** Main-process fact: this OS can do any translucency at all (not Linux). */
|
|
translucencySupported?: boolean
|
|
/** Launch flag: the app was started with --local, enabling the
|
|
* local-models GUI surfaces. Absent/false = every local surface hides. */
|
|
localModelsEnabled?: boolean
|
|
setTranslucency?: (payload: TranslucencyState) => void
|
|
setKeepAwake?: (on: boolean) => void
|
|
setDisableF12?: (blocked: boolean) => void
|
|
setPreviewShortcutActive?: (active: boolean) => void
|
|
openExternal: (url: string) => Promise<void>
|
|
/** One-shot loopback callback listener for MCP OAuth against remote
|
|
* backends (electron/mcp-oauth-callback-ipc.ts): bind on THIS machine,
|
|
* pass redirectUri as client_redirect_uri to mcp.servers.oauth.start,
|
|
* await the provider redirect, relay code/state via oauth.callback. */
|
|
mcpOauth?: {
|
|
listen: () => Promise<{ id: string; redirectUri: string }>
|
|
wait: (
|
|
id: string,
|
|
timeoutMs?: number
|
|
) => Promise<{ code: null | string; error: null | string; state: null | string }>
|
|
cancel: (id: string) => Promise<boolean>
|
|
}
|
|
openPreviewInBrowser?: (url: string) => Promise<void>
|
|
fetchLinkTitle: (url: string) => Promise<string>
|
|
/** A site's icon as a data URL, or '' when it has none we can read.
|
|
* Resolved and cached in the main process (electron/favicon.ts). */
|
|
resolveFavicon?: (url: string) => Promise<string>
|
|
sanitizeWorkspaceCwd: (cwd?: null | string) => Promise<{ cwd: string; sanitized: boolean }>
|
|
settings: {
|
|
getDefaultProjectDir: () => Promise<{ defaultLabel: string; dir: null | string; resolvedCwd: string }>
|
|
pickDefaultProjectDir: () => Promise<{ canceled: boolean; dir: null | string }>
|
|
setDefaultProjectDir: (dir: null | string) => Promise<{ dir: null | string }>
|
|
}
|
|
zoom?: {
|
|
get: () => Promise<{ level: number; percent: number }>
|
|
/** Synchronous zoom factor of this window (1 = 100%). */
|
|
factor?: () => number
|
|
setPercent: (percent: number) => void
|
|
onChanged: (callback: (payload: { level: number; percent: number }) => void) => () => void
|
|
}
|
|
revealLogs: () => Promise<{ ok: boolean; path: string; error?: string }>
|
|
getRecentLogs: () => Promise<{ path: string; lines: string[] }>
|
|
/** Persist a renderer error-boundary catch to desktop.log (fire-and-forget). */
|
|
reportRendererError?: (report: {
|
|
label: string
|
|
boundary: string
|
|
message: string
|
|
componentStack: string
|
|
}) => void
|
|
readDir: (path: string) => Promise<HermesReadDirResult>
|
|
gitRoot?: (path: string) => Promise<string | null>
|
|
// Reveal a path in the OS file manager (Finder / Explorer).
|
|
revealPath?: (path: string) => Promise<boolean>
|
|
// Open a DIRECTORY (created if missing) in the OS file manager.
|
|
openDir?: (path: string) => Promise<{ ok: boolean; error?: string }>
|
|
// Local Desktop runtime-plugin root (<HERMES_HOME>/desktop-plugins),
|
|
// resolved by Electron independently of the connected backend (#66899).
|
|
// Created on demand; returns the normalized absolute path.
|
|
desktopPluginsRoot?: () => Promise<string>
|
|
/** LOCAL `<HERMES_HOME>/logs` (profile-aware) — error card "Open Logs". */
|
|
logsRoot?: () => Promise<string>
|
|
// Local AGENT-plugin root (<HERMES_HOME>/plugins), same Electron-local
|
|
// resolution. The disk door also scans it for `<name>/desktop/plugin.js`
|
|
// so one agent-plugin package can ship a desktop UI half. Optional:
|
|
// older Electron shells predate it — the scanner then skips this root.
|
|
agentPluginsRoot?: () => Promise<string>
|
|
// Rename a file/folder in place (new base name, same parent dir).
|
|
renamePath?: (path: string, newName: string) => Promise<{ path: string }>
|
|
// Write a small UTF-8 text file (hardened path, parent must exist).
|
|
writeTextFile?: (path: string, content: string) => Promise<{ path: string }>
|
|
// Move a file/folder to the OS trash (recoverable).
|
|
trashPath?: (path: string) => Promise<boolean>
|
|
// Git-driven worktree management for the "Start work" flow.
|
|
git?: {
|
|
worktreeList: (repoPath: string) => Promise<HermesGitWorktree[]>
|
|
worktreeAdd: (
|
|
repoPath: string,
|
|
options?: { name?: string; branch?: string; base?: string; existingBranch?: string }
|
|
) => Promise<{ path: string; branch: string; repoRoot: string }>
|
|
worktreeRemove: (
|
|
repoPath: string,
|
|
worktreePath: string,
|
|
options?: { force?: boolean }
|
|
) => Promise<{ removed: string }>
|
|
branchSwitch: (repoPath: string, branch: string) => Promise<{ branch: string }>
|
|
// The local branches, plus the remote-tracking refs that have no local
|
|
// branch, for the "convert a branch into a worktree" picker.
|
|
branchList: (repoPath: string) => Promise<HermesGitBranch[]>
|
|
// Local + remote-tracking branches for the "base branch" picker in the
|
|
// new-worktree dialog. The remote default (origin/HEAD) is flagged so
|
|
// the UI can preselect it.
|
|
baseBranchList: (repoPath: string) => Promise<HermesGitBaseBranch[]>
|
|
// Compact working-tree status for the composer coding rail. Null on a
|
|
// non-repo / remote backend (where the Electron probe can't run).
|
|
repoStatus: (repoPath: string) => Promise<HermesRepoStatus | null>
|
|
// Working-tree-vs-HEAD unified diff for one file (the preview's diff
|
|
// view). Empty string when the file is unchanged or not in a repo.
|
|
fileDiff: (repoPath: string, filePath: string) => Promise<string>
|
|
// Codex-style review pane: changed files per scope, per-file diff, and
|
|
// stage / unstage / revert.
|
|
review: {
|
|
list: (repoPath: string, scope: HermesReviewScope, baseRef?: null | string) => Promise<HermesReviewList>
|
|
diff: (
|
|
repoPath: string,
|
|
filePath: string,
|
|
scope: HermesReviewScope,
|
|
baseRef?: null | string,
|
|
staged?: boolean
|
|
) => Promise<string>
|
|
stage: (repoPath: string, filePath?: null | string) => Promise<{ ok: boolean }>
|
|
unstage: (repoPath: string, filePath?: null | string) => Promise<{ ok: boolean }>
|
|
revert: (repoPath: string, filePath?: null | string) => Promise<{ ok: boolean }>
|
|
revParse: (repoPath: string, ref?: null | string) => Promise<null | string>
|
|
commit: (repoPath: string, message: string, push: boolean) => Promise<{ ok: boolean }>
|
|
// Diff (staged-or-all) + recent commit subjects for drafting a
|
|
// commit message. Reads only; empty strings off-repo.
|
|
commitContext: (repoPath: string) => Promise<{ diff: string; recent: string }>
|
|
push: (repoPath: string) => Promise<{ ok: boolean }>
|
|
shipInfo: (repoPath: string) => Promise<HermesReviewShipInfo>
|
|
// The PR on each of the given branches — plus any known only by
|
|
// number — for badging a list of sessions in one request instead of
|
|
// one `pr view` per checkout.
|
|
prList: (repoPath: string, branches: string[], numbers?: number[]) => Promise<HermesRepoPullRequests>
|
|
// A pasted PR review/issue comment URL resolved to its structured
|
|
// context (author, body, file + line anchor, diff hunk). Null when
|
|
// gh can't answer — the paste stays a plain URL.
|
|
fetchPrComment: (repoPath: string, url: string) => Promise<HermesPrComment | null>
|
|
createPr: (repoPath: string) => Promise<{ url: string }>
|
|
}
|
|
// Repo-first discovery: scan bounded roots for git repos (depth-capped).
|
|
scanRepos: (
|
|
roots: string[],
|
|
options?: { maxDepth?: number; enabled?: boolean; excludePaths?: string[] }
|
|
) => Promise<{ root: string; label: string }[]>
|
|
}
|
|
terminal: {
|
|
attach: (id: string) => Promise<boolean>
|
|
/** Best-effort current working directory of the live PTY child (POSIX
|
|
* only; null on Windows or when unavailable). Used to reopen a tab
|
|
* where the user last `cd`'d. */
|
|
cwd: (id: string) => Promise<string | null>
|
|
dispose: (id: string) => Promise<boolean>
|
|
onData: (id: string, callback: (payload: string) => void) => () => void
|
|
onExit: (id: string, callback: (payload: HermesTerminalExit) => void) => () => void
|
|
resize: (id: string, size: { cols: number; rows: number }) => Promise<boolean>
|
|
start: (options?: { cols?: number; cwd?: string; rows?: number }) => Promise<HermesTerminalSession>
|
|
write: (id: string, data: string) => Promise<boolean>
|
|
}
|
|
reachPreviewUrl?: (url: string) => Promise<string>
|
|
setActiveConnectionRoute?: (
|
|
route: {
|
|
connectionId?: null | string
|
|
profile?: string
|
|
registryScoped?: boolean
|
|
} | null
|
|
) => void
|
|
onClosePreviewRequested?: (callback: () => void) => () => void
|
|
onPreviewNav?: (callback: (command: 'back' | 'forward' | 'reload') => void) => () => void
|
|
onOpenFolderRequested?: (callback: () => void) => () => void
|
|
onOpenUpdatesRequested?: (callback: () => void) => () => void
|
|
onDeepLink?: (
|
|
callback: (payload: { kind: string; name: string; params: Record<string, string> }) => void
|
|
) => () => void
|
|
signalDeepLinkReady?: () => Promise<{ ok: boolean }>
|
|
probePluginRepo?: (payload: { identifier?: string; repo?: string }) => Promise<{
|
|
ok: boolean
|
|
agent: boolean
|
|
desktop: boolean
|
|
agentName?: string | null
|
|
desktopName?: string | null
|
|
warnings?: string[]
|
|
insecure?: boolean
|
|
error?: string
|
|
}>
|
|
installDesktopPlugin?: (payload: {
|
|
identifier?: string
|
|
repo?: string
|
|
force?: boolean
|
|
}) => Promise<{ ok: boolean; pluginName?: string; path?: string; error?: string }>
|
|
onWindowStateChanged?: (callback: (payload: HermesWindowState) => void) => () => void
|
|
onFocusSession?: (callback: (sessionId: string) => void) => () => void
|
|
onNotificationAction?: (callback: (payload: { actionId: string; sessionId?: string }) => void) => () => void
|
|
/** Plugin (and other session-less) notification body/action activation. */
|
|
onNotificationActivate?: (
|
|
callback: (payload: { actionId?: string; activate?: string; notifyId?: string; tag?: string }) => void
|
|
) => () => void
|
|
onPreviewFileChanged: (callback: (payload: HermesPreviewFileChanged) => void) => () => void
|
|
onBackendExit: (callback: (payload: BackendExit) => void) => () => void
|
|
// Soft gateway-mode apply: primary backend was torn down without a window
|
|
// reload. Wipe session lists (skeletons) and re-dial.
|
|
onConnectionApplied?: (callback: () => void) => () => void
|
|
onPowerResume?: (callback: () => void) => () => void
|
|
getOnBattery?: () => Promise<boolean>
|
|
onBatteryChanged?: (callback: (onBattery: boolean) => void) => () => void
|
|
onBootProgress: (callback: (payload: DesktopBootProgress) => void) => () => void
|
|
getBootstrapState: () => Promise<DesktopBootstrapState>
|
|
continueBootstrapLocal: () => Promise<{ ok: boolean }>
|
|
recycleBackend?: (profile?: null | string) => Promise<{ ok: boolean }>
|
|
resetBootstrap: () => Promise<{ ok: boolean }>
|
|
repairBootstrap: () => Promise<{ ok: boolean }>
|
|
cancelBootstrap: () => Promise<{ ok: boolean; cancelled: boolean }>
|
|
onBootstrapEvent: (callback: (payload: DesktopBootstrapEvent) => void) => () => void
|
|
getVersion: () => Promise<DesktopVersionInfo>
|
|
/** Restart the app in place — loads the swapped bundle when bundleSwapPending. */
|
|
relaunchApp?: () => Promise<void>
|
|
getRemoteDisplayReason?: () => Promise<string | null>
|
|
updates: {
|
|
check: () => Promise<DesktopUpdateStatus>
|
|
apply: (opts?: DesktopUpdateApplyOptions) => Promise<DesktopUpdateApplyResult>
|
|
getBranch: () => Promise<{ branch: string }>
|
|
setBranch: (name: string) => Promise<{ branch: string }>
|
|
onProgress: (callback: (payload: DesktopUpdateProgress) => void) => () => void
|
|
}
|
|
uninstall: {
|
|
summary: () => Promise<DesktopUninstallSummary>
|
|
run: (mode: DesktopUninstallMode) => Promise<DesktopUninstallResult>
|
|
}
|
|
themes: {
|
|
// Download a VS Code Marketplace extension and return the raw color
|
|
// theme files it contributes. The renderer converts + persists them.
|
|
fetchMarketplace: (id: string) => Promise<DesktopMarketplaceThemeResult>
|
|
// Search the Marketplace for color-theme extensions. An empty query
|
|
// returns the most-installed themes.
|
|
searchMarketplace: (query: string) => Promise<DesktopMarketplaceSearchItem[]>
|
|
}
|
|
// Find-in-page: delegates to Electron's webContents.findInPage on the
|
|
// IPC sender's window so Cmd+F from 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: string, options?: { forward?: boolean; findNext?: boolean }) => Promise<{ count: number }>
|
|
stopFindInPage: () => Promise<void>
|
|
onFoundInPage: (callback: (result: { activeMatchOrdinal: number; count: number }) => void) => () => void
|
|
// Main-process `before-input-event` forwards Ctrl/Cmd+F here so the
|
|
// renderer can still open the FindBar when the OS compositor has
|
|
// already grabbed the chord (#81727, e.g. Pop!_OS / GNOME).
|
|
onOpenFindBarRequested: (callback: () => void) => () => void
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface DesktopMarketplaceSearchItem {
|
|
extensionId: string
|
|
displayName: string
|
|
publisher: string
|
|
description: string
|
|
installs: number
|
|
}
|
|
|
|
export interface DesktopMarketplaceThemeFile {
|
|
label: string
|
|
/** VS Code's `uiTheme` for this entry (vs-dark / vs / hc-black). */
|
|
uiTheme?: string
|
|
/** Raw theme JSON (JSONC) text, parsed + converted by the renderer. */
|
|
contents: string
|
|
}
|
|
|
|
export interface DesktopMarketplaceThemeResult {
|
|
extensionId: string
|
|
displayName: string
|
|
themes: DesktopMarketplaceThemeFile[]
|
|
}
|
|
|
|
export interface HermesTerminalSession {
|
|
cwd: string
|
|
id: string
|
|
shell: string
|
|
}
|
|
|
|
export interface HermesTerminalExit {
|
|
code: number | null
|
|
signal: string | null
|
|
}
|
|
|
|
export interface DesktopVersionInfo {
|
|
appVersion: string
|
|
electronVersion: string
|
|
nodeVersion: string
|
|
platform: string
|
|
hermesRoot: string
|
|
/** True when the running renderer bundle predates desktop changes in the
|
|
* installed source tree (runtime updated, app binary not rebuilt/swapped). */
|
|
bundleOutOfSync?: boolean
|
|
/** Commits under apps/desktop/ the running bundle is missing (null unknown). */
|
|
bundleCommitsBehind?: null | number
|
|
/** True when the bundle on disk is newer than the running process — a plain
|
|
* app restart (no rebuild, no installer) is enough to load it. */
|
|
bundleSwapPending?: boolean
|
|
}
|
|
|
|
export type DesktopUninstallMode = 'full' | 'gui' | 'lite'
|
|
|
|
export interface DesktopUninstallSummary {
|
|
hermes_home: string
|
|
agent_installed: boolean
|
|
gui_installed: boolean
|
|
source_built_artifacts: string[]
|
|
packaged_app_paths: string[]
|
|
userdata_dir: string
|
|
userdata_exists: boolean
|
|
platform: string
|
|
running_app_path?: null | string
|
|
probe?: string
|
|
}
|
|
|
|
export interface DesktopUninstallResult {
|
|
ok: boolean
|
|
mode?: DesktopUninstallMode
|
|
willRemoveAppBundle?: boolean
|
|
scriptPath?: string
|
|
error?: string
|
|
message?: string
|
|
}
|
|
|
|
export interface DesktopUpdateCommit {
|
|
sha: string
|
|
summary: string
|
|
author: string
|
|
at: number
|
|
}
|
|
|
|
export interface DesktopUpdateStatus {
|
|
supported: boolean
|
|
updateAvailable?: boolean
|
|
branch?: string
|
|
currentBranch?: string
|
|
reason?: string
|
|
message?: string
|
|
error?: string
|
|
/** Exact commits behind. null = update available, but the count is
|
|
* unknowable (shallow clone without a merge-base) — never render it as a
|
|
* literal number. */
|
|
behind?: number | null
|
|
currentSha?: string
|
|
/** Backend only: the version string the backend reports for itself. */
|
|
currentVersion?: string
|
|
targetSha?: string
|
|
commits?: DesktopUpdateCommit[]
|
|
dirty?: boolean
|
|
fetchedAt?: number
|
|
}
|
|
|
|
export type DesktopUpdateDirtyStrategy = 'abort' | 'stash' | 'force'
|
|
|
|
export interface DesktopUpdateBlocker {
|
|
pid: number
|
|
name: string
|
|
cmdline: string
|
|
kind: 'local-preview' | 'other'
|
|
safeToStop: boolean
|
|
label?: string
|
|
port?: number
|
|
createTime?: number
|
|
}
|
|
|
|
export interface DesktopUpdateApplyOptions {
|
|
dirtyStrategy?: DesktopUpdateDirtyStrategy
|
|
/** User confirmed that Desktop may stop freshly re-scanned safe local preview servers. */
|
|
stopSafeBlockers?: boolean
|
|
}
|
|
|
|
export interface DesktopUpdateApplyResult {
|
|
ok: boolean
|
|
branch?: string
|
|
error?: string
|
|
message?: string
|
|
blockers?: DesktopUpdateBlocker[]
|
|
/** True when no staged updater exists (CLI install) and the user should run
|
|
* `hermes update` themselves. `command` is the exact line to run. */
|
|
manual?: boolean
|
|
command?: string
|
|
hermesRoot?: string
|
|
/** True when the backend was updated but the GUI couldn't be relaunched in
|
|
* place (AppImage / dev run): the new version loads on next launch. */
|
|
backendUpdated?: boolean
|
|
/** False when the running GUI package was NOT replaced by this update
|
|
* (Linux GUI/backend skew, or a sandbox-blocked relaunch). Distinguishes
|
|
* "backend only" outcomes from a real in-place GUI relaunch. (#45205) */
|
|
guiUpdated?: boolean
|
|
/** True for the Linux GUI/backend-skew terminal state: backend updated but
|
|
* the running AppImage/.deb/.rpm shell is unchanged and must be
|
|
* reinstalled. Renders a closeable "update the desktop app" message. */
|
|
guiSkew?: boolean
|
|
/** True when the update finished but the app must be quit + reopened by hand
|
|
* (e.g. the rebuilt sandbox helper isn't launchable): keep a working
|
|
* window, don't auto-quit into a dead app. (#45205) */
|
|
manualRestart?: boolean
|
|
/** True when the auto-relaunch was skipped specifically because the rebuilt
|
|
* chrome-sandbox helper is not launchable (not root:root + setuid). */
|
|
sandboxBlocked?: boolean
|
|
/** True when a detached relauncher took over (macOS bundle swap / Linux
|
|
* re-exec): the app is about to quit and reopen itself. */
|
|
handedOff?: boolean
|
|
}
|
|
|
|
export type DesktopUpdateStage =
|
|
| 'idle'
|
|
| 'prepare'
|
|
| 'fetch'
|
|
| 'pull'
|
|
| 'pydeps'
|
|
| 'update'
|
|
| 'rebuild'
|
|
| 'restart'
|
|
| 'done'
|
|
| 'manual'
|
|
/** Backend updated but the running GUI package (AppImage/.deb/.rpm) was NOT
|
|
* changed — the user must update/reinstall the desktop app. Terminal,
|
|
* closeable; never claims the GUI was updated. (#45205) */
|
|
| 'guiSkew'
|
|
| 'error'
|
|
|
|
export interface DesktopUpdateProgress {
|
|
stage: DesktopUpdateStage
|
|
message: string
|
|
percent: number | null
|
|
error: string | null
|
|
at: number
|
|
}
|
|
|
|
export interface DesktopPluginProfileRoute {
|
|
// Registry source identity. Pair with profile; profile names are not unique
|
|
// across sources.
|
|
connectionId: string
|
|
mode: 'local' | 'remote'
|
|
profile: string
|
|
targetProfile: string
|
|
}
|
|
|
|
export interface HermesConnection {
|
|
baseUrl: string
|
|
darwinMajor?: number
|
|
isFullscreen: boolean
|
|
// The live, RESOLVED connection mode. Only ever 'local' or 'remote' — a
|
|
// 'cloud' saved-config entry resolves to a 'remote' connection under the hood
|
|
// (cloud-auto-discovery Q3/Q6), so this never carries 'cloud'.
|
|
mode?: 'local' | 'remote'
|
|
authMode?: 'oauth' | 'token'
|
|
remoteHost?: string
|
|
remoteIdentity?: string
|
|
remoteKind?: 'cloud' | 'ssh' | 'url'
|
|
remoteHermesVersion?: string
|
|
nativeOverlayWidth: number
|
|
source?: 'env' | 'local' | 'settings'
|
|
token: string
|
|
wsUrl: string
|
|
logs: string[]
|
|
// Set for pool (non-primary) backends so the renderer knows which profile a
|
|
// connection belongs to.
|
|
profile?: string
|
|
// The registry connection this descriptor resolves to. Registry-scoped
|
|
// secondaries carry it directly; legacy primary remotes preserve it from
|
|
// their selected stored route before dialing.
|
|
connectionId?: string
|
|
// True only when getConnectionFor explicitly resolved a v2 registry route.
|
|
// An inferred connectionId identifies the visible source but its v1 profile
|
|
// name may still be a client-side routing alias rather than a backend profile.
|
|
registryScoped?: boolean
|
|
// True only when `profile` is a request scope on the shared primary backend.
|
|
// A pooled backend also carries `profile`, so presence alone cannot identify
|
|
// the shared-primary routing case.
|
|
sharedPrimary?: boolean
|
|
// True when `profile` is a request scope on a SHARED registry remote/cloud
|
|
// backend (one host, many profiles) — the registry analogue of sharedPrimary.
|
|
sharedRemote?: boolean
|
|
windowButtonPosition: { x: number; y: number } | null
|
|
}
|
|
|
|
export interface HermesTitleBarTheme {
|
|
background: string
|
|
foreground: string
|
|
}
|
|
|
|
/** Turns in flight, so the main process can confirm before a quit kills them. */
|
|
export interface HermesActiveWork {
|
|
count: number
|
|
titles: string[]
|
|
}
|
|
|
|
export interface HermesWindowState {
|
|
darwinMajor?: number
|
|
isFullscreen: boolean
|
|
isMinimized?: boolean
|
|
isVisible?: boolean
|
|
nativeOverlayWidth: number
|
|
windowButtonPosition: { x: number; y: number } | null
|
|
}
|
|
|
|
export interface DesktopActiveProfile {
|
|
// The desktop's stored profile preference, or null when unset (legacy launch
|
|
// that defers to the sticky active_profile / default).
|
|
profile: string | null
|
|
}
|
|
|
|
export interface DesktopConnectionConfig {
|
|
envOverride: boolean
|
|
// The saved connection mode. 'cloud' is a Hermes Cloud connection: it carries
|
|
// a remote-shaped block (remoteUrl = the selected agent's dashboardUrl,
|
|
// remoteAuthMode 'oauth') but is remembered as cloud so settings reopens into
|
|
// the cloud picker. Resolution treats cloud exactly as remote
|
|
// (cloud-auto-discovery Q3/Q6).
|
|
mode: 'local' | 'remote' | 'cloud' | 'ssh'
|
|
// The profile this config describes, or null for the global/default
|
|
// connection. Per-profile entries let a profile point at its own backend.
|
|
profile: null | string
|
|
remoteAuthMode: 'oauth' | 'token'
|
|
remoteOauthConnected: boolean
|
|
remoteTokenPreview: string | null
|
|
remoteTokenSet: boolean
|
|
// Whether OS-keychain-backed encryption (Electron safeStorage) is currently
|
|
// available on this machine. When false, a persisted remote token can only be
|
|
// stored as plain text on disk (with an explicit opt-in).
|
|
secureTokenStorage: boolean
|
|
// Whether the currently-persisted remote token is stored with encoding
|
|
// 'plain' (i.e. plain text on disk in connection.json), which happens when
|
|
// the user opted in on a machine without secure storage.
|
|
remoteTokenPlainText: boolean
|
|
remoteUrl: string
|
|
// For a 'cloud' connection: the persisted Hermes Cloud org (slug or id) the
|
|
// connected instance was discovered under, so Settings → Gateway can reopen
|
|
// into that org. Empty string for remote/local.
|
|
cloudOrg: string
|
|
sshHost: string
|
|
sshUser: string
|
|
sshPort: number | null
|
|
sshKeyPath: string
|
|
sshRemoteHermesPath: string
|
|
sshRemoteProfile: string
|
|
}
|
|
|
|
export interface DesktopConnectionConfigInput {
|
|
mode: 'local' | 'remote' | 'cloud' | 'ssh'
|
|
// When set, the save/apply/test targets this profile's per-profile remote
|
|
// override instead of the global connection.
|
|
profile?: null | string
|
|
remoteAuthMode?: 'oauth' | 'token'
|
|
remoteToken?: string
|
|
// When true and secure (OS-keychain) storage is unavailable, persist the
|
|
// remote token as plain text on disk instead of failing. Requires an explicit
|
|
// user opt-in from the renderer.
|
|
allowPlainTextToken?: boolean
|
|
remoteUrl?: string
|
|
// For a 'cloud' connection: the selected Hermes Cloud org (slug or id) to
|
|
// persist so Settings can reopen into it. Ignored for remote/local modes.
|
|
cloudOrg?: string
|
|
sshHost?: string
|
|
sshUser?: string
|
|
sshPort?: number | null
|
|
sshKeyPath?: string
|
|
sshRemoteHermesPath?: string
|
|
sshRemoteProfile?: string
|
|
}
|
|
|
|
export interface DesktopConnectionTestResult {
|
|
baseUrl?: string
|
|
ok?: boolean
|
|
version?: string | null
|
|
reachable?: boolean
|
|
sshError?:
|
|
| 'auth-failed'
|
|
| 'hermes-not-found'
|
|
| 'host-key-changed'
|
|
| 'timeout'
|
|
| 'unreachable'
|
|
| 'unsupported-platform'
|
|
| 'update-required'
|
|
| 'unknown'
|
|
| null
|
|
error?: string | null
|
|
host?: string
|
|
remoteHermesPath?: string
|
|
remoteHermesVersion?: string
|
|
remotePlatform?: string
|
|
}
|
|
|
|
// ── v2 multi-connection registry (named agent sources) ─────────────────────
|
|
|
|
export type DesktopConnectionKind = 'cloud' | 'local' | 'remote' | 'ssh'
|
|
|
|
// A registered agent source as the renderer sees it: token bytes never cross
|
|
// the IPC boundary (preview + set flag instead, like DesktopConnectionConfig).
|
|
export interface DesktopRegistryConnection {
|
|
id: string
|
|
kind: DesktopConnectionKind
|
|
// Required, registry-unique device name ("Homelab", "Work laptop").
|
|
label: string
|
|
url?: string
|
|
authMode?: 'oauth' | 'token'
|
|
org?: string
|
|
host?: string
|
|
user?: string
|
|
port?: number
|
|
keyPath?: string
|
|
remoteHermesPath?: string
|
|
remoteProfile?: string
|
|
tokenSet: boolean
|
|
tokenPreview: null | string
|
|
// Names of the stored extra gateway headers (Cloudflare Access etc.);
|
|
// header VALUES are secrets and never cross the IPC boundary. Optional so
|
|
// fixtures/older payloads without the field remain valid.
|
|
headerNames?: string[]
|
|
// Last-known stable backend identity (the /api/status `install_id`).
|
|
// Present once a roster enumeration or connection test has seen it; two
|
|
// connections sharing it are one physical backend registered under two
|
|
// addresses (display-only "Same backend as …" hint in Settings).
|
|
installId?: string
|
|
}
|
|
|
|
export interface DesktopConnectionsRegistry {
|
|
version: number
|
|
// id of the connection that owns the window/primary backend.
|
|
primary: string
|
|
// Preserve old installs by defaulting to the explicit primary; users may
|
|
// instead resume the last successfully opened source.
|
|
launchMode?: 'last-used' | 'primary'
|
|
// Last source the Sessions workspace opened successfully. Optional for
|
|
// compatibility with an older Electron main during a rolling app update.
|
|
lastUsed?: string
|
|
// Whether OS-keychain-backed encryption (Electron safeStorage) is available;
|
|
// false drives the plain-text token opt-in on keyring-less Linux.
|
|
secureTokenStorage: boolean
|
|
connections: DesktopRegistryConnection[]
|
|
}
|
|
|
|
export interface DesktopRegistryConnectionInput {
|
|
// Present for edits; omitted on create (the main process mints the id).
|
|
id?: string
|
|
kind: DesktopConnectionKind
|
|
label: string
|
|
url?: string
|
|
authMode?: 'oauth' | 'token'
|
|
// Plaintext token to store (encrypted at rest); omit to keep the saved one.
|
|
token?: string
|
|
allowPlainTextToken?: boolean
|
|
// Extra gateway headers for remote/cloud entries (access proxies such as
|
|
// Cloudflare Access). The map is authoritative when present: name → new
|
|
// plaintext value (encrypted at rest), or null to keep the stored secret
|
|
// for that name. Omit the field entirely to keep the saved set unchanged.
|
|
headers?: Record<string, null | string>
|
|
org?: string
|
|
host?: string
|
|
user?: string
|
|
port?: null | number
|
|
keyPath?: string
|
|
remoteHermesPath?: string
|
|
remoteProfile?: string
|
|
}
|
|
|
|
// One agent in the union roster: a profile on a registered source, with the
|
|
// pre-computed @name-device handle for duplicate profile names.
|
|
export interface DesktopRosterAgent {
|
|
connectionId: string
|
|
connectionKind: DesktopConnectionKind
|
|
connectionLabel: string
|
|
profile: string
|
|
targetProfile?: string
|
|
handle: string
|
|
}
|
|
|
|
export interface DesktopAgentRoster {
|
|
agents: DesktopRosterAgent[]
|
|
sources: {
|
|
connectionId: string
|
|
label: string
|
|
kind: DesktopConnectionKind
|
|
reachable: boolean
|
|
error?: string
|
|
// Stable backend identity (/api/status install_id) when known.
|
|
installId?: string
|
|
}[]
|
|
}
|
|
|
|
// Per-connection result row from the update fan-out.
|
|
export interface DesktopConnectionUpdateResult {
|
|
connectionId: string
|
|
label: string
|
|
kind: DesktopConnectionKind
|
|
ok: boolean
|
|
skipped?: boolean
|
|
reason?: string
|
|
detail?: string
|
|
error?: string
|
|
}
|
|
|
|
export type DesktopManagedConnectionUpdateOutcome =
|
|
'updated' | 'update-failed' | 'restore-failed' | 'update-and-restore-failed' | 'refused'
|
|
|
|
export interface DesktopManagedUpdateReceipt {
|
|
correlationId: string
|
|
// Additive receipt outcomes remain forward-compatible with newer updater
|
|
// kernels instead of making an older renderer reject their proof.
|
|
outcome: string
|
|
startedAt?: string
|
|
finishedAt?: string
|
|
preSha?: string
|
|
postSha?: string
|
|
preVersion?: string
|
|
postVersion?: string
|
|
stopReason?: string
|
|
}
|
|
|
|
export interface DesktopManagedConnectionUpdateResult {
|
|
connectionId: string
|
|
correlationId: string
|
|
ok: boolean
|
|
updateOk: boolean
|
|
restoreOk: boolean
|
|
outcome: DesktopManagedConnectionUpdateOutcome
|
|
exitCode: number | null
|
|
receipt: DesktopManagedUpdateReceipt | null
|
|
scopes: Array<{ profile: string; restored: boolean; error?: string }>
|
|
error?: string
|
|
message?: string
|
|
}
|
|
|
|
export interface DesktopSshResolveResult {
|
|
hostname: string | null
|
|
identityFile: string | null
|
|
port: number | null
|
|
user: string | null
|
|
}
|
|
|
|
export interface DesktopSshHostsResult {
|
|
hosts: string[]
|
|
}
|
|
|
|
export interface DesktopAuthProvider {
|
|
name: string
|
|
displayName: string
|
|
// True when this provider authenticates with a username + password
|
|
// (the gateway's /login page renders a credential form) rather than an
|
|
// OAuth redirect. The session/cookie/ws-ticket machinery is identical;
|
|
// only the login-page form and the desktop's button copy differ.
|
|
supportsPassword?: boolean
|
|
}
|
|
|
|
export interface DesktopConnectionProbeResult {
|
|
baseUrl: string
|
|
reachable: boolean
|
|
authMode: 'oauth' | 'token' | 'unknown'
|
|
providers: DesktopAuthProvider[]
|
|
version: string | null
|
|
error: string | null
|
|
}
|
|
|
|
export interface DesktopOauthLoginResult {
|
|
ok: boolean
|
|
baseUrl: string
|
|
connected: boolean
|
|
}
|
|
|
|
export interface DesktopOauthLogoutResult {
|
|
ok: boolean
|
|
connected: boolean
|
|
}
|
|
|
|
// --- Hermes Cloud (cloud-auto-discovery Phase 3) ---
|
|
|
|
export interface DesktopCloudStatus {
|
|
// The portal base URL the desktop talks to (default or env-overridden).
|
|
portalBaseUrl: string
|
|
// Whether the OAuth partition holds a live Nous portal (Privy) session — the
|
|
// portal authenticates via Privy, so this reflects the privy-token cookie, NOT
|
|
// the hermes gateway session cookies. See cookiesHavePrivySession.
|
|
signedIn: boolean
|
|
}
|
|
|
|
// A discovered Hermes Cloud agent — the trimmed DTO from NAS GET /api/agents.
|
|
export interface DesktopCloudAgent {
|
|
id: string
|
|
name: string
|
|
status: string
|
|
// null until the agent has a provisioned dashboard (show "provisioning…").
|
|
dashboardUrl: string | null
|
|
// "active" | "degraded" | "down" | "unknown".
|
|
dashboardGatewayState: string
|
|
}
|
|
|
|
// An org the signed-in user belongs to — for the org picker shown when a
|
|
// multi-org user's discovery call needs disambiguation (NAS 409).
|
|
export interface DesktopCloudOrg {
|
|
id: string
|
|
slug: string | null
|
|
name: string
|
|
isPersonal: boolean
|
|
// "OWNER" | "MEMBER".
|
|
role: string
|
|
}
|
|
|
|
// Discovery result: either the agent list, OR a request to pick an org first
|
|
// (multi-org user, no org chosen yet). The renderer shows a picker on the
|
|
// latter and re-calls discover(org). On the agents branch, `org` echoes the
|
|
// authoritatively-resolved org the list was scoped to (from NAS), so the
|
|
// desktop persists it without relying on transient picker state.
|
|
export type DesktopCloudDiscoverResult =
|
|
| { agents: DesktopCloudAgent[]; org?: DesktopCloudOrg | null; needsOrgSelection?: false }
|
|
| { needsOrgSelection: true; orgs: DesktopCloudOrg[] }
|
|
|
|
export interface DesktopCloudAgentSignInResult {
|
|
// The agent gateway base URL the silent sign-in targeted.
|
|
baseUrl: string
|
|
// Whether the agent's gateway session cookie landed (silent cascade done).
|
|
connected: boolean
|
|
}
|
|
|
|
export interface DesktopBootProgress {
|
|
error: string | null
|
|
fakeMode: boolean
|
|
/** True when the boot failure is a Nous Cloud agent that is down (HTTP 502/503/504). */
|
|
isCloudBackendDown?: boolean
|
|
message: string
|
|
phase: string
|
|
progress: number
|
|
/**
|
|
* True when the boot failure carried by `error` was a TRANSIENT remote
|
|
* failure (dropped SSH/HTTP registered connection, mint timeout) that the
|
|
* renderer may retry automatically. Absent/false on success updates,
|
|
* local failures, and confirmed reauth rejections.
|
|
*/
|
|
retryable?: boolean
|
|
running: boolean
|
|
/** Structured HTTP status when the boot failure carried one (e.g. 503). */
|
|
statusCode?: number | null
|
|
timestamp: number
|
|
}
|
|
|
|
// First-launch install ("bootstrap") event types -- emitted by
|
|
// electron/bootstrap-runner.ts and observed by the renderer install overlay.
|
|
// Mirrors the event shapes emitted by runBootstrap()'s onEvent callback.
|
|
|
|
export interface DesktopBootstrapStageDescriptor {
|
|
name: string
|
|
title?: string
|
|
category?: string
|
|
needs_user_input?: boolean
|
|
}
|
|
|
|
export type DesktopBootstrapStageState = 'pending' | 'running' | 'succeeded' | 'skipped' | 'failed'
|
|
|
|
export interface DesktopBootstrapStageResult {
|
|
state: DesktopBootstrapStageState
|
|
durationMs: number | null
|
|
startedAt: number | null
|
|
json: { ok: boolean; skipped?: boolean; reason?: string | null; stage: string } | null
|
|
error: string | null
|
|
}
|
|
|
|
export interface DesktopBootstrapUnsupportedPlatform {
|
|
platform: string
|
|
activeRoot: string
|
|
installCommand: string
|
|
docsUrl: string
|
|
}
|
|
|
|
export interface DesktopBootstrapSetupChoice {
|
|
platform: string
|
|
activeRoot: string
|
|
}
|
|
|
|
export interface DesktopBootstrapState {
|
|
active: boolean
|
|
manifest: { type: 'manifest'; stages: DesktopBootstrapStageDescriptor[]; protocolVersion: number | null } | null
|
|
stages: Record<string, DesktopBootstrapStageResult>
|
|
error: string | null
|
|
log: Array<{ ts: number; stage: string | null; line: string; stream?: 'stdout' | 'stderr' }>
|
|
startedAt: number | null
|
|
completedAt: number | null
|
|
setupChoice: DesktopBootstrapSetupChoice | null
|
|
unsupportedPlatform: DesktopBootstrapUnsupportedPlatform | null
|
|
}
|
|
|
|
export type DesktopBootstrapEvent =
|
|
| { type: 'dismissed' }
|
|
| {
|
|
type: 'setup-choice'
|
|
active: boolean
|
|
platform?: string
|
|
activeRoot?: string
|
|
}
|
|
| { type: 'manifest'; stages: DesktopBootstrapStageDescriptor[]; protocolVersion: number | null }
|
|
| {
|
|
type: 'stage'
|
|
name: string
|
|
state: DesktopBootstrapStageState
|
|
durationMs?: number
|
|
json?: DesktopBootstrapStageResult['json']
|
|
error?: string | null
|
|
}
|
|
| { type: 'log'; stage?: string | null; line: string; stream?: 'stdout' | 'stderr' }
|
|
| { type: 'complete'; marker: Record<string, unknown> }
|
|
| { type: 'failed'; stage?: string | null; error: string }
|
|
| {
|
|
type: 'unsupported-platform'
|
|
platform: string
|
|
activeRoot: string
|
|
installCommand: string
|
|
docsUrl: string
|
|
}
|
|
|
|
export interface HermesApiRequest {
|
|
path: string
|
|
method?: string
|
|
body?: unknown
|
|
// Single-file multipart upload (FastAPI UploadFile endpoints). Mutually
|
|
// exclusive with `body`; bytes transfer over IPC as a structured-clone
|
|
// ArrayBuffer. Token-mode backends only.
|
|
upload?: { filename: string; contentType?: string; bytes: ArrayBuffer }
|
|
timeoutMs?: number
|
|
// Route this REST call to a specific profile's backend. Omit for the primary
|
|
// (window) backend. Read-only cross-profile data is served by the primary, so
|
|
// this is only needed for profile-scoped live/settings calls.
|
|
profile?: string | null
|
|
// Route this REST call to a specific REGISTERED gateway connection (v2
|
|
// registry). Data owned by a remote gateway — cron jobs and their run
|
|
// sessions — lives in that host's state.db, so requests for it must resolve
|
|
// through the owning connection, not the local profile pool. Omit / '' to
|
|
// keep the legacy profile-routed path; explicit 'local' forces this device.
|
|
connectionId?: string | null
|
|
}
|
|
|
|
export interface HermesNotification {
|
|
title?: string
|
|
body?: string
|
|
silent?: boolean
|
|
kind?: string
|
|
sessionId?: string
|
|
/** Dedupe discriminator for session-less notifications (e.g. plugin id). */
|
|
tag?: string
|
|
/** Absolute icon path for Electron `Notification`. */
|
|
icon?: string
|
|
/** Resolved hash-router path opened on body click (plugin / deeplink-compatible). */
|
|
activate?: string
|
|
/** Renderer handle for onActivate / onAction callbacks. */
|
|
notifyId?: string
|
|
actions?: { id: string; text: string; activate?: string }[]
|
|
}
|
|
|
|
export interface HermesPreviewTarget {
|
|
binary?: boolean
|
|
byteSize?: number
|
|
kind: 'file' | 'url'
|
|
label: string
|
|
large?: boolean
|
|
language?: string
|
|
mimeType?: string
|
|
path?: string
|
|
previewKind?: 'binary' | 'html' | 'image' | 'pdf' | 'text'
|
|
renderMode?: 'preview' | 'source'
|
|
source: string
|
|
url: string
|
|
}
|
|
|
|
export interface HermesReadFileTextResult {
|
|
binary?: boolean
|
|
byteSize?: number
|
|
language?: string
|
|
mimeType?: string
|
|
path: string
|
|
text: string
|
|
truncated?: boolean
|
|
}
|
|
|
|
export interface HermesPreviewWatch {
|
|
id: string
|
|
path: string
|
|
}
|
|
|
|
// A real git worktree as reported by `git worktree list` (source of truth for
|
|
// the "Start work" flow), as opposed to the session-cwd-derived grouping above.
|
|
export interface HermesGitWorktree {
|
|
path: string
|
|
branch: null | string
|
|
isMain: boolean
|
|
detached: boolean
|
|
locked: boolean
|
|
}
|
|
|
|
// A branch that the "convert a branch into a worktree" picker offers: the local
|
|
// heads, plus the remote-tracking refs that have no local branch yet.
|
|
// `checkedOut` means that a selection opens that checkout. `isDefault` means
|
|
// that a selection switches the main checkout, and does not make
|
|
// `.worktrees/main`. `isRemote` means that a selection first makes a local
|
|
// branch that tracks the remote one.
|
|
export interface HermesGitBranch {
|
|
name: string
|
|
checkedOut: boolean
|
|
isDefault: boolean
|
|
isRemote: boolean
|
|
worktreePath: null | string
|
|
}
|
|
|
|
// A branch the new worktree can be based on: local heads + remote-tracking
|
|
// refs. `isRemote` distinguishes `origin/main` from a local `main` (the UI
|
|
// may show a remote glyph); `isDefault` flags origin/HEAD so the dialog can
|
|
// preselect it.
|
|
export interface HermesGitBaseBranch {
|
|
name: string
|
|
isRemote: boolean
|
|
isDefault: boolean
|
|
}
|
|
|
|
// A single changed path from `git status --porcelain=v2`, classified by state
|
|
// so the coding rail / switcher can group + open the right diff.
|
|
export interface HermesRepoStatusFile {
|
|
path: string
|
|
staged: boolean
|
|
unstaged: boolean
|
|
untracked: boolean
|
|
conflicted: boolean
|
|
}
|
|
|
|
// Compact working-tree status for the composer coding rail (parsed from
|
|
// `git status --porcelain=v2 --branch`).
|
|
export interface HermesRepoStatus {
|
|
branch: null | string
|
|
// The repo's trunk ("main" / "master" / …), so the UI can offer "branch off
|
|
// the default" from anywhere. Null when no trunk is detected.
|
|
defaultBranch: null | string
|
|
detached: boolean
|
|
ahead: number
|
|
behind: number
|
|
staged: number
|
|
unstaged: number
|
|
untracked: number
|
|
conflicted: number
|
|
// Total distinct changed paths (tracked modified + conflicts + untracked).
|
|
changed: number
|
|
// +/- line counts of tracked changes vs HEAD (staged + unstaged). Untracked
|
|
// files aren't in the diff, so they don't contribute lines.
|
|
added: number
|
|
removed: number
|
|
// Capped changed-file list (REPO_STATUS_FILE_CAP) for the diff/open actions.
|
|
files: HermesRepoStatusFile[]
|
|
}
|
|
|
|
// Diff scope for the review pane, mirroring Codex: uncommitted working-tree
|
|
// changes, all changes vs the branch base, or everything since the current
|
|
// turn began.
|
|
export type HermesReviewScope = 'branch' | 'lastTurn' | 'uncommitted'
|
|
|
|
// One changed file in the review pane (status letter, +/- lines, staged flag).
|
|
export interface HermesReviewFile {
|
|
path: string
|
|
added: number
|
|
removed: number
|
|
// M(odified) A(dded) D(eleted) R(enamed) C(opied) U(nmerged) ?(untracked)
|
|
status: string
|
|
staged: boolean
|
|
}
|
|
|
|
export interface HermesReviewList {
|
|
files: HermesReviewFile[]
|
|
// The resolved base ref the scope diffed against (branch merge-base / turn
|
|
// baseline), or null for the uncommitted scope.
|
|
base: null | string
|
|
}
|
|
|
|
// The branch's PR (if any) as reported by `gh pr view`.
|
|
export interface HermesReviewPr {
|
|
url: string
|
|
state: string
|
|
number: number
|
|
}
|
|
|
|
// One repo's PRs as reported by `gh pr list`, each tied to the branch it was
|
|
// opened from — how a session row finds its own PR.
|
|
export interface HermesBranchPullRequest {
|
|
branch: string
|
|
draft: boolean
|
|
number: number
|
|
/** `open` | `closed` | `merged`, lowercased from gh. */
|
|
state: string
|
|
title: string
|
|
url: string
|
|
}
|
|
|
|
export interface HermesRepoPullRequests {
|
|
ghReady: boolean
|
|
prs: HermesBranchPullRequest[]
|
|
}
|
|
|
|
// A PR review/issue comment resolved from a pasted GitHub URL — the composer's
|
|
// review-comment attachment context. `path`/`line`/`diffHunk` are empty for
|
|
// conversation-tab (issue) comments; `line` is null when the comment is
|
|
// outdated and only `original_line` remained.
|
|
export interface HermesPrComment {
|
|
author: string
|
|
body: string
|
|
diffHunk: string
|
|
kind: 'issue' | 'review'
|
|
line: null | number
|
|
path: string
|
|
prNumber: number
|
|
startLine: null | number
|
|
url: string
|
|
}
|
|
// gh availability/auth + the current branch's PR — drives the review pane's PR
|
|
// button (disabled when gh isn't ready, "Open PR" vs "Create PR" otherwise).
|
|
export interface HermesReviewShipInfo {
|
|
ghReady: boolean
|
|
pr: HermesReviewPr | null
|
|
}
|
|
|
|
export interface HermesReadDirEntry {
|
|
name: string
|
|
path: string
|
|
isDirectory: boolean
|
|
}
|
|
|
|
export interface HermesReadDirResult {
|
|
entries: HermesReadDirEntry[]
|
|
error?: string
|
|
}
|
|
|
|
export interface HermesPreviewFileChanged {
|
|
id: string
|
|
path: string
|
|
url: string
|
|
}
|
|
|
|
export interface HermesSelectPathsOptions {
|
|
title?: string
|
|
defaultPath?: string
|
|
directories?: boolean
|
|
multiple?: boolean
|
|
/** Backend profile that produced defaultPath; Electron uses it for WSL gating. */
|
|
profile?: string
|
|
filters?: Array<{ name: string; extensions: string[] }>
|
|
}
|
|
|
|
export interface BackendExit {
|
|
code: number | null
|
|
signal: string | null
|
|
}
|