Files
hermes-agent/web
teknium1 0ddeaf9334 test: pin REST profile scoping with real multi-profile activation
The destructive-route file covered 3 of the 12 gated routes and faked the
gate itself (`monkeypatch` on `is_multiplex_active`), which left the boot
wiring the whole 400 branch rests on untested.

* All 12 destructive/privileged routes are parametrized through the refusal
  case, and the refusal now asserts the ABSENCE of the effect (no spawn, no
  session store opened, no file touched) instead of the detail wording.
* Multi-profile hosting is armed for real: `activate_multi_profile_hosting_eagerly()`
  (the boot probe) for the refusal cases, plus a test that a cross-profile
  request is itself the lazy activation. An autouse fixture enters each test
  single-profile and restores the process-global flag and frozen launch-env
  snapshot, so the one-way flip cannot leak into the single-profile-host test.
* New coverage for the scoping that had none: PUT /api/dashboard/plugin-providers,
  POST /api/local-models/quickstart, memory-provider readiness judged inside the
  write scope, and one invariant each for status/actions/config_env/memory_providers.
* web: ?profile= on /api/credentials, the serving-profile fallback for
  getManagementProfile/fetchJSON, authedFetch going through withManagementProfile,
  and initialProfileScope's URL > bootstrap > serving precedence.

Every new assertion was proven red against the PR's own previous head
(ac3d794) or, for the scoping introduced there, against the pre-PR base.
2026-09-21 09:59:11 -07:00
..
…
…

Hermes Agent — Web UI

Browser-based dashboard for managing Hermes Agent configuration, API keys, and monitoring active sessions.

Stack

  • Vite + React 19 + TypeScript
  • Tailwind CSS v4 with custom dark theme
  • shadcn/ui-style components (hand-rolled, no CLI dependency)

Development

# Start the backend API server
cd ../
python -m hermes_cli.main web --no-open

# In another terminal, start the Vite dev server (with HMR + API proxy)
cd web/
npm install
npm run dev

Open the Vite URL printed in the terminal (usually http://localhost:5173). That is the live-reload UI.

hermes dashboard on port 9119 serves the built bundle from hermes_cli/web_dist/, not the Vite dev server — changes in web/src/ will not appear there until you run npm run build and restart the dashboard (or use web --no-open + Vite as above).

The Vite dev server proxies /api requests to http://127.0.0.1:9119 (the FastAPI backend).

Build

npm run build

This outputs to ../hermes_cli/web_dist/, which the FastAPI server serves as a static SPA. The built assets are included in the Python package via pyproject.toml package-data.

Structure

src/
├── components/ui/   # Reusable UI primitives (Card, Badge, Button, Input, etc.)
├── lib/
│   ├── api.ts       # API client — typed fetch wrappers for all backend endpoints
│   └── utils.ts     # cn() helper for Tailwind class merging
├── pages/
│   ├── StatusPage   # Agent status, active/recent sessions
│   ├── ConfigPage   # Dynamic config editor (reads schema from backend)
│   └── EnvPage      # API key management with save/clear
├── App.tsx          # Main layout and navigation
├── main.tsx         # React entry point
└── index.css        # Tailwind imports and theme variables

Typography & contrast rules

Read before adding or editing UI styles. These rules keep the dashboard legible across all built-in themes and stop drift back into the patterns the design system was just refactored out of.

Text size floor

  • Minimum body size: text-xs (12px / 0.75rem). Do not use arbitrary text-[0.6rem], text-[0.65rem], text-[9px], text-[10px], or text-[11px] on copy, hints, labels, counts, or badges. Use the standard scale: text-xs, text-sm, text-base.
  • Smaller sizes are only acceptable on decorative overlays (chart stripes, empty-state icons) — never on text the user is meant to read.

Opacity floor on text

  • Never apply opacity below 0.7 to text. No opacity-30, opacity-50, opacity-60 on <span>s, <p>s, labels, etc.
  • Do not stack opacity tokens. Patterns like text-muted-foreground/60, text-midground/70, text-foreground/50 create unpredictable WCAG failures because the parent token already has alpha.
  • Use the semantic text tokens from @nous-research/ui's globals.css:
    • text-text-primary — default body text.
    • text-text-secondary — subtitles, meta, inactive nav.
    • text-text-tertiary — small chrome labels, counts, footnotes.
    • text-text-disabled — disabled states.
    • text-text-on-accent — text on filled accent surfaces.

Brand uppercase via text-display, not raw uppercase

  • The dashboard preserves the Nous brand uppercase aesthetic, but it is opt-in per element, not global.
  • Apply uppercase via the DS utility text-display on brand chrome only — page titles, nav section headings, badges, brand wordmark. DS components (Button, Badge, Tabs, Segmented, etc.) already self-apply text-display.
  • Do not introduce new uppercase (the literal Tailwind class) in hermes-agent/web/src. Prefer text-display for new brand chrome. Legacy uppercase call sites (e.g. components/ui/label.tsx, card.tsx) remain until migrated.
  • The app shell no longer forces uppercase globally, so blanket normal-case opt-outs are unnecessary. Use normal-case only where a DS component applies text-display but the label should stay sentence case — e.g. dynamic user content (model slugs, theme names) or fixed UI copy that is not brand chrome (EnvPage “not configured” toggle, sidebar “New chat”).

Fonts

Typography is opt-in per surface, not global on layout shells — the app shell and page header keep their original theme/expanded fonts; Mondwest applies only where explicitly set.

Tier Classes Use for
Brand chrome font-mondwest text-display (or themedChrome) Sidebar nav, card section headers (CardTitle), Segmented filter buttons, filter panel headings
Themed body font-mondwest normal-case (or themedBody) Card content (Card, CardDescription), session/platform rows, analytics tables — scoped to the component
Page chrome font-expanded Page header h1 (PageHeaderProvider) — sentence case, not text-display
Wordmark Typography + size/tracking only Sidebar/mobile “Hermes Agent” — mixed case, no Mondwest, no text-display
Technical font-mono-ui / font-mono / font-courier Model slugs, env keys, schedules, YAML, repo URLs
  • Do not put themedBody or themedFont on <main>, App, or other layout wrappers — it overrides component-scoped styles.
  • Card applies themedBody; CardTitle uses text-display (uppercase chrome); CardDescription uses themedBody.
  • NouiTypography defaults to font-sans unless a font prop is passed.
  • Do not use raw font-sans or font-display (theme sans variable) on new dashboard UI — prefer Mondwest tiers above where brand-appropriate.

Color tokens

  • Prefer semantic tokens (text-text-*, bg-card, border-border, text-foreground, text-destructive, text-success, text-warning) over raw layer references (text-midground, text-foreground).
  • text-muted-foreground is now wired to --color-text-secondary, so existing call sites stay correct, but new code should prefer the semantic name.
  • When you genuinely need a non-token color (icon de-emphasis on a chart, terminal foreground via inline style), keep alpha at ≥ 0.7 for any text.