Files
hermes-agent/apps/desktop/src/global.d.ts
Austin Pickett 5fef73015a fix(desktop): cooperative pool retirement for foreground dials behind an admission fence
A foreground bot open against a full 3-slot local pool waited 30s for a slot
and timed out (production: 7,122 slot waits, 6,305 timeouts, ~790 cancelled
starts in 10 days on a 17-profile host). Nothing could free a slot: a child's
hard lease lives until the child exits (main.ts child 'exit' handler,
teardownFailedLocalBackend, stopPoolBackend after the bounded SIGTERM->SIGKILL
exit), while LRU eviction and the idle reaper key off lastActiveAt, which the
renderer refreshes every 60s for every open socket. A bot-tile-pinned resident
is keepalive-fresh forever. Occupied is not busy.

New `electron/pool-retire.ts` (pure, DI-testable like pool-spawn-coordinator):
a foreground dial that finds `activeCount >= poolMaxBackends()` may retire ONE
resident, under these rules:

* Proof is the backend's. Each LRU candidate is probed over
  `/api/health/idle` (running sessions + running cron jobs + prompts waiting on
  a human). Only `true` is idle; `false` and `null` (older runtime 404, probe
  error, unreadable ledger) are busy. The renderer-published `activeTurn`
  lease is an early skip, never the proof; entries no longer initialise it to
  false, so a fresh backend is not evictable by default.
* Admission fence: concurrent foreground dials share one retirement and one
  probe; the coordinator hands the freed slot to the ticket that queued first.
* Identity recheck (`pool.get(key) === entry`, no new turn lease) after every
  await, and a re-probe immediately before the stop.
* The waiter's `coordinator.request()` is issued BEFORE the stop so it sits at
  the queue head; the slot is released only by the retired child's real exit
  through the existing stopPoolBackend -> releaseLocalBackendSlot path.
* `hermes:pool:retiring` is broadcast to every window before the SIGTERM so
  the renderer parks the scope instead of redialing into the vacated slot.

main.ts grows only the trigger point, the HTTP probe, the broadcast and the
retirer construction. Kept from #104871 with credit: the trigger point before
`coordinator.request`, the `touchBackend(profile, options)` IPC widening
(preload.ts / global.d.ts), and the LRU-among-eligible selector shape.

Tests (pool-retire.test.ts): fence with two concurrent tickets over a real
LocalBackendSpawnCoordinator (one probe, one SIGTERM, slot granted only after
the simulated exit, exactly one waiter served); identity recheck aborts on a
swapped entry or a lease published after the probe; idle null / cron-running
ineligible with fall-through to the queue; renderer activeTurn:false loses to
a backend re-probe.

Co-authored-by: bounce12340 <128559392+bounce12340@users.noreply.github.com>
2026-09-17 00:34:50 -05:00

1502 lines
62 KiB
TypeScript

import type { GatewayWsUrlResult } from '@hermes/shared'
import type { TranslucencyState } from '@hermes/shared/translucency'
import type { ScreenshotApi } from '../electron/command-screenshot-types'
import type { HermesNotification } from '../electron/notification-types'
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: {
// 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,
opts?: { priority?: 'foreground' | 'background' }
) => 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
priority?: 'foreground' | 'background'
}) => 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. `activeTurn` reports whether
// a prompt turn leases the backend (early skip for cooperative
// retirement; the backend probe is the proof).
touchBackend: (profile?: string | null, options?: { activeTurn?: boolean }) => 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 on an explicit connection/profile, or inherits
// the calling window's route when no options are supplied.
openWindow: (options?: DesktopProfileRoute) => 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
}
// Intro reveal: the full-screen first-run brand sequence. The main
// renderer owns the phase; the overlay window (`?win=intro`) owns
// the animation clock and plays sound locally.
introReveal?: {
open: (payload?: { hideMain?: boolean }) => Promise<{ ok: boolean }>
close: (payload?: { showMain?: boolean }) => Promise<{ ok: boolean }>
skip: () => void
/** The surface painted its first frame — reveal the OS window now. */
ready: () => void
onSkip: (callback: () => void) => () => void
onClosed: (callback: () => void) => () => void
}
// In-chat onboarding assembly: grow the main window outward by per-edge
// pixel deltas so the chat pane keeps its exact screen rect while the
// app assembles around it.
chatOnboarding?: {
grow: (request: {
bottom: number
left: number
/** Floor for the resulting CSS-pixel viewport width, for layouts that
* need one (a docked sidebar). Clamped to the display. */
minWidth?: number
right: number
top: number
}) => void
/** The film has revealed the app. Animate the visible window down to
* the solo-chat size as the guided chat starts. */
soloBoot?: () => 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
}
// macOS native screenshot gesture; absent on other platforms.
screenshot?: ScreenshotApi
// 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: {
getDefault: () => Promise<DesktopProfileRoute | null>
setDefault: (route: DesktopProfileRoute) => Promise<DesktopProfileRoute>
onDefaultChanged: (callback: (route: DesktopProfileRoute | null) => void) => () => void
get: () => Promise<DesktopActiveProfile>
// Remembers last use without interrupting a live workspace switch or
// replacing an explicit default route.
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
sessionId?: 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>
savePastedText: (text: string) => 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
/** Launch flag: the Nous free tier is on for this launch
* (HERMES_GUEST_ONBOARDING=1 or --guest-onboarding). Read-only fact the
* main process also stamps onto every backend it spawns. */
guestOnboardingEnabled?: boolean
/** Launch flag: skip the first-run film (HERMES_SKIP_INTRO=1 or
* --skip-intro) so a fresh HERMES_HOME lands on the guided chat. */
skipIntro?: 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/iss via oauth.callback. */
mcpOauth?: {
listen: () => Promise<{ id: string; redirectUri: string }>
wait: (
id: string,
timeoutMs?: number
) => Promise<{ code: null | string; error: null | string; iss: 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>
/** Re-copy unified packages' desktop halves into the app-level root; returns touched paths. */
reconcileDesktopPlugins?: () => 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>
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
// Cooperative pool retirement: main is stopping the pooled backend under
// `poolKey` for a foreground open. The renderer parks that scope.
onPoolBackendRetiring?: (callback: (payload: { poolKey: string }) => 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>
/** Host facts for the guided first run. Optional: an older preload (a
* mid-upgrade managed install) simply doesn't answer. */
getMachineProfile?: () => Promise<DesktopMachineProfile>
/** Restart the app in place — loads the swapped bundle when bundleSwapPending. */
relaunchApp?: () => Promise<void>
getRemoteDisplayReason?: () => Promise<string | null>
updates: {
check: (opts?: { force?: boolean }) => 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 interface DesktopMachineProfile {
/** Days since the OS created this user account; null when unknowable. */
ageDays: null | number
arch: string
/** The OS display language (`app.getLocale()`, e.g. "ja", "pt-BR"); '' when
* unknowable. A first-run DEFAULT for the UI language, never a lock — the
* user's saved `display.language` always wins, and the picker still rules. */
locale: string
/** Hardware's self-reported model (`NVIDIA_DGX_Spark`); '' when unavailable. */
model: string
/** An NVIDIA GPU is present, by PCI vendor id. */
nvidia: boolean
platform: string
release: string
/** OS login name ('' when unknowable) — a first-name SUGGESTION for the
* guided chat, never a default. The renderer blocklists handles that are
* not a name before offering it. */
username: string
}
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 DesktopProfileRoute {
connectionId: null | string
profile: string
}
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
cloudName?: 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
// Passive background read that must never cold-start a pooled backend (#103375).
// When true and the target profile has no warm pool entry, the main process
// fails fast without spawning a child or consuming a pool slot, so background
// tile reconciles cannot starve interactive opens.
passive?: boolean
// An interactive Settings scope selection may cold-start a profile backend.
// Keep that intent separate from passive hydration so the pool can reserve a
// slot for the user's visible request.
priority?: 'foreground'
}
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[]
}
// 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
}