Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b5fb8da
refactor(web): dashboard typography & contrast pass
austinpickett May 19, 2026
88b7e1b
fix(web): refresh package-lock for @nous-research/ui 0.16.0
austinpickett May 19, 2026
9d0dc33
fix(nix): refresh npm lockfile hashes
github-actions[bot] May 19, 2026
64faf1b
ci: re-run checks after nix lockfile hash fix
austinpickett May 19, 2026
3b2bdec
docs(web): soften README typography rules per review
austinpickett May 19, 2026
b69b3b2
fix(web): address Copilot review on footer and sheet title
austinpickett May 19, 2026
1b5fc1f
merge(main): sync branch before fonts.css import
austinpickett May 19, 2026
a44c925
fix(web): import DS fonts.css before globals.css
austinpickett May 19, 2026
9a85a87
fix(web): apply text-display to page headers and brand nav
austinpickett May 19, 2026
104d1d5
fix(web): restore Mondwest brand wordmark tracking
austinpickett May 19, 2026
08b9be9
fix(web): add uppercase fallback on nav and page titles
austinpickett May 19, 2026
330f2f9
fix(web): restore Mondwest brand wordmark without Typography
austinpickett May 19, 2026
e4cda79
fix(web): drop Mondwest from Hermes Agent wordmark
austinpickett May 19, 2026
168affd
fix(web): keep Hermes Agent wordmark mixed case (normal-case)
austinpickett May 19, 2026
b03166f
fix(web): address Copilot review on ToolCall labels and copy button
austinpickett May 22, 2026
d1a1fa2
merge(main): resolve package conflicts keeping @nous-research/ui 0.16.0
austinpickett May 22, 2026
967c15a
fix(nix): refresh web npmDepsHash after merge lockfile update
austinpickett May 22, 2026
967bcb1
refactor(web): scope Mondwest body typography to dashboard content
austinpickett May 22, 2026
fb677fb
feat(dashboard): polish sessions, env, keys, and page chrome UX
austinpickett May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4319,12 +4319,13 @@ async def post_agent_plugin_install(request: Request, body: _AgentPluginInstallB

def _validate_plugin_name(name: str) -> str:
"""Reject path-traversal attempts in plugin name URL parameters."""
if not name or "/" in name or "\\" in name or ".." in name:
name = name.strip("/")
if not name or ".." in name or "\\" in name:
raise HTTPException(status_code=400, detail="Invalid plugin name.")
return name


@app.post("/api/dashboard/agent-plugins/{name}/enable")
@app.post("/api/dashboard/agent-plugins/{name:path}/enable")
async def post_agent_plugin_enable(request: Request, name: str):
_require_token(request)
name = _validate_plugin_name(name)
Expand All @@ -4336,7 +4337,7 @@ async def post_agent_plugin_enable(request: Request, name: str):
return result


@app.post("/api/dashboard/agent-plugins/{name}/disable")
@app.post("/api/dashboard/agent-plugins/{name:path}/disable")
async def post_agent_plugin_disable(request: Request, name: str):
_require_token(request)
name = _validate_plugin_name(name)
Expand All @@ -4348,7 +4349,7 @@ async def post_agent_plugin_disable(request: Request, name: str):
return result


@app.post("/api/dashboard/agent-plugins/{name}/update")
@app.post("/api/dashboard/agent-plugins/{name:path}/update")
async def post_agent_plugin_update(request: Request, name: str):
_require_token(request)
name = _validate_plugin_name(name)
Expand All @@ -4361,7 +4362,7 @@ async def post_agent_plugin_update(request: Request, name: str):
return result


@app.delete("/api/dashboard/agent-plugins/{name}")
@app.delete("/api/dashboard/agent-plugins/{name:path}")
async def delete_agent_plugin(request: Request, name: str):
_require_token(request)
name = _validate_plugin_name(name)
Expand Down Expand Up @@ -4399,7 +4400,7 @@ class _PluginVisibilityBody(BaseModel):
hidden: bool


@app.post("/api/dashboard/plugins/{name}/visibility")
@app.post("/api/dashboard/plugins/{name:path}/visibility")
async def post_plugin_visibility(request: Request, name: str, body: _PluginVisibilityBody):
"""Toggle a plugin's sidebar visibility (persists to config.yaml dashboard.hidden_plugins)."""
_require_token(request)
Expand Down
2 changes: 1 addition & 1 deletion nix/web.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ let
src = ../web;
npmDeps = pkgs.fetchNpmDeps {
inherit src;
hash = "sha256-xSsyluzU2lNhwGqB6XMCGMv3QFHZizE6hgUyc1jvyOw=";
hash = "sha256-6qhGuifHVtCeep1SiQdCUxBMr7UGhYpdMTvXhrQu/zA=";
};

npm = hermesNpmLib.mkNpmPassthru { folder = "web"; attr = "web"; pname = "hermes-web"; };
Expand Down
56 changes: 56 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ 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
Expand All @@ -46,3 +51,54 @@ src/
β”œβ”€β”€ 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.

67 changes: 45 additions & 22 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading