Files
aiturk-hermes-ide/apps/desktop/src/global.d.ts
T
yilsemandClaude Opus 5.5 03d3803dfc feat(aiturk): phone link for AITURK Mobil (1.0.0-beta.15)
Settings gets a Phone link window: when enabled, the IDE main process runs a
small TLS proxy on the local network (self-signed certificate, pinned by its
fingerprint) in front of the loopback backend and pairs a phone by QR code
or an 8-character code. Each phone gets its own revocable token; the backend
session token never leaves the computer. The root page, terminal sockets and
secret or settings-changing endpoints are not reachable from a phone. The
old WebRTC pairing window is replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
2026-10-04 06:02:02 +03:00

1476 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-click TurkServis sign-in (electron/aiturk-login.ts). API keys stay
* in the main process; only the short code and a status come back. */
aiturkLogin?: {
start: (scope?: { connectionId?: string; profile?: string }) => Promise<AiturkLoginStart>
poll: () => Promise<AiturkLoginPoll>
cancel: () => Promise<{ ok: true }>
overwriteCli: () => Promise<{ cli: AiturkCliStatus }>
}
/** "Telefon bağlantısı": opens the pairing window (electron/mobile-host.mjs). Windows only. */
aiturkMobile?: {
open: () => Promise<{ ok: true }>
}
/** 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 type AiturkCliStatus = 'ok' | 'missing' | 'outdated' | 'other-account' | 'unknown-account' | 'failed'
export interface AiturkLoginStart {
userCode: string
verificationUri: string
expiresAt: number
}
export type AiturkLoginPoll =
| { state: 'pending' }
| { state: 'expired' }
| { state: 'connected'; cli: AiturkCliStatus }
| { state: 'error'; error: 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
}