hermes-agent/web/vite.config.ts
Brooklyn Nicholson dfb561a3ae refactor(desktop+dashboard): extract shared WebSocket/JSON-RPC layer
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".
2026-06-28 21:20:35 -05:00

102 lines
3.3 KiB
TypeScript

import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
const BACKEND = process.env.HERMES_DASHBOARD_URL ?? "http://127.0.0.1:9119";
/**
* In production the Python `hermes dashboard` server injects a one-shot
* session token into `index.html` (see `hermes_cli/web_server.py`). The
* Vite dev server serves its own `index.html`, so unless we forward that
* token, every protected `/api/*` call 401s.
*
* This plugin fetches the running dashboard's `index.html` on each dev page
* load, scrapes the `window.__HERMES_SESSION_TOKEN__` assignment, and
* re-injects it into the dev HTML. No-op in production builds.
*/
function hermesDevToken(): Plugin {
const TOKEN_RE = /window\.__HERMES_SESSION_TOKEN__\s*=\s*"([^"]+)"/;
const EMBEDDED_RE =
/window\.__HERMES_DASHBOARD_EMBEDDED_CHAT__\s*=\s*(true|false)/;
return {
name: "hermes:dev-session-token",
apply: "serve",
async transformIndexHtml() {
try {
const res = await fetch(BACKEND, { headers: { accept: "text/html" } });
const html = await res.text();
const match = html.match(TOKEN_RE);
if (!match) {
console.warn(
`[hermes] Could not find session token in ${BACKEND}` +
`is \`hermes dashboard\` running? /api calls will 401.`,
);
return;
}
const embeddedMatch = html.match(EMBEDDED_RE);
const embeddedJs = embeddedMatch ? embeddedMatch[1] : "true";
return [
{
tag: "script",
injectTo: "head",
children:
`window.__HERMES_SESSION_TOKEN__="${match[1]}";` +
`window.__HERMES_DASHBOARD_EMBEDDED_CHAT__=${embeddedJs};`,
},
];
} catch (err) {
console.warn(
`[hermes] Dashboard at ${BACKEND} unreachable — ` +
`start it with \`hermes dashboard\` or set HERMES_DASHBOARD_URL. ` +
`(${(err as Error).message})`,
);
}
},
};
}
export default defineConfig({
plugins: [react(), tailwindcss(), hermesDevToken()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@hermes/shared": path.resolve(__dirname, "../apps/shared/src"),
},
// When @nous-research/ui is symlinked via `file:../../design-language`,
// Node's module resolution would pick up shared deps from
// design-language/node_modules/*, giving us two copies + breaking
// hooks (useRef-of-null), webgl contexts, etc. Force everything that
// exists in BOTH places to use the dashboard's copy.
//
// Don't list packages here that only exist in the DS (nanostores,
// @nanostores/react) — Vite dedupe errors out when it can't find
// them at the project root.
dedupe: [
"react",
"react-dom",
"@react-three/fiber",
"@observablehq/plot",
"three",
"leva",
"gsap",
],
},
build: {
outDir: "../hermes_cli/web_dist",
emptyOutDir: true,
},
server: {
proxy: {
"/api": {
target: BACKEND,
ws: true,
},
// Same host as `hermes dashboard` must serve these; Vite has no
// dashboard-plugins/* files, so without this, plugin scripts 404
// or receive index.html in dev.
"/dashboard-plugins": BACKEND,
},
},
});