mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Phase 3 (non-UI) of cloud-auto-discovery. Adds the 'cloud' connection mode and
the IPC plumbing for a single portal login that powers both agent discovery and
silent per-agent sign-in. The Phase 4 UI (cloud ModeCard + instance picker)
sits on top of these IPC methods.
Mode widening (Model A, decisions.md Q6): DesktopConnectionConfig.mode and
DesktopConnectionConfigInput.mode widen to 'local'|'remote'|'cloud'. A cloud
entry is a remote-shaped block (remoteUrl = the selected agent's dashboardUrl,
remoteAuthMode 'oauth') tagged mode 'cloud' so settings reopens into the cloud
picker. Every RESOLUTION site treats cloud as remote via the new
modeIsRemoteLike() helper (centralized in connection-config.cjs): readDesktop-
ConnectionConfig, sanitizeConnectionProfiles, sanitizeDesktopConnectionConfig,
coerceDesktopConnectionConfig, profileRemoteOverride, resolveRemoteBackend,
globalRemoteActive, testDesktopConnectionConfig, and isRemoteReauthFailure. The
live resolved HermesConnection.mode stays 'local'|'remote' — cloud never reaches
the boot path or the renderer remote-gating sites.
Cloud mechanics (main.cjs): one portal session in the persist:hermes-remote-oauth
partition does double duty — discoverCloudAgents() GETs {portal}/api/agents over
the partition-bound net (cookie-authed; NAS #542 accepts the cookie), and
cloudAgentSilentSignIn() opens a selected agent's /login in the same partition so
the portal's silent auto-approve 302s back with that agent's session cookie, no
second prompt. Portal base URL resolves via DEFAULT_NOUS_PORTAL_URL +
HERMES_PORTAL_BASE_URL/NOUS_PORTAL_BASE_URL overrides, mirroring the CLI.
IPC: hermes☁️{status,login,logout,discover,agent-sign-in} in main.cjs +
preload.cjs, typed in global.d.ts (DesktopCloudStatus/Agent/DiscoverResult/
AgentSignInResult).
Tests: modeIsRemoteLike + cloud profileRemoteOverride (node --test, 52 pass);
cloud reauth-failure cases (vitest, 16 pass). tsc clean; eslint clean. New tests
verified to fail without the source changes.
cloud-auto-discovery Phase 3 (non-discovery half + discovery/cascade plumbing).
732 lines
27 KiB
TypeScript
732 lines
27 KiB
TypeScript
import type {
|
|
PetOverlayBounds,
|
|
PetOverlayControl,
|
|
PetOverlayOpenRequest,
|
|
PetOverlayStatePayload
|
|
} from './store/pet-overlay'
|
|
|
|
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) => Promise<HermesConnection>
|
|
// 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 }>
|
|
getGatewayWsUrl: (profile?: null | string) => Promise<string>
|
|
// 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?: { watch?: boolean }) => Promise<{ ok: boolean; error?: string }>
|
|
// Open (or focus) a compact secondary window on the new-session draft.
|
|
openNewSessionWindow: () => Promise<{ ok: boolean; error?: string }>
|
|
// 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
|
|
}
|
|
getBootProgress: () => Promise<DesktopBootProgress>
|
|
getConnectionConfig: (profile?: null | string) => Promise<DesktopConnectionConfig>
|
|
saveConnectionConfig: (payload: DesktopConnectionConfigInput) => Promise<DesktopConnectionConfig>
|
|
applyConnectionConfig: (payload: DesktopConnectionConfigInput) => Promise<DesktopConnectionConfig>
|
|
testConnectionConfig: (payload: DesktopConnectionConfigInput) => Promise<DesktopConnectionTestResult>
|
|
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: () => Promise<DesktopCloudDiscoverResult>
|
|
agentSignIn: (dashboardUrl: string) => Promise<DesktopCloudAgentSignInResult>
|
|
}
|
|
profile: {
|
|
get: () => 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>
|
|
readFileDataUrl: (filePath: string) => Promise<string>
|
|
readFileText: (filePath: string) => Promise<HermesReadFileTextResult>
|
|
selectPaths: (options?: HermesSelectPathsOptions) => Promise<string[]>
|
|
writeClipboard: (text: string) => Promise<boolean>
|
|
saveImageFromUrl: (url: string) => Promise<boolean>
|
|
saveImageBuffer: (data: ArrayBuffer | Uint8Array, ext: string) => Promise<string>
|
|
saveClipboardImage: () => Promise<string>
|
|
getPathForFile: (file: File) => string
|
|
normalizePreviewTarget: (target: string, baseDir?: string) => Promise<HermesPreviewTarget | null>
|
|
watchPreviewFile: (url: string) => Promise<HermesPreviewWatch>
|
|
stopPreviewFileWatch: (id: string) => Promise<boolean>
|
|
setTitleBarTheme?: (payload: HermesTitleBarTheme) => void
|
|
setNativeTheme?: (mode: 'dark' | 'light' | 'system') => void
|
|
setTranslucency?: (payload: { intensity: number }) => void
|
|
setPreviewShortcutActive?: (active: boolean) => void
|
|
openExternal: (url: string) => Promise<void>
|
|
openPreviewInBrowser?: (url: string) => Promise<void>
|
|
fetchLinkTitle: (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 }>
|
|
}
|
|
revealLogs: () => Promise<{ ok: boolean; path: string; error?: string }>
|
|
getRecentLogs: () => Promise<{ path: string; lines: string[] }>
|
|
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>
|
|
// 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 }>
|
|
// Local branches for the "convert a branch into a worktree" picker.
|
|
branchList: (repoPath: string) => Promise<HermesGitBranch[]>
|
|
// 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>
|
|
createPr: (repoPath: string) => Promise<{ url: string }>
|
|
}
|
|
// Repo-first discovery: scan bounded roots for git repos (depth-capped).
|
|
scanRepos: (roots: string[], options?: { maxDepth?: number }) => Promise<{ root: string; label: string }[]>
|
|
}
|
|
terminal: {
|
|
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>
|
|
}
|
|
onClosePreviewRequested?: (callback: () => void) => () => void
|
|
onOpenUpdatesRequested?: (callback: () => void) => () => void
|
|
onDeepLink?: (
|
|
callback: (payload: { kind: string; name: string; params: Record<string, string> }) => void
|
|
) => () => void
|
|
signalDeepLinkReady?: () => Promise<{ ok: boolean }>
|
|
onWindowStateChanged?: (callback: (payload: HermesWindowState) => void) => () => void
|
|
onFocusSession?: (callback: (sessionId: string) => void) => () => void
|
|
onNotificationAction?: (callback: (payload: { actionId: string; sessionId?: string }) => void) => () => void
|
|
onPreviewFileChanged: (callback: (payload: HermesPreviewFileChanged) => void) => () => void
|
|
onBackendExit: (callback: (payload: BackendExit) => void) => () => void
|
|
onPowerResume?: (callback: () => void) => () => void
|
|
onBootProgress: (callback: (payload: DesktopBootProgress) => void) => () => void
|
|
getBootstrapState: () => Promise<DesktopBootstrapState>
|
|
resetBootstrap: () => Promise<{ ok: boolean }>
|
|
repairBootstrap: () => Promise<{ ok: boolean }>
|
|
cancelBootstrap: () => Promise<{ ok: boolean; cancelled: boolean }>
|
|
onBootstrapEvent: (callback: (payload: DesktopBootstrapEvent) => void) => () => void
|
|
getVersion: () => Promise<DesktopVersionInfo>
|
|
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[]>
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
behind?: number
|
|
currentSha?: string
|
|
targetSha?: string
|
|
commits?: DesktopUpdateCommit[]
|
|
dirty?: boolean
|
|
fetchedAt?: number
|
|
}
|
|
|
|
export type DesktopUpdateDirtyStrategy = 'abort' | 'stash' | 'force'
|
|
|
|
export interface DesktopUpdateApplyOptions {
|
|
dirtyStrategy?: DesktopUpdateDirtyStrategy
|
|
}
|
|
|
|
export interface DesktopUpdateApplyResult {
|
|
ok: boolean
|
|
branch?: string
|
|
error?: string
|
|
message?: string
|
|
/** 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 HermesConnection {
|
|
baseUrl: string
|
|
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'
|
|
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
|
|
windowButtonPosition: { x: number; y: number } | null
|
|
}
|
|
|
|
export interface HermesTitleBarTheme {
|
|
background: string
|
|
foreground: string
|
|
}
|
|
|
|
export interface HermesWindowState {
|
|
isFullscreen: 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'
|
|
// 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
|
|
remoteUrl: string
|
|
}
|
|
|
|
export interface DesktopConnectionConfigInput {
|
|
mode: 'local' | 'remote' | 'cloud'
|
|
// 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
|
|
remoteUrl?: string
|
|
}
|
|
|
|
export interface DesktopConnectionTestResult {
|
|
baseUrl: string
|
|
ok: boolean
|
|
version: string | null
|
|
}
|
|
|
|
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 portal session (AT-or-RT).
|
|
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
|
|
}
|
|
|
|
export interface DesktopCloudDiscoverResult {
|
|
agents: DesktopCloudAgent[]
|
|
}
|
|
|
|
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
|
|
message: string
|
|
phase: string
|
|
progress: number
|
|
running: boolean
|
|
timestamp: number
|
|
}
|
|
|
|
// First-launch install ("bootstrap") event types -- emitted by
|
|
// electron/bootstrap-runner.cjs 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 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
|
|
unsupportedPlatform: DesktopBootstrapUnsupportedPlatform | null
|
|
}
|
|
|
|
export type DesktopBootstrapEvent =
|
|
| { 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
|
|
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
|
|
}
|
|
|
|
export interface HermesNotification {
|
|
title?: string
|
|
body?: string
|
|
silent?: boolean
|
|
kind?: string
|
|
sessionId?: string
|
|
actions?: { id: string; text: 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' | '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 local branch as offered by the "convert a branch into a worktree" picker.
|
|
// `checkedOut` means selecting opens that checkout; `isDefault` means selecting
|
|
// switches the main checkout instead of creating `.worktrees/main`.
|
|
export interface HermesGitBranch {
|
|
name: string
|
|
checkedOut: boolean
|
|
isDefault: boolean
|
|
worktreePath: null | string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
filters?: Array<{ name: string; extensions: string[] }>
|
|
}
|
|
|
|
export interface BackendExit {
|
|
code: number | null
|
|
signal: string | null
|
|
}
|