hermes-agent/apps/desktop
emozilla c858484b45 desktop: swap node-pty fork for upstream microsoft/node-pty 1.1.0
The previous dependency, @homebridge/node-pty-prebuilt-multiarch@0.13.1,
publishes no win32-arm64 prebuilds on its v0.13.x line, and its v0.14.x
betas (which do add an arm64 Windows build) ship no electron-vXXX-win32-
arm64 prebuilds at all -- so packaged Electron 40 builds (NMV 143) would
fail at runtime even on a successful npm install. Net effect: the
desktop's integrated terminal was unbuildable on Windows-on-ARM, in
both dev (npm install fails: 404 fetching the node-vXXX-win32-arm64
prebuilt) and packaged builds (no Electron-ABI prebuilt exists).

The homebridge fork was originally created because upstream node-pty
shipped no prebuilds at all. That hasn't been true since node-pty@1.0
(April 2024), which:

- bundles prebuilts for mac (arm64+x64) and Windows (arm64+x64) directly
  inside the npm tarball -- no GitHub-Releases fetch, no missing-binary
  failure mode
- uses N-API (node-addon-api) for ABI stability across Node and Electron
  major versions, so the same pty.node binary loads under Node 22 (dev)
  and Electron 40+ (packaged) without per-ABI rebuilds
- is what VS Code, Hyper, and Theia actually ship

API surface is identical (spawn / onData / onExit / write / resize /
kill) -- no call-site changes needed.

Specifically:

- apps/desktop/package.json: replace the @homebridge fork with
  node-pty@1.1.0 (exact pin). Widen `asarUnpack` from `["**/*.node"]`
  to also unpack `**/prebuilds/**`, because node-pty ships runtime-
  execed helpers alongside its .node files (darwin spawn-helper has no
  extension and would not be matched by `**/*.node`; conpty.dll,
  OpenConsole.exe, winpty.dll, winpty-agent.exe on Windows are also
  exec'd at runtime and cannot live inside asar).

- apps/desktop/electron/main.cjs: update both require() strings to
  match the new package name and the new staged path under
  resources/native-deps/node-pty/.

- apps/desktop/scripts/stage-native-deps.cjs: point at node_modules/
  node-pty. node-pty's prebuilts live under prebuilds/<plat>-<arch>/
  (not build/Release/), so update the include glob to copy that dir.
  Per-arch staging keeps the resource bundle small (target arch comes
  from npm_config_arch when electron-builder cross-builds, else
  process.arch). Explicitly enumerate file types in the prebuilds glob
  so the ~25 MB of .pdb debug symbols that prebuild-install bundles
  for Windows crash analysis don't bloat the installer (29 MB -> 2.6 MB
  staged on win32-arm64). Re-assert +x on the darwin spawn-helper
  defensively, since a stripped mode bit would manifest as a silent
  ENOENT at first pty.spawn().

- apps/desktop/scripts/test-desktop.mjs: update expectedNativeDepPaths()
  and its assertion site to look at prebuilds/<plat>-<arch>/ instead of
  build/Release/. Add an explicit spawn-helper-exists check on darwin
  so a regression in the asarUnpack glob would fail loudly in CI rather
  than at first PTY spawn.

Trade-off: Linux end-users lose prebuilts and fall back to building
node-pty from source on `npm install`. Acceptable because Hermes
ships no Linux desktop builds (desktop-release.yml matrix is mac + win
only, package.json declares no `linux` target), and Linux developers
hacking on the desktop already need a C++ toolchain for the rest of
the stack.

Verified on Windows 11 ARM64 (Snapdragon):
  npm install                                          -> exit 0
  node -e "require('node-pty').spawn(...)" round-trip  -> OK
  stage-native-deps                                    -> 27 files, 2.6 MB
  load from staged tree (simulates packaged fallback)  -> ConPTY
                                                           round-trip OK
2026-05-18 21:50:53 -07:00
..
assets feat: add install readme et al 2026-05-01 22:20:05 -05:00
electron desktop: swap node-pty fork for upstream microsoft/node-pty 1.1.0 2026-05-18 21:50:53 -07:00
public feat(desktop): theme polish, prose chat typography, composer chrome 2026-05-11 10:25:23 -04:00
scripts desktop: swap node-pty fork for upstream microsoft/node-pty 1.1.0 2026-05-18 21:50:53 -07:00
src feat(desktop): persistent terminal pane + fullscreen takeover 2026-05-18 02:20:41 -05:00
.prettierrc feat(desktop): add structured desktop chat app 2026-05-01 12:49:12 -05:00
components.json feat(desktop): add structured desktop chat app 2026-05-01 12:49:12 -05:00
eslint.config.mjs feat(desktop): add structured desktop chat app 2026-05-01 12:49:12 -05:00
index.html feat(desktop): theme polish, prose chat typography, composer chrome 2026-05-11 10:25:23 -04:00
package.json desktop: swap node-pty fork for upstream microsoft/node-pty 1.1.0 2026-05-18 21:50:53 -07:00
preview-demo.html feat: better composer etc 2026-05-04 22:19:16 -05:00
README.md feat(desktop): thin installer + first-launch install.ps1 bootstrap 2026-05-18 02:26:46 -04:00
tsconfig.json feat: move dashboard to apps/ so we can share ws proto 2026-05-02 13:38:49 -05:00
vite.config.ts feat: glass ui pass 2026-05-16 19:21:33 -05:00

Hermes Desktop

Native Electron shell for Hermes. It packages the desktop renderer, a bundled Hermes source payload, and installer targets for macOS and Windows.

Setup

Install workspace dependencies from the repo root so apps/desktop, apps/dashboard, and apps/shared stay linked:

npm install

For Python, you have two options:

Option A — let the desktop provision it for you (recommended for first-time setup): just run npm run dev. On first launch the desktop creates a venv at HERMES_HOME/hermes-agent/venv and runs pip install -e . against the resolved Hermes source automatically. Requires Python 3.11+ on PATH.

Option B — share an existing CLI install: if you already ran scripts/install.ps1 / scripts/install.sh, that's the same layout the desktop uses. The desktop reuses your existing venv and editable install — no extra steps. See Runtime Bootstrap below for details.

If you're hacking on Hermes from a clone outside HERMES_HOME/hermes-agent, point the desktop at it explicitly:

HERMES_DESKTOP_HERMES_ROOT=/path/to/your/clone npm run dev

Runtime prerequisites

Hermes Desktop needs:

  • Python 3.11+ — for the agent runtime, dashboard backend, and tool execution. (required)
  • Git for Windows (Windows only) — provides Git Bash, which Hermes' terminal tool calls directly. Linux and macOS already ship a system bash. (required)
  • ripgrep — used by Hermes' search_files tool for fast .gitignore-aware file/content search. Recommended on all platforms; Hermes falls back to grep/find if missing (works but slower and noisier).

The packaged Windows installer (Hermes-*.exe) detects all three at install time. Required items missing are auto-installed via winget install -e --id Python.Python.3.11 --scope user and winget install -e --id Git.Git. The recommended ripgrep is offered as winget install -e --id BurntSushi.ripgrep.MSVC --scope user. If winget isn't available the installer shows manual download URLs and lets you continue. The MSI installer (Hermes-*.msi) doesn't run the prereq page — enterprise deploys are expected to handle prereqs out-of-band.

For dev (npm run dev) the Python and Git Bash checks happen at first launch via the Electron bootstrapper, which throws a clear error if either prereq is missing. Manual install commands you can run yourself:

winget install -e --id Python.Python.3.11 --scope user
winget install -e --id Git.Git
winget install -e --id BurntSushi.ripgrep.MSVC --scope user

Development

cd apps/desktop
npm run dev

npm run dev starts Vite on 127.0.0.1:5174, launches Electron, and lets Electron boot the Hermes backend (hermes dashboard --no-open --tui) on an open port in 9120-9199. This path is for UI iteration and may still show Electron/dev identities in OS prompts.

Useful overrides:

HERMES_DESKTOP_HERMES_ROOT=/path/to/hermes-agent npm run dev
HERMES_DESKTOP_PYTHON=/path/to/python npm run dev
HERMES_DESKTOP_CWD=/path/to/project npm run dev
HERMES_DESKTOP_IGNORE_EXISTING=1 npm run dev
HERMES_HOME=/tmp/throwaway-hermes-home npm run dev
HERMES_DESKTOP_BOOT_FAKE=1 npm run dev
HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=900 npm run dev

HERMES_DESKTOP_IGNORE_EXISTING=1 skips any hermes CLI already on PATH, which is useful when testing the factory-image bootstrap path.

HERMES_HOME overrides the install root (default: %LOCALAPPDATA%\hermes on Windows, ~/.hermes elsewhere) — handy for sandboxed dev runs that shouldn't touch your real config.

HERMES_DESKTOP_BOOT_FAKE=1 adds deterministic per-phase delays to desktop startup so you can validate the startup overlay and progress bar. For convenience, npm run dev:fake-boot enables fake mode with defaults.

On a fresh Hermes profile, Desktop shows a first-run setup overlay after boot. The overlay saves the minimum required provider credential (for example OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY) to the active Hermes .env, reloads the backend env, and then lets the user continue without opening Settings manually.

Dashboard Dev

Run the Python dashboard backend with embedded chat enabled:

hermes dashboard --tui --no-open

For dashboard HMR, start Vite in another terminal:

cd apps/dashboard
npm run dev

Open the Vite URL. The dev server proxies /api, /api/pty, and plugin assets to http://127.0.0.1:9119 and fetches the live dashboard HTML so the ephemeral session token matches the running backend.

Build

npm run build
npm run pack          # unpacked app at release/mac-<arch>/Hermes.app
npm run dist:mac      # macOS DMG + zip
npm run dist:mac:dmg  # DMG only
npm run dist:mac:zip  # zip only
npm run dist:win      # NSIS + MSI

Before packaging, the desktop app no longer bundles a copy of the Hermes Agent Python source. Instead, the packaged Electron app will fetch and install Hermes Agent at first launch via scripts/install.ps1's stage protocol (Windows) — see the bootstrap flow documented in electron/main.cjs. macOS and Linux packaged builds are temporarily non-functional until install.sh gains the same stage protocol; dev workflows on all three platforms continue to work since they resolve a sibling source checkout.

Automated Releases

Desktop installers are published by .github/workflows/desktop-release.yml with two channels:

  • Stable: runs on published GitHub releases and uploads signed artifacts to that release tag.
  • Nightly: runs on main pushes and updates the rolling desktop-nightly prerelease.

The workflow injects a channel-aware desktop version at build time:

  • stable: derived from the release tag (for example v2026.5.5 -> 2026.5.5)
  • nightly: 0.0.0-nightly.YYYYMMDD.<sha>

Artifact names include channel, platform, and architecture:

Hermes-<version>-<channel>-<platform>-<arch>.<ext>

Each run also publishes SHA256SUMS-<platform>.txt so installers can be verified.

Stable release gates

Stable builds fail fast if signing credentials are missing:

  • macOS signing + notarization: CSC_LINK, CSC_KEY_PASSWORD, APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_ISSUER
  • Windows signing: WIN_CSC_LINK, WIN_CSC_KEY_PASSWORD

Stable macOS builds also validate stapling and Gatekeeper assessment in CI before upload.

Icons

Desktop icons live in assets/:

  • assets/icon.icns
  • assets/icon.ico
  • assets/icon.png

The builder config points at assets/icon. Replace these files directly if the app icon changes.

Testing Install Paths

Use the package-local test scripts from this directory:

npm run test:desktop:all
npm run test:desktop:existing
npm run test:desktop:fresh
npm run test:desktop:dmg
npm run test:desktop:platforms

test:desktop:existing builds the packaged app and opens it normally. It should use an existing hermes CLI if one is on PATH, preserving the users real ~/.hermes config.

test:desktop:fresh builds the packaged app and launches it in a throwaway fresh-install sandbox. It sets HERMES_DESKTOP_IGNORE_EXISTING=1, points Electron userData at a temp dir, points HERMES_HOME at a temp dir, and launches through the factory-image bootstrap path without touching your real desktop runtime or ~/.hermes.

test:desktop:dmg builds and opens the DMG.

test:desktop:platforms runs platform bootstrap-path assertions, including:

  • existing-CLI vs factory-image runtime path selection semantics
  • WSL2 protection against Windows .exe/.cmd/.bat/.ps1 overrides
  • platform-specific runtime import checks (winpty vs ptyprocess)

For fast reruns without rebuilding:

HERMES_DESKTOP_SKIP_BUILD=1 npm run test:desktop:fresh
HERMES_DESKTOP_SKIP_BUILD=1 npm run test:desktop:existing
HERMES_DESKTOP_SKIP_BUILD=1 npm run test:desktop:dmg

Installing Locally

npm run dist:mac:dmg
open release/Hermes-0.0.0-arm64.dmg

Drag Hermes to Applications. If testing repeated installs, replace the existing app.

Runtime Bootstrap

Hermes Desktop shares its install layout with the CLI installers (scripts/install.ps1, scripts/install.sh) so a desktop-only user and a CLI-only user end up with the same files in the same places.

Where things live

HERMES_HOME/                       # %LOCALAPPDATA%\hermes (Windows)
                                   # ~/.hermes (macOS / Linux)
├── hermes-agent/                  # ACTIVE_HERMES_ROOT — git checkout
│   ├── .git/                      # canonical install is always a git checkout
│   ├── hermes_cli/, agent/, ...   # Python source
│   ├── pyproject.toml             # source of truth for deps
│   ├── venv/                      # virtualenv (Scripts\python.exe on Windows,
│   │                              #             bin/python elsewhere)
│   └── .hermes-bootstrap-complete # marker: first-launch install.ps1 succeeded
├── git/                           # PortableGit (Windows; installed by install.ps1)
├── config.yaml                    # user config
├── .env                           # API keys
└── logs/
    ├── desktop.log                # Electron-side boot log
    ├── agent.log
    ├── errors.log
    └── gateway.log

The packaged installer ships only the Electron app — Hermes Agent itself is fetched and installed at first launch by running scripts/install.ps1 (Windows) against the git ref baked into the .exe at build time (see apps/desktop/scripts/write-build-stamp.cjs).

Resolution order

The desktop resolves a Hermes backend in this order:

  1. HERMES_DESKTOP_HERMES_ROOT — explicit dev override.
  2. Repo source root — only when running npm run dev from a checkout. Takes precedence over HERMES_HOME/hermes-agent so devs always run their local edits.
  3. HERMES_HOME/hermes-agent if the .hermes-bootstrap-complete marker is present. The marker attests that install.ps1 succeeded and the user finished initial configuration; we trust the install and skip the bootstrap flow on every launch after the first.
  4. Existing hermes CLI on PATH (skipped when HERMES_DESKTOP_IGNORE_EXISTING=1).
  5. Pip-installed hermes_cli module via system Python.
  6. None of the above → bootstrap-needed sentinel. The desktop's first-launch wizard runs scripts/install.ps1 stages, then writes the marker on success.

First-launch flow on a packaged install

  1. resolveHermesBackend() returns kind: 'bootstrap-needed'.
  2. The renderer shows the install overlay; main fetches scripts/install.ps1 from GitHub at the pinned commit (from install-stamp.json).
  3. Main drives install.ps1 -Manifest to get the stage list, then iterates install.ps1 -Stage <name> -NonInteractive -Json with live progress events to the renderer.
  4. On all stages succeeding, main writes .hermes-bootstrap-complete with { schemaVersion, pinnedCommit, pinnedBranch, completedAt, desktopVersion }.
  5. Renderer hands off to the existing onboarding overlay (API key / model / persona).
  6. Subsequent launches see the marker and skip everything in steps 1-5.

Updates

Once bootstrapped, the install is a real git checkout. Updates flow through the in-app update path (applyUpdates()git fetch && git pull --ff-only against the configured branch) or hermes update from the CLI. Both check pyproject.toml drift and re-run pip install -e . only when needed.

A user who installed via scripts/install.ps1 directly (so HERMES_HOME/hermes-agent/.git exists but no .hermes-bootstrap-complete marker) is detected via resolver step 4 (their hermes CLI on PATH) and the desktop reuses their install without re-running the bootstrap.

Debugging

Desktop boot logs are written to:

HERMES_HOME/logs/desktop.log     # %LOCALAPPDATA%\hermes\logs\desktop.log on Windows
                                  # ~/.hermes/logs/desktop.log on macOS / Linux

If the UI reports Desktop boot failed, check that log first. It includes the backend command output and recent Python traceback context.

To force a fresh first-launch bootstrap (rare — useful for development / dogfooding the install flow):

# macOS / Linux
rm "$HOME/.hermes/hermes-agent/.hermes-bootstrap-complete"

# Windows (PowerShell)
Remove-Item "$env:LOCALAPPDATA\hermes\hermes-agent\.hermes-bootstrap-complete"

For a full reset of just the Python venv (rare — usually only needed if the venv is broken):

# macOS / Linux
rm -rf "$HOME/.hermes/hermes-agent/venv"

# Windows (PowerShell)
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\hermes\hermes-agent\venv"

To reset stale macOS microphone permission prompts:

tccutil reset Microphone com.github.Electron
tccutil reset Microphone com.nousresearch.hermes

Verification

Run before handing off installer changes:

npm run fix
npm run type-check
npm run lint
npm run test:desktop:all

Current lint may report existing warnings, but it should exit with no errors.