* feat(desktop): add Webhooks page for subscription CRUD Brings the desktop GUI to parity with the dashboard's Webhooks page. Adds a /webhooks route that lists webhook subscriptions, enables the webhook gateway platform, and creates/toggles/deletes subscriptions, hitting the same /api/webhooks* endpoints the dashboard and CLI use. - types/hermes.ts: WebhookRoute, WebhooksResponse, WebhookCreatePayload, WebhookCreateResponse, WebhookEnableResponse - hermes.ts: getWebhooks, enableWebhooks, createWebhook, deleteWebhook, setWebhookEnabled (profile-scoped) + type re-exports - app/webhooks/index.tsx: WebhooksView (enable card, restart banner, subscription list with copy/toggle/delete, create dialog with one-time secret reveal); optimistic toggle, profile re-home - routing: routes.ts, contrib/surfaces.tsx, chat/route-tile.tsx - nav: command palette, keybinds (nav.webhooks), sidebar row - i18n: en + zh full, types interface; ja/zh-hant fall back to English - test: webhooks-rest.test.ts covers the REST helper contracts * feat(desktop): surface Webhooks in the status bar instead of nav Moves the Webhooks entry point from the sidebar nav / command palette / keybind to a status bar action next to Cron, matching where scheduled jobs live. The /webhooks route, page, and REST helpers are unchanged. - use-statusbar-items.tsx: add webhooks action (Globe icon) after cron - i18n: shell.statusbar.webhooks / openWebhooks (en, zh, types) - revert nav wiring: sidebar row, command palette entry, nav.webhooks keybind action + label, commandCenter.nav webhooks entry * feat(desktop): add skills field to webhook create form Exposes the backend's per-subscription skills list in the create dialog (comma-separated) and shows skill badges on subscription rows, so this page covers the skill-backed endpoint case as well as general CRUD. - create form: Skills input; passes skills[] to createWebhook - rows: render skill badges - i18n: fieldSkills / fieldSkillsPlaceholder (en, zh, types) Consolidates the skill-endpoint framing from #42817. Co-authored-by: LionGateOS <98371158+LionGateOS@users.noreply.github.com> * feat(desktop): render Webhooks as an overlay instead of a full page Webhooks took over the whole workspace/chat pane because 'webhooks' was missing from OVERLAY_VIEWS, so it routed through PageSearchShell while cron rendered through the Panel overlay. Add it to OVERLAY_VIEWS and wire it up like cron: mount WebhooksView as a floating overlay in wiring.tsx, render null for the webhooks route in the workspace table, and convert WebhooksView from PageSearchShell to the Panel primitive with an onClose. Drop webhooks from route-tile BUILTIN_PAGES so it can't be tiled as a page either. * fix(desktop): place webhook URL copy button next to the URL The URL span used flex-1, stretching it across the row and pushing the copy button to the far right. Drop flex-1 so the span sizes to content and the copy button sits directly after the URL. * fix(desktop): top-align the deliver-only checkbox with its wrapped label The label used h-9 items-center, centering the checkbox against the two-line hint text. Switch to items-start so the checkbox aligns to the first line. * feat(desktop): give Webhooks a cron-style master/detail layout Replace the single-column subscription list with the same Panel master/detail cron uses: a left PanelList of subscription rows (status dot + kebab menu) capped by a PanelAddButton, and a right PanelDetail showing the selected subscription's deliver/events/skills, URL with copy, description, and prompt. Enable/restart banners sit above the body; the empty state keeps its own New subscription action. * fix(desktop): drop header on the Webhooks empty state to match cron The zero-subscriptions state still rendered the PanelHeader with the title and the refresh/new buttons on the right. Remove it so the empty state is just the centered PanelEmpty (icon, message, New subscription), matching the cron empty state. Also drop the header from the loading state. * fix(desktop): top-align deliver-only checkbox and its wrapped label Drop the min-h-9/pt-1.5 baseline shim that pushed the row down and made the wrapped hint look misaligned. Use plain items-start with a mt-0.5 on the checkbox so it sits at the first line, and wrap the hint in a leading-snug span. * fix(desktop): drop header refresh/new buttons; + button owns create flow The populated Webhooks header carried a refresh icon and a New subscription button. Remove both — the PanelAddButton at the bottom of the list is the create flow, matching cron. Profile-change reload and the refresh hotkey still run; the restart banner keeps its own refresh. * fix(desktop): nudge deliver-only checkbox down 2px Bump the checkbox top margin from mt-0.5 to mt-[4px] so it sits level with the first line of the wrapped hint. * refactor(desktop): reuse shared primitives on the Webhooks page Address OutThisLife's review — stop reinventing primitives the app already ships: - copy: drop the local navigator.clipboard button for the shared CopyButton (routes through the Electron clipboard bridge + haptic + error state instead of swallowing failures) - banners: enable/restart callouts now use Alert variant=warning (primary color-mix tokens) instead of a hand-rolled amber palette - toggle: detail Enable/Disable is a Switch (messaging idiom), not a text ghost button - checkbox: create dialog uses the Checkbox primitive, not a raw input Rows/chips already moved to PanelListRow/PanelDetail/PanelPill in the earlier cron-layout pass. Left the main-list fetch on manual load and the local Field helper: cron itself does both, so useQuery here would diverge from the reference idiom rather than align with it. * refactor(desktop): move Webhooks fetch to the react-query layer Replace the manual useState/useEffect load with useQuery keyed by ['webhooks', profileScope] — profile change re-fetches automatically, no effect. reload() invalidates the query; the optimistic toggle writes the cache via queryClient.setQueryData then invalidates so backend truth wins. Load failures surface via an error-watching effect (react-query v5 dropped useQuery onError). Refresh hotkey calls refetch(). Left the create-dialog Field helper as-is: settings ListRow is a side-by-side settings row, wrong for a stacked dialog form, and cron's editor dialog (the reference) defines the same local Field. * refactor(desktop): drop bespoke Field for shared ListRow/ToggleRow Remove the local Field wrapper entirely. Every create-dialog field now uses settings ListRow (wide, so label stacks over the full-width control), the deliver-only pref uses ToggleRow (ListRow + Switch, haptic baked in) instead of a bare Checkbox, and the created URL/secret reveal rows use ListRow too. No component in this file is hand-rolled anymore. * fix(desktop): pair webhook create fields into a 2-column layout The single-column dialog scrolled awkwardly. Group fields: name + description side by side, prompt full-width under them, events + skills side by side, deliver-to + deliver-only side by side. Drop the deliver-only help text and rename the label to 'Deliver payload only' (remove the now-unused fieldDeliverOnlyHint i18n key from en/zh/types). * fix(desktop): align Webhooks page with cron conventions and DESIGN.md - Delete copy uses deleteDescPrefix + bolded name + deleteDescSuffix (no em-dash) - Drop the duplicated detail-header Switch + Trash2; enable/disable and delete live only in the row kebab, matching CronJobDetail - Collapse three copyable-value chromes into one flat token-backed CopyValueRow - Delete success toast gains a w.deleted title - Replace hand-rolled delete Dialog with shared ConfirmDialog --------- Co-authored-by: LionGateOS <98371158+LionGateOS@users.noreply.github.com> |
||
|---|---|---|
| .. | ||
| assets | ||
| e2e | ||
| electron | ||
| pr-assets | ||
| public | ||
| scripts | ||
| src | ||
| AGENTS.md | ||
| components.json | ||
| DESIGN.md | ||
| eslint.config.mjs | ||
| index.html | ||
| package.json | ||
| playwright.config.ts | ||
| preview-demo.html | ||
| README.md | ||
| tsconfig.e2e.json | ||
| tsconfig.electron.json | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
Hermes Desktop ☤
The native desktop app for Hermes Agent — the self-improving AI agent from Nous Research. Same agent, same skills, same memory as the CLI and gateway, in a polished native window — chat with streaming tool output, side-by-side previews, a file browser, voice, and settings, no terminal required. Available for macOS, Windows, and Linux.
| Chat with the full agent | Streaming responses, live tool activity, structured tool summaries, and the same conversation history as every other Hermes surface. |
| Side-by-side previews | Render web pages, files, and tool outputs in a right-hand pane while you keep chatting. |
| File browser | Explore and preview the working directory without leaving the app. |
| Voice | Talk to Hermes and hear it back. |
| Settings & onboarding | Manage providers, models, tools, and credentials from a real UI. First-run setup gets you to your first message in seconds. |
| Stays current | Built-in updates pull the latest agent and rebuild the app in place. |
Install
Install with Hermes (recommended)
Already have the Hermes CLI? Just run:
hermes desktop
It builds and launches the GUI against your existing install — same config, keys, sessions, and skills. On first launch Hermes walks you through picking a provider and model; nothing else to configure.
Prebuilt installers
Prebuilt installers are built and distributed via the Hermes Desktop website..
Updating
The app checks for updates in the background and offers a one-click update when one is ready. You can also update any time from the CLI:
hermes update
Requirements
The installer handles everything for you (Python 3.11+, a portable Git, ripgrep).
Development
Want to hack on the app itself? Install workspace deps from the repo root once, then run the dev server from this directory:
npm install # from repo root — links apps/desktop, web, apps/shared
cd apps/desktop
npm run dev # Vite renderer + Electron, which boots the Python backend
Point the app at a specific source checkout, or sandbox it away from your real config:
# throwaway HERMES_HOME, separate Electron userData, distinct app name to avoid the single-instance lock
../scripts/dev-sandbox.sh npm run dev
HERMES_DESKTOP_HERMES_ROOT=/path/to/clone npm run dev
HERMES_HOME=/tmp/throwaway npm run dev
npm run dev:fake-boot # exercise the startup overlay with deterministic delays
Building installers
npm run dist:mac # DMG + zip
npm run dist:win # NSIS + MSI
npm run dist:linux # AppImage + deb + rpm
npm run pack # unpacked app under release/ (no installer)
Installers are built and uploaded to GitHub Releases manually. macOS/Windows signing & notarization happen automatically when the relevant credentials are present in the environment (CSC_LINK / CSC_KEY_PASSWORD / APPLE_* for macOS, WIN_CSC_* for Windows).
How it works
The packaged app ships the Electron shell and a native React chat surface. On
first launch it can install the Hermes Agent runtime into HERMES_HOME
(~/.hermes, or %LOCALAPPDATA%\hermes on Windows), using the same layout as a
CLI install.
The app has three boundaries:
- Electron resolves and validates a runnable backend, owns native filesystem/git/window capabilities, and exposes a narrow preload bridge.
- React owns the Desktop routes, panes, interaction state, and
@assistant-ui/reacttranscript. - Hermes Agent runs as a headless
hermes serveprocess and exposes thetui_gatewayJSON-RPC/WebSocket API. The renderer connects throughapps/shared, which is also used by the browser dashboard.
Backend resolution is an ordered ladder:
HERMES_DESKTOP_HERMES_ROOT- the current source checkout during development
- a completed managed install
HERMES_DESKTOP_HERMES, orhermesonPATH- a system Python that can import the Hermes runtime
- the first-launch bootstrap installer
Candidates are probed before use; an existing shim or interpreter is not enough.
A runtime that predates serve falls back to headless
dashboard --no-open. This is compatibility for the backend command only and
does not launch or embed the dashboard UI.
The Electron orchestration entry point is electron/main.ts; pure resolution,
probe, hardening, and platform policies live in focused modules beside it. The
renderer is under src/, with shared atoms in src/store and transport/native
adapters in src/lib.
Before changing the app, read:
AGENTS.md: architecture, state ownership, resolver/fallback, transport, performance, and testing rules.DESIGN.md: visual system, information architecture, motion, direct manipulation, and keyboard behavior.
Connections, projects, and switching
Desktop supports a managed local backend, explicit remote gateways, and Hermes Cloud connections. Remote and cloud modes use the same remote-capability path; authentication and discovery differ, not the renderer feature model.
Projects are the workspace abstraction. A project may own multiple folders, repositories, worktrees, and sessions; a bare new chat remains detached unless the user enters a project or configures a default project directory. Use the Projects UI rather than adding a second per-session folder-picker workflow.
Changing profiles or connection modes is a soft workspace switch, not another cold boot. The shell and current management overlay remain mounted while gateway-bound nanostores are wiped, query-backed data is invalidated, and the new connection repopulates skeletons. This prevents rows or transcripts from the previous gateway bleeding into the next one.
Verification
Run before opening a PR (lint may surface pre-existing warnings but must exit cleanly):
npm run fix
npm run typecheck
npm run lint
npm run test:ui
npm run test:desktop:platforms
Run npm run test:desktop:all for install, boot, update, packaging, or other
release-path changes.
Troubleshooting
Boot logs land in HERMES_HOME/logs/desktop.log (includes backend output and recent Python tracebacks) — check it first if the app reports a boot failure.
macOS / Linux:
# Force a clean first-launch setup
rm "$HOME/.hermes/hermes-agent/.hermes-bootstrap-complete"
# Rebuild a broken Python venv
rm -rf "$HOME/.hermes/hermes-agent/venv"
# Reset a stuck macOS microphone prompt (macOS only)
tccutil reset Microphone com.nousresearch.hermes
Windows (PowerShell):
# Force a clean first-launch setup
Remove-Item "$env:LOCALAPPDATA\hermes\hermes-agent\.hermes-bootstrap-complete"
# Rebuild a broken Python venv
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\hermes\hermes-agent\venv"
The default Hermes home on Windows is
%LOCALAPPDATA%\hermes. Set theHERMES_HOMEenv var if you've relocated it.
Community
- 💬 Discord
- 📖 Documentation
- 🐛 Issues
License
MIT — see LICENSE.
Built by Nous Research.