* feat(desktop): hover-reveal collapsed chat sidebar as a fixed overlay When the sessions sidebar is collapsed, hovering the left edge now floats it back in as a fixed overlay over the main content instead of just being hidden. The collapsed grid track stays at 0px so the panel never reserves space — it slides over whatever's underneath and retracts on pointer-leave. - PaneShell: new hoverReveal prop. When a pane is collapsed + hoverReveal, render an edge hot-zone + a side-anchored floating panel (absolute, full height, honors any persisted resize width) that slides in on hover/focus. - ChatSidebar: force the (otherwise opacity-0 when collapsed) sidebar fully visible + interactive while the overlay is revealed, via an in-data-[pane-hover-reveal=open] variant. - desktop-controller: opt the chat-sidebar pane into hoverReveal. * feat(desktop): lower window minWidth 900→400 Lets the window shrink to a narrow rail (e.g. for the collapsed hover-reveal sidebar) instead of being floored at 900px. * fix(desktop): render full sidebar content in hover-reveal overlay The hover-reveal overlay showed only the nav rail — session rows, search, pinned/recents were gated behind `sidebarOpen` (false while collapsed), so they never mounted in the floated panel. Add a $sidebarRevealed store the PaneShell overlay drives via a new onHoverRevealChange callback, and gate ChatSidebar's content on `sidebarOpen || sidebarRevealed` (contentVisible) instead of raw open state. The overlay now shows the complete sidebar. * fix(desktop): drop shadow on hover-reveal sidebar overlay * feat(desktop): hover-reveal the file-browser sidebar too The reveal mechanism already lives in the shared Pane primitive — the right rail just opts in with hoverReveal. Its content renders unconditionally, so (unlike the chat sidebar) it needs no extra content-visibility gating. * clean(desktop): tighten hover-reveal pane code KISS pass — flatten the translate ternary, derive a single `revealed`, inline the edge style, drop the redundant set-guard, and trim comments to the house one-liner style. No behavior change. * fix(desktop): stop hiding sidebar nav labels on narrow windows The nav labels (New session, Skills, …) and the ⌘N hint were gated on a viewport breakpoint (max-[46.25rem]:hidden), so shrinking the window hid them even when the sidebar itself was wide — including in the hover-reveal overlay. Drop the gate; the label already truncates (min-w-0 flex-1) so it ellipsizes gracefully in a narrow rail, and contentVisible already hides it when collapsed to the icon rail. * feat(desktop): auto-collapse both sidebars below 600px into hover-reveal Add a Pane `forceCollapsed` prop — collapses the track without writing to the store (so the saved open state restores when the window widens) while keeping hoverReveal alive (unlike `disabled`, which suppresses it). desktop-controller watches (max-width: 600px) and force-collapses the chat sidebar + file browser, so on a narrow window both rails get out of the way and the hover-reveal overlay becomes the way in. * feat(desktop): hover-intent + refined easing for sidebar reveal - Gate the reveal on pointer velocity: the full-height edge hot-zone now only arms on a slow, deliberate pass (<=0.55 px/ms). Fast sweeps toward the titlebar/statusbar — or off the window — blow past the threshold and never trigger, so the wide hit area stops being a nuisance. - Swap the slide easing to cubic-bezier(0.32,0.72,0,1) at 260ms (snappy-out, soft-land) for a more serious-app feel. * fix(desktop): don't reveal sidebar during window resize Resizing the window parks the cursor on the screen edge and fires slow pointermoves over the hot-zone, reading as deliberate intent. Guard the reveal on (a) e.buttons !== 0 — any button-held drag, incl. edge-resize — and (b) a 250ms cooldown after any window resize event. * feat(desktop): hoverIntent-style poll gate + inert contents during slide Replace the single-sample velocity check (too eager — fired on any one slow move, incl. resize drift) with a port of Brian Cherne's hoverIntent: poll the pointer every 90ms and only arm once it has *settled* (moved <5px between two consecutive polls inside the edge zone). Fly-bys, pass-throughs, and resize drift never produce two close samples in a row, so they don't trigger. Also keep the revealed panel's CONTENTS pointer-events-none until the slide-in transition finishes (onTransitionEnd → settled), so you can't misclick a session row mid-animation. Resets on retract. * fix(desktop): no cursor/hit-test leak before reveal settles The edge hot-zone showed cursor:pointer the instant the pointer touched it — before the panel was armed or in view. And contents were inert but the panel itself still hit-tested, so the cursor could flip mid-slide. Fix: hot-zone is cursor-default (it's invisible), and the whole panel is pointer-events-none until revealed && settled, so the cursor never changes or lands on a row before the slide-in finishes. * fix(desktop): geometry-driven close so revealed panel always retracts The revealed panel relied on its own onPointerLeave to close — but a panel that slid in under a still cursor (or whose contents were inert during the slide) never fires enter/leave, so it got stuck open (esp. the file browser). onTransitionEnd also bubbled from the file-tree's own row transitions, tripping the settled flag wrongly. Replace with a document-level pointermove watcher that closes once the cursor leaves the panel's bounding rect + a 24px grace — independent of pointer-events state or what the contents do. Gate interactivity on a simple slide-duration timer (interactive) instead of the fragile transitionEnd, so the cursor still can't flip or land on a row before the panel is in view. * feat(desktop): make sidebar toggle shortcuts reveal when force-collapsed mod+b / mod+j were no-ops on a narrow (force-collapsed) window — they flipped the store but the pane ignores it. Now the toggle handlers also dispatch PANE_TOGGLE_REVEAL_EVENT; a force-collapsed Pane listens (only while overlayActive) and flips its hover-reveal, so the shortcut floats the rail in (and back out) at this responsive breakpoint. * refactor(desktop): name the 600px sidebar collapse breakpoint Hoist the inline '(max-width: 600px)' literal into SIDEBAR_COLLAPSE_BREAKPOINT_PX + SIDEBAR_COLLAPSE_MEDIA_QUERY in layout-constants, so the responsive collapse point is a single named source of truth instead of a magic string in the controller. * tweak(desktop): sidebar auto-collapse breakpoint 600px -> 768px 768 is the standard md breakpoint and a more honest 'no room to dock' point. * tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms * Revert "tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms" This reverts commit 6009a132008105f5f871370d554a5097be3090af. * perf(desktop): pre-mount hover-reveal contents to kill slide-in stall The reveal mounted the (heavy, virtualized) sidebar contents in the same frame the slide started, so the browser stalled painting the transform until the mount finished — a ~100-200ms beat before the panel moved, very visible on the instant keyboard toggle (hover masked it via the 90ms intent poll). Report overlayActive (collapsed-overlay mode) rather than the live reveal state to the mount consumer, so contents stay mounted off-screen while collapsed and reveal is a pure transform. Visibility is still driven separately by the data-pane-hover-reveal attr + the slide transform. * fix(desktop): make reveal hotkey spammable Two throttles on the reveal toggle: - The handler fired both the reveal event AND toggleSidebarOpen() per press; the store write hits localStorage synchronously every keystroke + recomputes the grid, janking rapid presses. When collapsed, only dispatch the reveal event (the store toggle was a no-op anyway). - The geometry close-watcher slammed a keyboard-opened panel shut on the first stray pointermove (trackpad jitter), fighting hotkey spam. Keyboard reveals now ignore geometry until the cursor actually enters the panel, then the mouse takes over. * fix(desktop): inset reveal hot-zone past the OS window-resize gutter The hot-zone sat flush at the window edge (left-0/right-0), overlapping the OS resize grab strip — reaching to drag-resize naturally slows the cursor there, which hoverIntent reads as settled and reveals before the resize drag even starts. Inset the hot-zone 8px so the outermost edge stays a pure resize/drag region and only an intentful move just inside it arms a reveal. * fix(desktop): keep reveal hot-zone at edge, gate arming past resize gutter Insetting the hot-zone made it unreachable when moving fast. Instead, anchor the zone flush at the edge (w-4, always captures the pointer) but only ARM the reveal when the cursor settles >=8px in from the edge — so a resize-reach that parks on the outermost OS grab strip never triggers, while a deliberate move into the zone still does. Keeps polling while in the gutter so moving inward still arms. * refactor(desktop): rebuild hover-reveal as pure CSS, delete the JS state machine The hand-rolled pointer state machine (hoverIntent poll, refs, timers, document pointermove geometry-close, interactive gate, resize cooldowns, keyboard-held suppression) was fragile and side/instance-specific — hover broke on the right rail, keyboard toggles triggered phantom animations, resize popped it open. Replace all of it with the native primitive: CSS group-hover drives the slide transform; a transition-delay on enter (instant on leave) is the hover-intent gate (a fast pass-by doesn't dwell long enough to open); a thin edge trigger inset past the OS resize grab strip arms it; and a single `forced` bool (data-forced, toggled by the keyboard event) pins it open. Side-agnostic by construction — group-hover doesn't care which edge or which pane. Net: ~200 lines of imperative pointer logic → ~40 lines of declarative CSS. * fix(desktop): don't animate hover-reveal panel across viewport on side flip Flipping panes changed the off-screen transform from -translateX (off the left) to +translateX (off the right). transition-transform interpolated between them, passing through translate-x-0 (fully on-screen) mid-way — so the hidden panel visibly slid across the window to reach its new hiding spot. Key the panel on side so it remounts off-screen on the new edge with no transition to play. * clean(desktop): tighten hover-reveal markup KISS pass on the CSS-driven reveal: reuse the existing `side` instead of a local `left`, move the static duration/ease to inline style (drop two single-use CSS vars + their arbitrary-value classes, keep only the state-dependent enter-delay var), and trim comments to the house one-liner density. No behavior change. * fix(desktop): inset titlebar past traffic lights when sidebar is force-collapsed The titlebar content inset (clearing the macOS traffic lights) keyed off the stored sidebarOpen/fileBrowserOpen, but below the collapse breakpoint both rails are force-collapsed so the left edge is uncovered while the store still says open — content (the intro wordmark) overflowed under the lights. Gate leftEdgePaneOpen on !narrowViewport using the shared SIDEBAR_COLLAPSE_MEDIA_QUERY. Also rename the now-misleading reveal plumbing to match what it actually does: onHoverRevealChange -> onOverlayActiveChange, $sidebarRevealed -> $sidebarOverlayMounted (+ setter/consumer). It reports/stores collapsed-overlay mode (mount gate), not live reveal state. * feat(desktop): small --nous-shadow lift on revealed hover-reveal panels Add a --nous-shadow token (white-based on light, black-based on dark) and apply it to the floating sidebar panel only while revealed (group-hover / data-forced) so it reads as lifted off the surface. No shadow on the off-screen panel. * feat(desktop): shadow-reveal lift on revealed hover-reveal panels Mirror the --shadow-nous layered falloff into a new --shadow-reveal token whose drop color flips per mode (white on light, black on dark) via --shadow-reveal-raw set in :root / :root.dark. Apply the generated shadow-reveal utility to the floated panel only while revealed (group-hover / data-forced). Leaves the shared --shadow-nous untouched. * feat(desktop): use tuned reveal shadow, drop per-mode token Replace the --shadow-reveal token machinery with Brooklyn's tuned literal (0 -18px 18px -5px #0000003b) inline per-panel via --reveal-shadow, y-offset sign flipped for the right side. Same color both modes. Reverts styles.css to pristine (token removed). * fix(desktop): use the reveal shadow verbatim, don't invert it per side Flipping the y-offset sign for the right side inverted the shadow's direction (cast-up -> cast-down), making it read heavier — not a mirror. The mirror axis for a left/right panel is offset-x, which is 0 here, so both sides take the tuned value as-is: 0 -18px 18px -5px #0000003b. * clean(desktop): hoist reveal shadow to a named const Move the inline reveal-shadow literal to HOVER_REVEAL_SHADOW alongside the other HOVER_REVEAL_* tuning consts; drop the now-stale per-side comment. * fix(desktop): truncate titlebar title before the right tool cluster The session title used a hardcoded max-w-[52vw] that's blind to where the right-side tools start, so it ran under them at narrow widths / with pane tools present. Bound the title container by the same vars the titlebar drag region uses (--titlebar-content-inset + --titlebar-tools-right + --titlebar-tools-width) so it truncates exactly at the cluster's left edge. * fix(desktop): responsive markdown tables — floor width + nowrap headers The wrapper had overflow-x-auto but the table was w-full with auto layout, so instead of scrolling it crushed columns until even header words broke mid-word (Tim/e, Nig/ht). Add a min-w-[18rem] floor so it scrolls horizontally when the column is narrower than readable, and whitespace-nowrap on th so headers never break mid-word. Above the floor it still wraps cells naturally. * fix intro |
||
|---|---|---|
| .. | ||
| assets | ||
| electron | ||
| public | ||
| scripts | ||
| src | ||
| .prettierrc | ||
| components.json | ||
| DESIGN.md | ||
| eslint.config.mjs | ||
| index.html | ||
| package.json | ||
| preview-demo.html | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.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:
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 only the Electron shell. On first launch it installs the Hermes Agent runtime into HERMES_HOME (~/.hermes, or %LOCALAPPDATA%\hermes on Windows) — the same layout a CLI install uses, so the two are interchangeable. The renderer (React, in src/) talks to a hermes dashboard backend over the standard gateway APIs and reuses the embedded TUI rather than reimplementing chat. The install, backend-resolution, and self-update logic all live in electron/main.cjs.
Verification
Run before opening a PR (lint may surface pre-existing warnings but must exit cleanly):
npm run fix
npm run type-check
npm run lint
npm run test:desktop:all
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.