refactor(desktop+dashboard): shared WebSocket layer + decouple desktop from dashboard (hermes serve) - #54568
Merged
Merged
Conversation
The Electron desktop app and the web dashboard each carried their own copy of the tui_gateway JSON-RPC WebSocket client plus near-identical auth'd WS-URL construction. The dashboard's copy was the historical source of the "is the dashboard required to run the desktop app?" confusion, since the two surfaces looked coupled. Consolidate the genuinely shared transport into the existing framework-agnostic `@hermes/shared` package so both surfaces consume it independently — neither app depends on the other: - Move `resolveGatewayWsUrl` + `GatewayReauthRequiredError` (single-use OAuth ticket re-mint vs long-lived token fallback) into `@hermes/shared`; desktop now imports them directly. - Add `buildHermesWebSocketUrl`, one base-path/scheme/auth-aware URL builder, and route every dashboard WS endpoint through it (`/api/ws`, `/api/events`, `/api/pty`, plugin WS URLs). - Reduce the dashboard `GatewayClient` to a thin subclass of the shared `JsonRpcGatewayClient`, deleting ~210 lines of duplicated pending-call /event-dispatch/connect plumbing while keeping its dashboard-specific ticket-vs-token auth selection. - Drop the stale "start it with --tui" chat banner, which implied the dashboard flag was required. Behavior is preserved on both surfaces; the dashboard additionally inherits the shared client's 15s connect timeout (previously desktop-only), so a hung connect now fails fast instead of pinning the composer in "connecting".
Ensure intentional client closes mark the transport closed and reject pending RPCs immediately instead of relying on a browser close event that can be ignored after the socket reference is cleared.
Keep dashboard pages and components on the dashboard API helper instead of calling the raw shared URL primitive directly. The shared helper remains the single low-level implementation; web/src/lib/api.ts is the dashboard-specific facade for auth, base path, and ticket minting.
Run the package-appropriate Prettier config on the shared WebSocket files so the extracted helpers match the surrounding desktop/shared TypeScript style.
The shared websocket package is a web file: dependency but was excluded by .dockerignore and never copied into the Docker build context. Also fix tsc -b errors: expose buildWsUrl on api and drop the GatewayClient state getter that conflicted with the shared base class.
Contributor
🔎 Lint report:
|
The desktop app spawns a headless `hermes dashboard --no-open` backend and talks to it through the shared @hermes/shared WebSocket client — it never runs or requires the browser dashboard UI. Spell this out in the desktop README, the desktop docs page, and AGENTS.md so "dashboard" stops reading as a desktop prerequisite.
…nches `dashboard` The desktop app spawned `hermes dashboard --no-open` as its backend, which made the dashboard look like a desktop prerequisite. Add a dedicated headless `hermes serve` command that boots the same gateway (shared cmd_dashboard / start_server) but never opens a browser, and point the desktop backend spawn exclusively at it. dashboard and serve are now independent surfaces — neither launches the other. - subcommands/dashboard.py: factor shared server args; add `serve` parser (always headless; accepts legacy --no-open as a no-op) - main.py: register serve in _BUILTIN_SUBCOMMANDS + coalesce set + gui-log detection; extend stale-backend reaper patterns to match `serve` - desktop electron: spawn `serve`, rename dashboardArgs -> backendArgs, update comments + windows-child-process test assertions - docs: desktop README, desktop.md (incl. remote-backend), AGENTS.md, and cli-commands.md now describe `hermes serve` as the desktop/headless backend
…absent `hermes serve` is newer than the desktop binary's release cadence, so a new app launched against an un-upgraded managed install / PATH `hermes` would crash on an unknown subcommand and brick the user mid-upgrade. Detect whether the resolved runtime registers `serve` (fast source read of its dashboard.py, with a one-time CLI probe fallback) and rewrite the backend argv to the legacy `dashboard --no-open` only when it does not. Happy path (current runtimes) pays nothing and still spawns `serve`. - electron/backend-command.cjs: pure serve/dashboard argv helpers + serve- source detection (unit-tested in backend-command.test.cjs) - main.cjs: backendSupportsServe() cache + getBackendArgsForRuntime() guard at both backend spawn sites; expose `root` from the Windows venv unwrap so the fast source check covers Windows too - docs: note the backward-compat fallback in README, desktop.md, AGENTS.md
Add a focused contract test for the headless `serve` command (routes to the shared dashboard handler, headless by default while `dashboard` is not, accepts the legacy --no-open, shares the same runtime/lifecycle flag surface). Also refresh the dashboard.py module docstring to cover both commands.
Clears the ty diff bot's warnings on the new test: pass real callables to build_dashboard_parser (not object()) and replace the pytest.mark.parametrize with a plain loop so the file is stdlib-only.
ethernet8023
approved these changes
Jun 29, 2026
OutThisLife
added a commit
that referenced
this pull request
Jun 30, 2026
`serve` (added in #54568) reused cmd_dashboard wholesale, so it still behaved like a dashboard: it ran a full vite build every launch, mounted and served the SPA whenever a stray web_dist/ existed, printed "Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless JSON-RPC/WS backend the desktop app and remote clients run — pure socket clients that never load the browser SPA. Mark serve with headless_backend=True (resolved once in cmd_dashboard) and: - skip _build_web_ui entirely on the serve path - export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a dist is present — only the JSON-RPC/WS/API surface is reachable - announce the bind ("Hermes backend listening on host:port") instead of a browser/auth-gated URL - print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy one and the desktop port-discovery regex matches either - preserve serve across the named-profile re-exec so it can't rebuild as dashboard `hermes dashboard` is unchanged (builds + serves the browser UI). Backward compatible: old apps only ever spawn dashboard (legacy token + UI intact) and never invoke serve; the ready-file side channel is name-agnostic. The one behavior change is that a remote `hermes serve` no longer serves the browser dashboard as a side effect — that's `hermes dashboard`'s job. Tests: serve headless_backend contract, SPA-disabled-with-dist, the HERMES_BACKEND_READY desktop parse (17/17 node), and the existing serve/dashboard/web_server suites. AGENTS.md documents the behavior.
1 task
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…socket-layer refactor(desktop+dashboard): shared WebSocket layer + decouple desktop from dashboard (hermes serve)
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…socket-layer refactor(desktop+dashboard): shared WebSocket layer + decouple desktop from dashboard (hermes serve)
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…socket-layer refactor(desktop+dashboard): shared WebSocket layer + decouple desktop from dashboard (hermes serve)
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
`serve` (added in NousResearch#54568) reused cmd_dashboard wholesale, so it still behaved like a dashboard: it ran a full vite build every launch, mounted and served the SPA whenever a stray web_dist/ existed, printed "Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless JSON-RPC/WS backend the desktop app and remote clients run — pure socket clients that never load the browser SPA. Mark serve with headless_backend=True (resolved once in cmd_dashboard) and: - skip _build_web_ui entirely on the serve path - export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a dist is present — only the JSON-RPC/WS/API surface is reachable - announce the bind ("Hermes backend listening on host:port") instead of a browser/auth-gated URL - print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy one and the desktop port-discovery regex matches either - preserve serve across the named-profile re-exec so it can't rebuild as dashboard `hermes dashboard` is unchanged (builds + serves the browser UI). Backward compatible: old apps only ever spawn dashboard (legacy token + UI intact) and never invoke serve; the ready-file side channel is name-agnostic. The one behavior change is that a remote `hermes serve` no longer serves the browser dashboard as a side effect — that's `hermes dashboard`'s job. Tests: serve headless_backend contract, SPA-disabled-with-dist, the HERMES_BACKEND_READY desktop parse (17/17 node), and the existing serve/dashboard/web_server suites. AGENTS.md documents the behavior.
justemu
pushed a commit
to justemu/hermes-agent
that referenced
this pull request
Jul 18, 2026
`serve` (added in NousResearch#54568) reused cmd_dashboard wholesale, so it still behaved like a dashboard: it ran a full vite build every launch, mounted and served the SPA whenever a stray web_dist/ existed, printed "Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless JSON-RPC/WS backend the desktop app and remote clients run — pure socket clients that never load the browser SPA. Mark serve with headless_backend=True (resolved once in cmd_dashboard) and: - skip _build_web_ui entirely on the serve path - export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a dist is present — only the JSON-RPC/WS/API surface is reachable - announce the bind ("Hermes backend listening on host:port") instead of a browser/auth-gated URL - print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy one and the desktop port-discovery regex matches either - preserve serve across the named-profile re-exec so it can't rebuild as dashboard `hermes dashboard` is unchanged (builds + serves the browser UI). Backward compatible: old apps only ever spawn dashboard (legacy token + UI intact) and never invoke serve; the ready-file side channel is name-agnostic. The one behavior change is that a remote `hermes serve` no longer serves the browser dashboard as a side effect — that's `hermes dashboard`'s job. Tests: serve headless_backend contract, SPA-disabled-with-dist, the HERMES_BACKEND_READY desktop parse (17/17 node), and the existing serve/dashboard/web_server suites. AGENTS.md documents the behavior.
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…socket-layer refactor(desktop+dashboard): shared WebSocket layer + decouple desktop from dashboard (hermes serve)
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
`serve` (added in NousResearch#54568) reused cmd_dashboard wholesale, so it still behaved like a dashboard: it ran a full vite build every launch, mounted and served the SPA whenever a stray web_dist/ existed, printed "Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless JSON-RPC/WS backend the desktop app and remote clients run — pure socket clients that never load the browser SPA. Mark serve with headless_backend=True (resolved once in cmd_dashboard) and: - skip _build_web_ui entirely on the serve path - export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a dist is present — only the JSON-RPC/WS/API surface is reachable - announce the bind ("Hermes backend listening on host:port") instead of a browser/auth-gated URL - print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy one and the desktop port-discovery regex matches either - preserve serve across the named-profile re-exec so it can't rebuild as dashboard `hermes dashboard` is unchanged (builds + serves the browser UI). Backward compatible: old apps only ever spawn dashboard (legacy token + UI intact) and never invoke serve; the ready-file side channel is name-agnostic. The one behavior change is that a remote `hermes serve` no longer serves the browser dashboard as a side effect — that's `hermes dashboard`'s job. Tests: serve headless_backend contract, SPA-disabled-with-dist, the HERMES_BACKEND_READY desktop parse (17/17 node), and the existing serve/dashboard/web_server suites. AGENTS.md documents the behavior.
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…socket-layer refactor(desktop+dashboard): shared WebSocket layer + decouple desktop from dashboard (hermes serve)
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
`serve` (added in NousResearch#54568) reused cmd_dashboard wholesale, so it still behaved like a dashboard: it ran a full vite build every launch, mounted and served the SPA whenever a stray web_dist/ existed, printed "Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless JSON-RPC/WS backend the desktop app and remote clients run — pure socket clients that never load the browser SPA. Mark serve with headless_backend=True (resolved once in cmd_dashboard) and: - skip _build_web_ui entirely on the serve path - export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a dist is present — only the JSON-RPC/WS/API surface is reachable - announce the bind ("Hermes backend listening on host:port") instead of a browser/auth-gated URL - print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy one and the desktop port-discovery regex matches either - preserve serve across the named-profile re-exec so it can't rebuild as dashboard `hermes dashboard` is unchanged (builds + serves the browser UI). Backward compatible: old apps only ever spawn dashboard (legacy token + UI intact) and never invoke serve; the ready-file side channel is name-agnostic. The one behavior change is that a remote `hermes serve` no longer serves the browser dashboard as a side effect — that's `hermes dashboard`'s job. Tests: serve headless_backend contract, SPA-disabled-with-dist, the HERMES_BACKEND_READY desktop parse (17/17 node), and the existing serve/dashboard/web_server suites. AGENTS.md documents the behavior.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
`serve` (added in NousResearch#54568) reused cmd_dashboard wholesale, so it still behaved like a dashboard: it ran a full vite build every launch, mounted and served the SPA whenever a stray web_dist/ existed, printed "Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless JSON-RPC/WS backend the desktop app and remote clients run — pure socket clients that never load the browser SPA. Mark serve with headless_backend=True (resolved once in cmd_dashboard) and: - skip _build_web_ui entirely on the serve path - export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a dist is present — only the JSON-RPC/WS/API surface is reachable - announce the bind ("Hermes backend listening on host:port") instead of a browser/auth-gated URL - print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy one and the desktop port-discovery regex matches either - preserve serve across the named-profile re-exec so it can't rebuild as dashboard `hermes dashboard` is unchanged (builds + serves the browser UI). Backward compatible: old apps only ever spawn dashboard (legacy token + UI intact) and never invoke serve; the ready-file side channel is name-agnostic. The one behavior change is that a remote `hermes serve` no longer serves the browser dashboard as a side effect — that's `hermes dashboard`'s job. Tests: serve headless_backend contract, SPA-disabled-with-dist, the HERMES_BACKEND_READY desktop parse (17/17 node), and the existing serve/dashboard/web_server suites. AGENTS.md documents the behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Desktop and the web dashboard each carried their own copy of the WebSocket/JSON-RPC gateway client, and the desktop app booted its backend as
hermes dashboard --no-open. That coupling made the dashboard look like a desktop prerequisite (thehermes dashboard --tui-era confusion) and meant two diverging client implementations.This PR does two things:
@hermes/sharedpackage consumed by bothapps/desktopandweb. No more duplicated client.dashboard. Adds a dedicated headlesshermes servebackend command (same gateway, no browser) and points the desktop app exclusively at it.dashboard(browser UI) andserve(headless backend) are now independent surfaces that share one server — neither launches the other.What changed
Shared layer (
apps/shared→@hermes/shared)JsonRpcGatewayClient(connection state, request/event dispatch, timeouts, deterministicclose()that rejects pending RPCs) and the WS-URL/reauth helpers now live here.apps/desktopconsumes it directly (HermesGateway extends JsonRpcGatewayClient); a thin re-export shim atapps/desktop/src/lib/gateway-ws-url.tskeeps existing desktop imports/tests working.webdepends on it via afile:workspace dep;web/src/lib/gatewayClient.tsdrops ~240 lines of bespoke client and extends the shared one. Dashboard-specific WS auth/URL stays behind theapi.buildWsUrl()facade so components never touch the low-level primitive.hermes serve(headless backend)cmd_dashboard/start_serverwithdashboard; shared runtime flags factored into one helper.serveis always headless; accepts the legacy--no-openas a no-op._BUILTIN_SUBCOMMANDS, the multi-word-arg coalesce set, gui-log-mode detection, and the stale-backend reaper (sohermes updatestill reaps/protects desktop backends).Desktop
hermes serve(washermes dashboard --no-open);dashboardArgs→backendArgs.serveis newer than the desktop release cadence, so a new app against an un-upgraded runtime would crash on an unknown subcommand.backendSupportsServe()detects support (fast read of the runtime'sdashboard.py, one-time CLI probe fallback) and rewrites the argv to the legacydashboard --no-openonly whenserveis absent. Happy path pays nothing.Docker / docs
apps/sharedis awebfile:dep, so it's added to the Docker build context (.dockerignoreun-ignore + twoCOPY apps/shared/). This was the original CI failure.desktop.md(incl. remote-backend),AGENTS.md, andcli-commands.mddescribeserveas the desktop/headless backend;dashboardstays the browser UI.Tests
tests/hermes_cli/test_serve_command.py(new) —serveroutes to the shared handler, is headless by default whiledashboardis not, tolerates--no-open, shares the runtime/lifecycle flag surface.tests/hermes_cli/test_startup_plugin_gating.py—_BUILTIN_SUBCOMMANDS↔ argparse parity holds withserveadded (37 pass).tests/hermes_cli/test_update_stale_dashboard.py,test_dashboard_lifecycle_flags.py,test_dashboard_unified_launch.py,test_default_interface_resolution.py,test_subcommands_batch.py— all green.apps/desktop/electron/backend-command.test.cjs(new) — pure serve/dashboard argv routing +servevs"server"detection (no false positives).apps/desktop/electron/windows-child-process.test.cjs— updated forbackendArgs.bootstrap-runnerfailure), CLI test subset.Risk / compatibility
dashboard --tuishim).dashboard --no-openvia capability detection — no break.