mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-29 11:42:04 +00:00
* feat(dashboard): nous-blue theme, bulk sessions, schedule picker
Batch of related dashboard improvements gathered on
austin/fix/dashboard-changes:
* Nous Blue theme — faithful port of the LENS_5I overlay system onto
the existing DashboardTheme. Lifts the foreground inversion layer to
z-index 200 to fix the long-standing hover / loading visual artifact,
adds an explicit swatchColors slot so the theme picker shows the
post-inversion preview, and migrates the legacy "lens-5i" theme key
from localStorage / API to "nous-blue" on first read.
* Theme-aware series colors: new --series-input-token /
--series-output-token CSS vars consumed by Analytics + Models
charts; ToolCall + ModelInfoCard switched to semantic
--color-success for diff lines and the Tools capability badge.
* Analytics + Models headers: consolidate period selector + refresh
next to the page title and drop the redundant period badge.
* Bulk session management — "Delete empty (N)" button + per-row
checkboxes with shift-click range select and a bulk-delete action
bar. Backed by SessionDB.delete_sessions() /
delete_empty_sessions() plus POST /api/sessions/bulk-delete and
DELETE /api/sessions/empty (registered before the templated
/api/sessions/{session_id} family so they don't get shadowed).
Hard cap of 500 IDs per bulk request. Full pytest coverage.
* Cron page — human-readable schedule picker (every-interval / daily
/ weekly / monthly / once / custom) replaces the raw cron
expression input; the job list now renders "Weekly on Mon, Wed,
Fri at 14:30" instead of "30 14 * * 1,3,5". English-only ordinals
for monthly schedules so non-English locales don't get incorrect
suffixes.
* example-dashboard plugin moved from plugins/ to tests/fixtures/ so
stock installs no longer ship the demo. Tests install it
dynamically via a pytest fixture that also reorders the FastAPI
routes.
* i18n: 40+ new keys for the bulk-select UI and schedule
picker/describer translated across all 16 locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(dashboard): dedupe memory provider picker
The memory provider <Select> lived on both /system and /plugins,
writing the same config.yaml field through two different endpoints
with no cross-page refresh. Remove the picker from /system in favor
of a read-only status row + link to /plugins, where it pairs with
the context-engine picker under "Plugin providers".
/system retains the destructive admin controls (file sizes, Reset
MEMORY.md / USER.md / all). The api.setMemoryProvider client and
PUT /api/memory/provider backend endpoint are left in place for
CLI / script callers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(dashboard): address Copilot review on PR #37383
- Backdrop layer-stack comment claimed LENS_5I-style themes override
--component-backdrop-bg-blend-mode to multiply, but our only
LENS_5I-style theme (nous-blue) keeps the default difference.
Reword to describe what the code actually does and present the
var as a forward-looking extension hook.
- /api/sessions/bulk-delete docstring promised the response would
echo back the list of deleted IDs, but the implementation only
returns {ok, deleted}. Tighten the docstring to match the wire
format; the client already knows what it asked to delete, so the
IDs aren't needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dashboard): address copilot review on cron describe + bulk-select checkbox
- schedule.ts: restrict `describeCronExpression` to strictly 5-field cron
expressions. The backend `parse_schedule` also accepts the 6-field
`min hour dom month dow year` form, and humanising those by
destructuring only the first five fields would silently drop the year
(e.g. ``0 9 * * * 2099`` rendered as "Daily at 09:00"). 6+ field
expressions now fall through to the raw-string fallback so the user
sees what's actually scheduled.
- SessionsPage.tsx (SessionRow): wire the bulk-select Checkbox's
``onClick`` directly instead of attaching it to a parent ``<span>``
with a no-op ``onCheckedChange``. Radix forwards onClick to the
underlying ``<button role=checkbox>``, so the same handler now drives
both mouse clicks (preserving shift-key state for range select) and
keyboard activation (Space on the focused checkbox, which the browser
synthesises as a click on the <button>). Improves a11y / keyboard UX
without changing the controlled-selection model.
- SessionsPage.tsx: also extend ``SessionRowProps`` with the new
``onRename`` / ``onExport`` props introduced on main so the row's
destructured prop types resolve after the merge.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
import type { DashboardTheme, ThemeTypography, ThemeLayout } from "./types";
|
|
|
|
/**
|
|
* Built-in dashboard themes.
|
|
*
|
|
* Each theme defines its own palette, typography, and layout so switching
|
|
* themes produces visible changes beyond just color — fonts, density, and
|
|
* corner-radius all shift to match the theme's personality.
|
|
*
|
|
* Theme names must stay in sync with the backend's
|
|
* `_BUILTIN_DASHBOARD_THEMES` list in `hermes_cli/web_server.py`.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared typography / layout presets
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Default system stack — neutral, safe fallback for every platform. */
|
|
const SYSTEM_SANS =
|
|
'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
|
const SYSTEM_MONO =
|
|
'ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace';
|
|
|
|
const DEFAULT_TYPOGRAPHY: ThemeTypography = {
|
|
fontSans: SYSTEM_SANS,
|
|
fontMono: SYSTEM_MONO,
|
|
baseSize: "15px",
|
|
lineHeight: "1.55",
|
|
letterSpacing: "0",
|
|
};
|
|
|
|
const DEFAULT_LAYOUT: ThemeLayout = {
|
|
radius: "0.5rem",
|
|
density: "comfortable",
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Themes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const defaultTheme: DashboardTheme = {
|
|
name: "default",
|
|
label: "Hermes Teal",
|
|
description: "Classic dark teal — the canonical Hermes look",
|
|
palette: {
|
|
background: { hex: "#041c1c", alpha: 1 },
|
|
midground: { hex: "#ffe6cb", alpha: 1 },
|
|
foreground: { hex: "#ffffff", alpha: 0 },
|
|
warmGlow: "rgba(255, 189, 56, 0.35)",
|
|
noiseOpacity: 1,
|
|
},
|
|
typography: DEFAULT_TYPOGRAPHY,
|
|
layout: DEFAULT_LAYOUT,
|
|
terminalBackground: "#000000",
|
|
};
|
|
|
|
export const midnightTheme: DashboardTheme = {
|
|
name: "midnight",
|
|
label: "Midnight",
|
|
description: "Deep blue-violet with cool accents",
|
|
palette: {
|
|
background: { hex: "#0a0a1f", alpha: 1 },
|
|
midground: { hex: "#d4c8ff", alpha: 1 },
|
|
foreground: { hex: "#ffffff", alpha: 0 },
|
|
warmGlow: "rgba(167, 139, 250, 0.32)",
|
|
noiseOpacity: 0.8,
|
|
},
|
|
typography: {
|
|
...DEFAULT_TYPOGRAPHY,
|
|
fontSans: `"Inter", ${SYSTEM_SANS}`,
|
|
fontMono: `"JetBrains Mono", ${SYSTEM_MONO}`,
|
|
fontUrl:
|
|
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap",
|
|
letterSpacing: "-0.005em",
|
|
},
|
|
layout: {
|
|
...DEFAULT_LAYOUT,
|
|
radius: "0.75rem",
|
|
},
|
|
};
|
|
|
|
export const emberTheme: DashboardTheme = {
|
|
name: "ember",
|
|
label: "Ember",
|
|
description: "Warm crimson and bronze — forge vibes",
|
|
palette: {
|
|
background: { hex: "#1a0a06", alpha: 1 },
|
|
midground: { hex: "#ffd8b0", alpha: 1 },
|
|
foreground: { hex: "#ffffff", alpha: 0 },
|
|
warmGlow: "rgba(249, 115, 22, 0.38)",
|
|
noiseOpacity: 1,
|
|
},
|
|
typography: {
|
|
...DEFAULT_TYPOGRAPHY,
|
|
fontSans: `"Spectral", Georgia, "Times New Roman", serif`,
|
|
fontMono: `"IBM Plex Mono", ${SYSTEM_MONO}`,
|
|
fontUrl:
|
|
"https://fonts.googleapis.com/css2?family=Spectral:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;700&display=swap",
|
|
},
|
|
layout: {
|
|
...DEFAULT_LAYOUT,
|
|
radius: "0.25rem",
|
|
},
|
|
colorOverrides: {
|
|
destructive: "#c92d0f",
|
|
warning: "#f97316",
|
|
},
|
|
};
|
|
|
|
export const monoTheme: DashboardTheme = {
|
|
name: "mono",
|
|
label: "Mono",
|
|
description: "Clean grayscale — minimal and focused",
|
|
palette: {
|
|
background: { hex: "#0e0e0e", alpha: 1 },
|
|
midground: { hex: "#eaeaea", alpha: 1 },
|
|
foreground: { hex: "#ffffff", alpha: 0 },
|
|
warmGlow: "rgba(255, 255, 255, 0.1)",
|
|
noiseOpacity: 0.6,
|
|
},
|
|
typography: {
|
|
...DEFAULT_TYPOGRAPHY,
|
|
fontSans: `"IBM Plex Sans", ${SYSTEM_SANS}`,
|
|
fontMono: `"IBM Plex Mono", ${SYSTEM_MONO}`,
|
|
fontUrl:
|
|
"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap",
|
|
},
|
|
layout: {
|
|
...DEFAULT_LAYOUT,
|
|
radius: "0",
|
|
},
|
|
};
|
|
|
|
export const cyberpunkTheme: DashboardTheme = {
|
|
name: "cyberpunk",
|
|
label: "Cyberpunk",
|
|
description: "Neon green on black — matrix terminal",
|
|
palette: {
|
|
background: { hex: "#040608", alpha: 1 },
|
|
midground: { hex: "#9bffcf", alpha: 1 },
|
|
foreground: { hex: "#ffffff", alpha: 0 },
|
|
warmGlow: "rgba(0, 255, 136, 0.22)",
|
|
noiseOpacity: 1.2,
|
|
},
|
|
typography: {
|
|
...DEFAULT_TYPOGRAPHY,
|
|
fontSans: `"Share Tech Mono", "JetBrains Mono", ${SYSTEM_MONO}`,
|
|
fontMono: `"Share Tech Mono", "JetBrains Mono", ${SYSTEM_MONO}`,
|
|
fontUrl:
|
|
"https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=JetBrains+Mono:wght@400;700&display=swap",
|
|
},
|
|
layout: {
|
|
...DEFAULT_LAYOUT,
|
|
radius: "0",
|
|
},
|
|
colorOverrides: {
|
|
success: "#00ff88",
|
|
warning: "#ffd700",
|
|
destructive: "#ff0055",
|
|
},
|
|
};
|
|
|
|
export const roseTheme: DashboardTheme = {
|
|
name: "rose",
|
|
label: "Rosé",
|
|
description: "Soft pink and warm ivory — easy on the eyes",
|
|
palette: {
|
|
background: { hex: "#1a0f15", alpha: 1 },
|
|
midground: { hex: "#ffd4e1", alpha: 1 },
|
|
foreground: { hex: "#ffffff", alpha: 0 },
|
|
warmGlow: "rgba(249, 168, 212, 0.3)",
|
|
noiseOpacity: 0.9,
|
|
},
|
|
typography: {
|
|
...DEFAULT_TYPOGRAPHY,
|
|
fontSans: `"Fraunces", Georgia, serif`,
|
|
fontMono: `"DM Mono", ${SYSTEM_MONO}`,
|
|
fontUrl:
|
|
"https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=DM+Mono:wght@400;500&display=swap",
|
|
},
|
|
layout: {
|
|
...DEFAULT_LAYOUT,
|
|
radius: "1rem",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Nous Blue — the inverted "light mode" Hermes look, ported from the
|
|
* LENS_5I overlay preset in `@nous-research/ui`.
|
|
*
|
|
* Unlike the other built-ins (which paint dark color directly on the
|
|
* canvas), this theme relies on `<Backdrop />`'s foreground inversion
|
|
* layer: an opaque white sheet at z-200 with `mix-blend-mode: difference`
|
|
* that flips the entire stack below it. Authoring colors stay dark
|
|
* (`#170d02` brown background, `#FFAC02` orange midground), and the
|
|
* inversion converts them to their visual complements at paint time —
|
|
* the orange midground reads as #0053FD Nous-blue on screen, against a
|
|
* cream `#E8F2FD` canvas.
|
|
*
|
|
* Note on bg blend mode: the DS Lens uses `multiply` for LENS_5I because
|
|
* nousnet-web's <body> is white; hermes-agent's App root is `bg-black`,
|
|
* so we leave the bg layer's blend mode at the `difference` default —
|
|
* `difference(#170d02, #000)` passes the bg through unchanged, and the
|
|
* subsequent FG-difference layer then inverts it to cream. Using
|
|
* `multiply` here would collapse the bg to pure black against the
|
|
* `bg-black` root and produce a plain-white canvas instead of the
|
|
* intended cream-blue.
|
|
*
|
|
* Source of truth for the palette: `design-language/src/ui/components/
|
|
* overlays/lens.ts` (LENS_5I export).
|
|
*/
|
|
export const nousBlueTheme: DashboardTheme = {
|
|
name: "nous-blue",
|
|
label: "Nous Blue",
|
|
description: "Light mode — vivid Nous-blue accents on cream canvas",
|
|
palette: {
|
|
background: { hex: "#170d02", alpha: 1 },
|
|
midground: { hex: "#FFAC02", alpha: 1 },
|
|
foreground: { hex: "#FFFFFF", alpha: 1 },
|
|
// Same warm-amber as nousnet-web's overlay glow; after the FG
|
|
// inversion it reads as a cool ultraviolet vignette in the top-left.
|
|
warmGlow: "rgba(255, 172, 2, 0.18)",
|
|
// Noise sits above the FG inversion and is NOT flipped, so a softer
|
|
// multiplier keeps it from speckling over the bright post-inversion
|
|
// canvas.
|
|
noiseOpacity: 0.4,
|
|
},
|
|
typography: DEFAULT_TYPOGRAPHY,
|
|
layout: DEFAULT_LAYOUT,
|
|
// Inverted page: the embedded terminal is below the FG layer too, so
|
|
// a `#000000` source paints as visual white — i.e. a proper light-mode
|
|
// terminal pane. xterm picks lighter palette colors against the "black"
|
|
// canvas, which then read as dark text on screen post-inversion.
|
|
terminalBackground: "#000000",
|
|
componentStyles: {
|
|
backdrop: {
|
|
// Lower than LENS_5I.Lens.fillerOpacity (0.06). The filler texture
|
|
// gets amplified post-inversion: small variations against the deep
|
|
// `#170d02` source bg are barely visible, but those same variations
|
|
// against the bright `#E8F2FD` post-inversion canvas read as a
|
|
// heavy cloud/marble pattern — especially on near-empty pages
|
|
// (loading spinners, blank states). 0.02 keeps subtle grain
|
|
// without overwhelming the canvas.
|
|
fillerOpacity: "0.02",
|
|
},
|
|
},
|
|
// Pre-invert absolute-hex tokens so they read as their familiar colors
|
|
// through the FG difference layer. e.g. source #04D3C9 (cyan) is what
|
|
// gets painted, and `255 - channel` flips it to #FB2C36 (red) on screen.
|
|
// Without these, the default destructive/success/warning tokens would
|
|
// appear as their unintuitive complements.
|
|
colorOverrides: {
|
|
destructive: "#04d3c9",
|
|
destructiveForeground: "#000000",
|
|
success: "#b5217f",
|
|
warning: "#0042c7",
|
|
},
|
|
// Pre-inverted data-series accents for the Analytics/Models token
|
|
// charts. The defaults (#ffe6cb cream + #34d399 emerald) would render
|
|
// through the FG difference layer as dark navy + hot-coral on the
|
|
// bright Nous-blue canvas — the coral is the "red" users see for
|
|
// Output values without these overrides. Source → on-screen:
|
|
// Input: #ffe6cb → #001934 (dark navy) ← unchanged
|
|
// Output: #ffac02 → #0053fd (vivid Nous-blue) ← brand accent
|
|
// Input keeps the cream source so it stays a neutral, low-contrast
|
|
// dark-blue against the cream canvas; output paints as the brand
|
|
// Nous-blue so the "primary" series in token-flow charts reads as
|
|
// the highlight color, matching the rest of the inverted UI chrome.
|
|
seriesColors: {
|
|
inputTokenAccent: "#ffe6cb",
|
|
outputTokenAccent: "#ffac02",
|
|
},
|
|
// Explicit picker swatch — the raw palette hex (`#170d02`, `#FFAC02`,
|
|
// amber rgba) doesn't reflect what users see after the FG inversion,
|
|
// so we paint the post-inversion visual triplet directly:
|
|
// white → vivid Nous-blue → cream/light-blue
|
|
// matching the actual on-screen rendering of the theme.
|
|
swatchColors: ["#FFFFFF", "#0053FD", "#E8F2FD"],
|
|
};
|
|
|
|
/**
|
|
* Same look as ``defaultTheme`` but with a larger root font size, looser
|
|
* line-height, and ``spacious`` density so every rem-based size in the
|
|
* dashboard scales up. For users who find the default 15px UI too dense.
|
|
*/
|
|
export const defaultLargeTheme: DashboardTheme = {
|
|
name: "default-large",
|
|
label: "Hermes Teal (Large)",
|
|
description: "Hermes Teal with bigger fonts and roomier spacing",
|
|
palette: defaultTheme.palette,
|
|
typography: {
|
|
...DEFAULT_TYPOGRAPHY,
|
|
baseSize: "18px",
|
|
lineHeight: "1.65",
|
|
},
|
|
layout: {
|
|
...DEFAULT_LAYOUT,
|
|
density: "spacious",
|
|
},
|
|
};
|
|
|
|
export const BUILTIN_THEMES: Record<string, DashboardTheme> = {
|
|
default: defaultTheme,
|
|
"default-large": defaultLargeTheme,
|
|
"nous-blue": nousBlueTheme,
|
|
midnight: midnightTheme,
|
|
ember: emberTheme,
|
|
mono: monoTheme,
|
|
cyberpunk: cyberpunkTheme,
|
|
rose: roseTheme,
|
|
};
|