Commit graph

131 commits

Author SHA1 Message Date
liuhao1024
28bb7e0a8e
fix(web): bridge Tailwind --font-sans to --theme-font-sans (#20406)
Tailwind v4 defines its own --font-sans and --font-mono tokens
independently of the Hermes theme variables. Components using
font-sans/font-mono utility classes bypass --theme-font-sans and
--theme-font-mono, so theme font changes have no effect.

Add --font-sans and --font-mono bridges in the @theme inline block
so Tailwind's font tokens follow the active Hermes theme.

Fixes #20380
2026-05-29 00:19:06 -04:00
Austin Pickett
a5c1f925b5 fix(web): stop /api/auth/me 401 from triggering a reload loop
In loopback mode the dashboard's identity probe (/api/auth/me) returns
401 by design — AuthWidget swallows it and renders nothing. But the
probe routed through fetchJSON, whose loopback 401 handler treats a 401
as a rotated session token and full-page-reloads to pick up a fresh one.
That reload is guarded by a one-shot sessionStorage flag which every
*successful* request clears, so with auth/me reliably 401ing and the
other dashboard calls (status/config/sessions) reliably succeeding, the
guard never sticks and the page reload-loops indefinitely (the "boot
flash").

Add an allowUnauthorized option to fetchJSON that skips only the loopback
stale-token reload (the 401 still throws so AuthWidget can catch it, and
the gated-mode login_url envelope redirect is unaffected), and use it for
getAuthMe.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 16:58:42 -04:00
Austin Pickett
a3cd974ee7 chore(web): bump @nous-research/ui to 0.18.2
Picks up the deferred GPU-tier detection fix (design-language) that
stops the synchronous WebGL probe from blocking first paint, which was
causing a boot-time flash in the dashboard backdrop.

nix/web.nix npmDepsHash is a placeholder here and is corrected in the
follow-up commit using the hash reported by the Nix CI job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 16:20:14 -04:00
Austin Pickett
c661fefa08 Merge remote-tracking branch 'origin/main' into refactor/use-ds-primitives
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	web/src/components/BottomPickSheet.tsx
#	web/src/components/SidebarFooter.tsx
#	web/src/components/ui/card.tsx
#	web/src/components/ui/confirm-dialog.tsx
#	web/src/pages/ChatPage.tsx
2026-05-28 14:20:49 -04:00
Teknium
fe5c8ec4ad
fix(dashboard): auto-reload SPA on stale-token 401 in loopback mode (#33861)
The dashboard's loopback auth uses an ephemeral '_SESSION_TOKEN' that
rotates on every server restart (hermes update, hermes gateway restart,
etc.). A tab kept open across the restart holds the OLD token in
window.__HERMES_SESSION_TOKEN__ from the previous HTML render, so every
'/api/*' fetch returns '401 Unauthorized' — surfacing in the UI as
'Failed to load Kanban board: 401: Unauthorized', 'Analytics 401', etc.
(#24186, #25275).

Before this patch the workaround was to manually clear site data or
hard-reload — annoying enough that users reported it as a regression
even though the token rotation is by design (security property:
stolen tokens can't survive a server restart).

The HTML response already sets 'Cache-Control: no-store, no-cache,
must-revalidate', so a reload reliably picks up the freshly-injected
token. fetchJSON now triggers that reload automatically on the first
loopback-mode 401, guarded by a sessionStorage flag so a genuine
auth bug (where even the new token fails) falls through to throw
on the second attempt instead of reload-looping. The flag is
cleared on any 2xx so a subsequent server restart in the same tab
gets its own reload cycle.

Gated mode is unaffected — that path already redirects to login_url
via the structured 401 envelope (Phase 6), and the new code is
explicitly skipped when window.__HERMES_AUTH_REQUIRED__ is set.

Refs #24186, #25275
2026-05-28 10:53:23 -07:00
Wesley Simplicio
10f13c3881
fix(web): allow mobile dashboard scrolling (#28051) (#28577)
* fix(web): allow mobile dashboard scrolling

* fix(web): combine mobile root scroll rules

---------

Co-authored-by: Wesley Simplicio <wesley.simplicio.ext@siemens-energy.com>
2026-05-28 00:02:50 -04:00
Austin Pickett
c9410b3462
feat(web): add collapsible sidebar for the dashboard (#33421)
* feat(web): add collapsible sidebar for the dashboard

The desktop sidebar can now be collapsed to an icon-only rail via a
toggle button in the sidebar header.  State is persisted in
localStorage so it survives page reloads.

When collapsed (lg+ only):
- Sidebar shrinks from w-64 to w-14 with a smooth width transition
- Nav items show only their icon with a native title tooltip
- Brand text, plugin headings, system actions, theme/language
  switchers, auth widget, and footer are hidden
- Mobile drawer behavior is unchanged (always full-width)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): align sidebar tooltips to sidebar edge consistently

Tooltip left position now uses the sidebar's right edge instead of the
anchor element's right edge, so narrow anchors (theme/language switchers)
align with full-width anchors (nav links, system actions).

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): add tooltip animations, restore theme label, rename Sessions tab

- Sidebar tooltips now animate in with a subtle 120ms ease-out slide;
  subsequent tooltips within the same hover sequence appear instantly
  (no delay/animation) following Emil Kowalski's tooltip pattern
- Restore theme name label when sidebar is expanded
- Rename Sessions segment tab to "History" across all 16 locales

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): smooth sidebar collapse animation

- Remove icon centering on collapse; icons stay left-aligned at px-5
  so they don't jump during the width transition
- Text labels fade out with opacity transition instead of instant
  display:none, clipped naturally by overflow-hidden
- Slow collapse duration from 450ms to 600ms for a more relaxed feel
- Gateway dot always rendered with opacity toggle so it doesn't
  slide in from the right on collapse
- Pin gateway dot at fixed left offset (pl-[1.625rem]) to align
  with nav icons
- Align header toggle button with justify-center when collapsed
- Bottom switchers use items-start when collapsed to prevent reflow

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 23:58:41 -04:00
Ben
af3d4a687f fix(dashboard-auth): ChatPage cleanup closes WS via wsRef.current
Phase 5.3 (1c99c2f5e) wrapped the WS construction in an IIFE so the
gated-mode ticket fetch could resolve asynchronously, but the effect's
top-level cleanup still referenced the IIFE-scoped `const ws`. TypeScript
catches it at build time:

  src/pages/ChatPage.tsx:654:7 - error TS2304: Cannot find name 'ws'.

LSP-cache-lag drowned the diagnostic under the JSX-types-missing noise
locally, so the bug shipped uncaught. Switch to `wsRef.current?.close()`
which:

  - resolves to the same WebSocket the IIFE assigned (line 562:
    `wsRef.current = ws`)
  - is null-safe when unmount races the ticket fetch (the IIFE early-
    returns on `unmounting` so wsRef.current is never set)

The ChatSidebar.tsx + gatewayClient.ts cleanup paths were already using
this pattern correctly (`ws?.close()` / `ws` was hoisted), so this fix
is ChatPage-only.
2026-05-27 02:12:27 -07:00
Ben
2fc4615fc4 feat(dashboard-auth): Phase 7 — SPA AuthWidget + /api/status auth fields
Phase 7 surfaces the OAuth gate state to users.

web/src/components/AuthWidget.tsx (new):
  Sidebar widget that fetches /api/auth/me on mount and renders a
  compact 'Logged in as <user_id…> via <provider>' row with a logout
  icon. Contract V1 (Nous Portal) emits no email/display_name claims,
  so user_id is the display value (truncated to 14 chars + ellipsis);
  display_name and email fallthroughs are forward-compat for OQ-C1.
  Renders nothing on 401 from /api/auth/me — that's the signal the
  gate isn't engaged (loopback mode), in which case the widget would
  be confusing.
  Logout POSTs /auth/logout (which clears cookies + redirects to
  /login) then full-page-navigates to /login itself; the SPA's fetch
  wrapper doesn't follow that redirect, so the navigation is explicit.

web/src/App.tsx: mounts <AuthWidget /> above <SidebarFooter />.
  Component is self-hiding in loopback mode so there's no need for a
  conditional mount.

web/src/lib/api.ts:
  - getAuthMe() + logout() helpers
  - AuthMeResponse type
  - StatusResponse gets optional auth_required + auth_providers fields
    so the existing StatusPage can render a gated/loopback badge.

hermes_cli/web_server.py: /api/status payload now includes
  - auth_required: bool — whether app.state.auth_required is True
  - auth_providers: list[str] — registered DashboardAuthProvider names
  Lazy-imports list_providers so early-startup status calls don't
  crash if the dashboard_auth module is still being set up.

tests/hermes_cli/test_dashboard_auth_status_endpoint.py: 3 new tests
covering the new status fields in both gated and loopback modes plus
a regression that no existing field got dropped from the payload.

The hermes status CLI is unchanged in this commit — that command
tracks model providers + OAuth credentials, not running-dashboard
state. The /api/status endpoint is the canonical place to query
dashboard auth-gate state, consumed by the React StatusPage already.
2026-05-27 02:12:27 -07:00
Ben
5e9308b5b8 feat(dashboard-auth): Phase 6 — 401 re-auth envelope + next= propagation
Contract V1 of nous-account-service PR #180 ships no refresh tokens, so
the original Phase 6 silent-refresh design is replaced with a thinner
'401 → redirect to /login' UX. The dashboard's gated middleware now
emits a structured envelope on any auth failure; the SPA's fetch
wrapper sees it and full-page-navigates the user through re-auth.

hermes_cli/dashboard_auth/cookies.py:
  set_session_cookies(refresh_token='') SKIPS writing the
  hermes_session_rt cookie. Forward-compat: a non-empty refresh_token
  still emits the cookie unchanged, so a future Portal contract that
  starts issuing RTs flips the persistence on with no other change.
  clear_session_cookies still emits a Max-Age=0 deletion for the RT
  cookie so stale cookies from earlier deployments get flushed on
  logout / session expiry. Deprecation marker + rationale in
  module docstring per the user's docstring-only deprecation pattern.

hermes_cli/dashboard_auth/middleware.py:
  _unauth_response now builds a structured JSON envelope for API 401s:
    { error: 'session_expired' | 'unauthenticated',
      detail: 'Unauthorized',
      reason: <internal>,
      login_url: '/login?next=<safe-path>' }
  HTML redirects also carry next= so a user landing on /sessions
  without a cookie bounces back to /sessions after re-auth.
  _safe_next_target validates same-origin: drops protocol-relative
  paths (//evil.com), absolute URLs, and any /login or /auth/* loop.
  Dead cookies are cleared on the 401 path so the browser stops
  replaying invalid tokens.

hermes_cli/dashboard_auth/routes.py:
  /auth/callback accepts next= query param and validates via
  _validate_post_login_target (same rules as the gate's
  _safe_next_target — defence-in-depth because next= survived a full
  IDP round trip and attacker-controlled state can re-enter via the
  callback URL). Open-redirect attempts land at '/' instead.

web/src/lib/api.ts:
  fetchJSON parses the 401 envelope and full-page-navigates to
  body.login_url ONLY on the known session-expiry error codes.
  Domain-level 401s (e.g. permission errors) bubble up as regular
  errors. credentials: 'include' added so cookie auth works for all
  fetches routed through this wrapper. sessionStorage.lastLocation is
  preserved for future use by AuthWidget / hermes_status.

Test files marked with pytest.mark.xdist_group so the four files that
mutate web_server.app.state.auth_required serialize onto the same xdist
worker — eliminates 'works locally, fails in CI' app-state bleed.

20 new tests in test_dashboard_auth_401_reauth.py:
  - set_session_cookies(refresh_token='') skips RT cookie
  - clear_session_cookies still emits RT deletion
  - 401 envelope shape (unauthenticated vs session_expired)
  - dead cookie cleared on invalid-token 401
  - login_url carries next= for deep paths
  - login loop avoided when path is /login/auth/api-auth
  - protocol-relative URL rejected
  - _safe_next_target unit tests (accept same-origin, reject loops/abs)
  - /auth/callback respects safe next= but rejects open redirects

2 pre-existing tests updated to accept the new /login?next=%2F shape.

Full dashboard-auth suite: 168 passed, 1 skipped (Phase 0 pre-existing).
2026-05-27 02:12:27 -07:00
Ben
8971e94831 feat(dashboard-auth): SPA WS auth — getWsTicket() + buildWsAuthParam()
Phase 5 task 5.3. The dashboard's three WS-using surfaces (ChatPage,
gatewayClient, ChatSidebar) previously hardcoded ?token=<session>. In
gated mode the server rejects that path; the SPA must mint a single-use
ticket via POST /api/auth/ws-ticket and pass ?ticket= on the upgrade.

web/src/lib/api.ts: adds getWsTicket() (POST /api/auth/ws-ticket with
credentials: 'include') and buildWsAuthParam() — a helper that returns
['ticket', <minted>] in gated mode and ['token', <session>] in loopback.
Window.__HERMES_AUTH_REQUIRED__ is read from the server-injected
bootstrap script and toggles the path. Documented as the bridge from
cookie auth (REST) to WS auth.

web/src/pages/ChatPage.tsx: buildWsUrl() now takes an [authName, authValue]
pair instead of a bare token. The WS construct is wrapped in an IIFE so
the outer effect can stay synchronous (the cleanup returns the effect's
disposer at top level). onDataDisposable + onResizeDisposable hoisted to
`let` bindings the cleanup closes over.

web/src/lib/gatewayClient.ts: connect() branches on
window.__HERMES_AUTH_REQUIRED__ before opening /api/ws. Explicit token
overrides win (test-only path); otherwise gated → fetch ticket, loopback
→ use injected session token.

web/src/components/ChatSidebar.tsx: events-feed WS opens through the
same IIFE pattern as ChatPage. The ws local is hoisted so the cleanup's
ws?.close() works after the async mint resolves.

Server side already injects window.__HERMES_AUTH_REQUIRED__ in
_serve_index (Phase 3.5).
2026-05-27 02:12:27 -07:00
TheOnlyMika
5744b17579 harden: restrict markdown link schemes; parse untrusted XML with defusedxml
Two small defensive-hardening changes:

- web/src/components/Markdown.tsx: render links only for http(s)/mailto
  schemes; other schemes (javascript:, data:, vbscript:) are dropped to
  plain text so a crafted link in rendered content can't execute on click.

- gateway/platforms/wecom_callback.py: parse the untrusted, pre-auth WeCom
  callback request body with defusedxml instead of xml.etree, blocking
  entity-expansion / billion-laughs (and XXE) on the parse path. defusedxml
  is already a dependency (uv.lock); response-building XML in
  wecom_crypto.py is unchanged (it is not parsed from untrusted input).

Verified: dashboard typechecks and builds; defusedxml blocks an
entity-expansion payload while valid WeCom envelopes still parse.
2026-05-25 23:30:43 -07:00
Austin Pickett
487c398dcf refactor(web): dashboard typography & contrast pass
Removes the global `uppercase` + `font-mondwest` from the App.tsx root
that forced every page to opt-out, replaces stacked-alpha text colors
with semantic tokens for WCAG-AA contrast across all 7 themes, and
applies the new `text-display` utility from @nous-research/ui@0.16.0
on intentional brand chrome (page titles, sidebar headings, segmented
filters) only. Bumps every sub-12px arbitrary text size to text-xs.

Also widens the dashboard plugin routes (/api/dashboard/agent-plugins/
{name:path}/...) so category-namespaced plugins like observability/
langfuse and image_gen/openai can be enable/disabled from the dashboard
— previously the FE encodeURIComponent-ed the slash and the backend
{name} route rejected it. _validate_plugin_name still blocks .. and
backslash, and strips leading/trailing slash.

Touches sessions/env/keys page chrome and adds two new i18n keys
(`overview`, `showMore`/`showLess`) across all 18 locales.

Squashes 19 commits from PR #28832.

Co-authored-by: Hermes <noreply@nousresearch.com>
2026-05-22 19:50:32 -07:00
Austin Pickett
c9e5a9bb08 refactor(web): consume DS primitives, remove local component copies
Replace locally-forked UI components and hooks with their newly
promoted counterparts from @nous-research/ui:

Deleted local components (now in DS):
- components/ui/input.tsx, label.tsx, separator.tsx, card.tsx,
  confirm-dialog.tsx
- components/Toast.tsx, BottomPickSheet.tsx, NouiTypography.tsx
- hooks/useToast.ts, useModalBehavior.ts, useBelowBreakpoint.ts,
  useConfirmDelete.ts

Import updates across 25 files to use DS deep imports:
- @nous-research/ui/ui/components/{input,label,separator,card,
  confirm-dialog,toast,bottom-sheet}
- @nous-research/ui/ui/components/typography (replaces NouiTypography)
- @nous-research/ui/hooks/{use-toast,use-modal-behavior,
  use-below-breakpoint,use-confirm-delete}

Requires design-language >= feat/promote-hermes-web-primitives.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 21:57:59 -04:00
Teknium
d246f9a278 fix(aux-picker): drop stale session_search slot
PR #27590 removed auxiliary.session_search from DEFAULT_CONFIG (single-shape
tool now returns DB content directly without an aux LLM), but the slot
remained in _AUX_TASK_SLOTS (web_server.py) and AUX_TASKS (ModelsPage.tsx).
Removing the dead entries while we're touching these tables.
2026-05-22 04:10:38 -07:00
flooryyyy
c1e93aa331 fix: add missing aux model slots to model picker
triage_specifier, kanban_decomposer, profile_describer exist in
DEFAULT_CONFIG auxiliary section but weren't in _AUX_TASK_SLOTS,
_AUX_TASKS, or the dashboard AUX_TASKS array — so users couldn't
configure them through hermes model or the web dashboard.

9â\x86\x9212 aux slots across all three UI surfaces.
2026-05-22 04:10:38 -07:00
Teknium
56b79f12ac
fix(dashboard): remove country flags from language picker (#29997)
Closes #29750. Reporter flagged that 繁體中文 displayed the TW flag
instead of the PRC flag. Rather than picking a side, drop the
language-flag pairings entirely — languages aren't countries
(English ≠ GB, Portuguese ≠ PT, Mandarin variants ≠ any single
jurisdiction), and endonyms are unambiguous.

- LOCALE_META: strip flagCountryCode field
- LanguageSwitcher: remove LocaleFlagIcon component + both call sites
- main.tsx: drop flag-icons CSS import
- package.json: uninstall flag-icons
2026-05-21 13:10:52 -07:00
Austin Pickett
edb2d91057
feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish (#28814)
* feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish

Replaces the hand-rolled shadcn-style `Checkbox` in `web/src/components/ui/`
with the Nous DS `Checkbox` (Radix-backed) from `@nous-research/ui`, bumps
the DS to 0.14.2, and picks up two regressions surfaced by the bump.

Checkbox migration
- bump `@nous-research/ui` 0.14.0 → ^0.14.2 and remove
  `web/src/components/ui/checkbox.tsx`
- migrate `ProfilesPage` and `ModelPickerDialog` to the DS Checkbox API
  (`onCheckedChange`, paired `<Label htmlFor>`)
- expose `Checkbox` on the dashboard plugin SDK
  (`web/src/plugins/registry.ts`) so plugin bundles can use the same
  DS component
- migrate the kanban dashboard plugin's 7 native `<input type="checkbox">`
  call sites to the SDK `Checkbox`, with a native-input fallback shim so
  the bundle still renders against older hosts that predate the SDK export

Fix: missing font registrations after the 0.14.x split
- import `@nous-research/ui/styles/fonts.css` before `globals.css` in
  `web/src/index.css`. As of 0.14.x, `globals.css` only declares the
  `--font-*` variables (Collapse, Mondwest, Rules Compressed/Expanded);
  the `@font-face` registrations now live in a separate `fonts.css`, so
  without this import the DS components silently fall back to a system
  font stack and look unstyled.

Fix: right-align page header toolbars on sm+ viewports
- The mobile dashboard polish in #28127 flipped four pages'
  `setEnd(...)` wrappers from `justify-end` to `w-full ... justify-start`
  so toolbars stack below the title and align left on small screens.
  But the outer `end` slot in `PageHeaderProvider` already has
  `sm:justify-end`, and that has no effect when its only child is
  `w-full` — once a flex child fills the row, the parent's `justify-*`
  can't move it. The toolbar pinned to the *left* of the right-side
  `sm:max-w-md` (~448px) slot, making the buttons appear to float a
  couple-hundred pixels off the right edge on Analytics, Models, Logs,
  and Plugins.
- Re-add `sm:justify-end` on the inner wrapper of each affected page,
  preserving the mobile stacked layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): update web npmDeps hash for package-lock bump

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): refresh npm lockfile hashes

* chore(ci): re-trigger checks after nix lockfile hash fix

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-20 08:00:17 -04:00
Austin Pickett
ff0a70381e
fix(web): consume bundled design system assets (#26391)
* fix: update design system package, replace bg image, remove sync assets

* fix(web): update bundled asset metadata

* fix(web): normalize npm lockfile metadata

* fix(nix): refresh npm lockfile hashes

* chore(ci): trigger PR checks

* fix(web): declare motion peer dependency

* fix(nix): refresh npm lockfile hashes

* chore(ci): trigger PR checks after dependency update

* fix(web): restore cross-platform lockfile entries

* fix(nix): refresh npm lockfile hashes

* chore(ci): trigger PR checks after lockfile restore

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-19 07:47:55 -04:00
Gille
709e37e19e
fix(dashboard): add scheduled kanban i18n strings (#28534)
Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 06:32:35 -04:00
YuanHanzhong
afffb8d9a5 fix(dashboard): use browser scrollback for chat wheel 2026-05-19 00:07:33 -07:00
Siddharth Balyan
1335ce996d
fix(web): add scheduled column to i18n type definitions (#28549)
columnLabels and columnHelp in en.ts include a scheduled entry but the
Translations interface in types.ts did not declare it, causing a
TypeScript build failure in the Nix derivation. Made the field optional
since only en.ts provides it currently.
2026-05-19 11:47:24 +05:30
roycepersonalassistant
e3823657d6 feat(kanban): add scheduled status for delayed follow-ups
Salvages #24533 by @roycepersonalassistant. Adds a first-class
'scheduled' Kanban status for time-delay follow-ups that aren't
waiting on human input.

- hermes kanban schedule <task_id> [reason] CLI command
- Dashboard/API transitions to/from Scheduled
- unblock_task() now releases both 'blocked' AND 'scheduled' tasks
  (re-checking parent dependencies before moving to ready/todo)
- i18n + docs updates

Resolved conflicts: kept HEAD's failure-counter reset on unblock
alongside the PR's scheduled state, kept HEAD's 'running' direct-set
rejection, combined both bulk-status branches. Dropped the dist/
bundle changes (months-stale; would need rebuild from source).
2026-05-18 21:39:03 -07:00
SerenityTn
1a5172742e feat(kanban): show dashboard cron jobs across profiles
Salvages #27568 by @SerenityTn. Dashboard cron page now lists cron
jobs from all profiles, with profile-aware filter UI and storage
routing. Includes test coverage for cross-profile listing, mutation,
deletion, and validation.

Also fixes orphan conflict markers in config.py left by an earlier
salvage merge (kanban.dispatch_stale_timeout_seconds was double-nested
in HEAD/PR markers from #28452 salvage of #23790).
2026-05-18 21:26:45 -07:00
maxmilian
c5cafd3847 fix(web): portal Change Model modal so it renders above the app sidebar
The dashboard's main column is `relative z-2` (App.tsx), which creates a
stacking context that traps fixed descendants below the app sidebar
(`z-50`). `ModelPickerDialog` renders `fixed inset-0 z-[100]` inline,
so its z-100 is scoped to z-2 and the sidebar covers its left edge.

The bug is visible across all themes but only obvious in the Large theme
variants (Hermes Teal (Large), etc.) where the larger root font widens
the dialog into the sidebar's column. Toast.tsx already documents the
same trap and uses the same `createPortal(..., document.body)` escape.

This commit ports the picker; the same pattern affects other inline
z-[100] modals in the dashboard (OAuthLoginModal, Cron / Models /
Profiles page modals) and is left for a follow-up — keeping this PR
scoped to the reporter's specific case.

Fixes #28103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:02:50 -07:00
Austin Pickett
6fa1701bd3
feat(web): mobile dashboard UX polish (#28127)
* feat(web): mobile dashboard UX polish

Bottom sheets for sidebar theme/language pickers on narrow viewports with
enter/exit animation and drag-to-close; inline header badges beside titles;
bottom padding on the route outlet for scroll clearance; profiles loading uses a
unicode braille spinner; align profile/cron card actions to the top; viewport-fit
cover and supporting layout tweaks across dashboard pages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Nix web npm hash and mobile sheet accessibility.

Align fetchNpmDeps in nix/web.nix with web/package-lock.json for CI. Improve BottomPickSheet backdrop labeling, avoid aria-hidden on the dialog during exit animation, and wire theme/language sheets with listbox semantics and localized dismiss labels.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:20:31 -04:00
Brian D. Evans
6a20ad6c0a
fix(dashboard): constrain theme picker dropdown height so themes are scrollable (#25213) (#25220)
The header theme picker (`ThemeSwitcher`) renders a `role="listbox"` popup
with no `max-height` or overflow. With 20+ community themes installed under
`~/.hermes/dashboard-themes/`, the list extends past the viewport and themes
at the top or bottom are unreachable — the user reports only 15 of 26 themes
visible, with no scrollbar to access the rest.

Sibling switchers (`LanguageSwitcher`, `SlashPopover`) already cap their
listboxes (`max-h-80 overflow-y-auto` / `max-h-64 overflow-y-auto`); this
just brings the theme picker into line. Scoped to the component instead of
a global `div[role="listbox"]` CSS rule so other dropdowns aren't affected.

`70dvh` matches the user's tested workaround and the `dvh` unit handles
mobile browser UI chrome correctly (unlike `vh`).

Fixes #25213.

Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:23:03 -04:00
duyua9
ac1536b19f
fix(web): render object config values structurally (#10949) 2026-05-18 10:03:25 -04:00
wesleysimplicio
74031e1e2a fix(dashboard): respect HERMES_BASE_PATH in WebSocket URLs (#25547)
When the dashboard is reverse-proxied under a path prefix
(`X-Forwarded-Prefix: /dashboard`), the SPA already routes its
`/api/...` REST traffic through `HERMES_BASE_PATH` via
`web/src/lib/api.ts`. Three WebSocket URLs constructed elsewhere
were still hardcoded to root `/api/...` and so opened
`wss://host/api/...` instead of `wss://host/dashboard/api/...`,
forcing operators to forward selected root API/WS paths through the
reverse proxy as a workaround (see issue #25547).

Add `HERMES_BASE_PATH` between `host` and `/api/...` in the
three constructed WebSocket URLs:

- `web/src/pages/ChatPage.tsx` — PTY WebSocket
- `web/src/components/ChatSidebar.tsx` — events subscriber
- `web/src/lib/gatewayClient.ts` — JSON-RPC gateway WebSocket

When the dashboard is served at root, `HERMES_BASE_PATH === """
and the URLs are bit-for-bit identical to before. Under a prefix,
the WebSocket connections now go through the same proxy path the
REST calls already use.

Note: bundled dashboard plugins (kanban, hermes-achievements) embed
`"/api/plugins/..."` in their compiled `dist/index.js` and
remain out of scope here — those need source-side fixes per plugin.

Fixes #25547.
2026-05-17 11:39:37 -07:00
wesleysimplicio
8e3cfdfb61 fix(webui): allow native text selection in chat via xterm.js bypass (#25720)
The chat panel renders via xterm.js, and when the inner Hermes TUI
enables mouse-events mode (CSI ?1000h family — used for nav inside
Ink overlays/pickers) every drag/double-click/triple-click in the
canvas is consumed by the terminal instead of producing a native
text selection. The reporter (macOS, Brave) confirmed:

- click-and-drag selects nothing
- Cmd+C with no selection copies the entire visible buffer
- existing CSS overrides and event handlers at the document layer
  have no effect — the issue is at xterm.js's mouse layer, not the
  DOM

Fix: two xterm.js options the user can opt into without disabling
mouse-events mode for the inner TUI:

- `macOptionClickForcesSelection: true` — holding Option (macOS)
  or Alt (Linux/Windows) during a click-and-drag bypasses mouse-events
  mode and produces a native xterm selection. This is the documented
  xterm.js path for this exact scenario. Selected text is copyable
  via Cmd+C / Ctrl+C through the existing OSC 52 + manual handlers.
- `rightClickSelectsWord: true` — right-click highlights the word
  under the pointer. Single-action path on top of the modifier-based
  bypass.

The two options coexist with the existing `macOptionIsMeta: true`
(which only affects keyboard, not mouse). No other code change
needed.

Fixes #25720.
2026-05-17 02:31:18 -07:00
Austin Pickett
ca413c6164 fix(dashboard): align Ukrainian Kanban Ready column help
Mirrors the dependency-ready / assign-profile semantics used in other locales;
Copilot review noted uk.ts was still on the old dispatcher-tick wording.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 23:39:29 -04:00
Austin Pickett
63503ebb14 fix(dashboard): clarify Kanban Ready vs assignment
Ready column help and fallbacks now describe dependency-ready work; show a
badge on unassigned ready cards and fix the stale unassigned tooltip. Align
localized Ready help strings with the new semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 22:40:21 -04:00
ioannis
0854640537 fix(web): cross-platform sync-assets + surface build errors on failure
Three Windows-only bugs in the web-dashboard build path. Each is small,
scoped, and verified end-to-end on Windows 11 — including under a stock
cmd.exe / PowerShell console with its default cp1252 encoding.

1. `sync-assets` shells out to Unix-only commands

   web/package.json hard-codes `rm -rf … && cp -r …`. Neither exists on
   Windows cmd.exe. `hermes_cli/main.py::_build_web_ui` runs npm via
   subprocess (which on Windows defaults to cmd.exe), so the prebuild
   hook crashed before Vite ever ran and the dashboard never built.

   Fix: web/scripts/sync-assets.mjs — ~20 lines of Node using fs.rmSync
   + fs.cpSync (stdlib, Node >= 16.7). No new deps, identical behavior
   on POSIX and Windows.

2. Build failures were silent

   _build_web_ui ran both subprocess calls with capture_output=True and
   never relayed the captured buffers on failure. Users saw 'Web UI
   build failed' and nothing else — no stdout, no stderr, no hint that
   the real problem was 'rm is not recognized'.

   Fix: inner _relay() helper that decodes and prints stdout + stderr
   (utf-8, errors='replace') whenever a step returns non-zero. Replaces
   the existing stderr_tail-only relay on the build path; success path
   is unchanged. (stderr_tail is preserved for the stale-dist fallback
   branch added by #23817.)

Salvaged from #13368 by @johnisag onto current main. Conflict
resolution preserves main's improvements:
- _run_npm_install_deterministic() (replaces bare subprocess.run for
  npm install)
- npm-build retry-after-sleep for Windows boot-time races (#23817)
- stale-dist fallback for non-interactive callers (#23817)

Closes #25073, #13368.
2026-05-14 15:57:59 -07:00
HxT9
ed84637d11 fix(web): make sync-assets script cross-platform
The prebuild step used `rm -rf` and `cp -r`, which fail on Windows
(`'rm' is not recognized`). Replace with an inline Node one-liner
using fs.rmSync / fs.cpSync so the build works on Windows, macOS,
and Linux without adding a dependency.
2026-05-14 15:55:17 -07:00
Teknium
f7ad2f1115
feat(dashboard): hide token/cost analytics behind config flag (default off) (#25438)
The Analytics page and the token/cost surfaces on the Models page show
local debug estimates only. They count input+output (and a bar viz adds
cache_read+reasoning, missing cache_write entirely) from successful
main-agent responses that returned a usable usage block.

Excluded silently:
- All auxiliary calls — context compression, title generation, vision,
  session search, web extract, smart approvals, MCP routing, plugin LLM
  access (13 production call sites bypass update_token_counts)
- Provider-side retries, fallback attempts
- Any call whose usage block didn't come back
- cache_write_tokens (column exists in sessions table but not returned
  by /api/analytics/models)

Real-world impact: a user on Kimi K2.6 saw 150K local vs 27M on the
OpenRouter side over the same window. Precise-looking numbers next to
provider billing create false confidence and support load.

This change adds dashboard.show_token_analytics (default False) to gate:
- The Analytics nav item (hidden from sidebar when off)
- The Analytics page (renders an explanation card instead of charts)
- Token bars, totals, cost figures, avg/api_calls on the Models page

The Models page keeps capability metadata (context window, vision,
tools, reasoning), the use-as-main/aux menu, sessions count, and
last-used timestamps when the flag is off.

Set dashboard.show_token_analytics: true in config.yaml to opt back in
to the local debug estimate. Fixing the underlying accounting (issue
#23270) is a separate, larger workstream.

Refs: #23270, #21705
2026-05-13 22:20:25 -07:00
aqilaziz
80375cbe2c fix(dashboard): display real config path on Config page
Replace the hardcoded i18n placeholder "~/.hermes/config.yaml" with the
real config_path returned from api.getStatus(), falling back to the i18n
string while loading or on API failure.

Co-authored-by: aqilaziz <gonzes7@gmail.com>
2026-05-12 16:40:10 -07:00
Austin Pickett
fc3fd6bb6b fix(dashboard): UI polish — modals, layout, consistency, test fixes
Dashboard UX polish pass — consolidates create forms into modals
triggered from the page header, fixes layout inconsistencies, adds
scroll-to navigation for the Keys page, and aligns the TokenBar with
the design system.

Changes:
- App.tsx: add padding to sidebar header
- resolve-page-title.ts: add missing routes, better fallback title
- en.ts: fix nav labels (Profiles was 'profiles : multi agents')
- ModelsPage: two-col layout, auxiliary tasks modal, TokenBar redesign
- ProfilesPage: create button in header, form in modal, Checkbox component
- CronPage: create button in header, form in modal
- EnvPage: scroll-to sub-nav in header, fix text overflow

Modal and dialog standardization:
- Replace all native confirm()/window.confirm() with ConfirmDialog
  (OAuthProvidersCard, PluginsPage, ModelsPage, ConfigPage)
- Add useModalBehavior hook (Escape-to-close, scroll lock, focus restore)
- Apply hook to ProfilesPage, CronPage, AuxiliaryTasksModal

Component fixes (from PR review):
- Checkbox: fix controlled/uncontrolled mismatch, add focus-visible ring
- TokenBar: add rounded-full to legend dots, remove dead code

CI/test fixes:
- Fix TS unused imports (noUnusedLocals), type-narrow PickerTarget union
- Add windows-footgun suppression on platform-guarded os.killpg
- Fix 19 stale unit tests + 9 e2e tests broken by recent main changes
- Restore minimal example-dashboard plugin for plugin auth test
2026-05-12 13:59:22 -04:00
Teknium
c39168453d
feat(i18n): localize all gateway commands + web dashboard, add 8 new locales (16 total) (#22914)
* feat(i18n): localize /model command output

Reported by @tianma8888: when Chinese users run /model, the labels
("Provider:", "Context:", "_session only_", etc.) are still English.
This routes the static prose through the existing i18n catalog so it
follows display.language / HERMES_LANGUAGE.

Changes:
- locales/{en,zh,ja,de,es,fr,tr,uk}.yaml: add 17 keys under
  gateway.model.* covering switched/provider/context/max_output/cost/
  capabilities/prompt_caching/warning/saved_global/session_only_hint/
  current_label/current_tag/more_models_suffix/usage_*.
- gateway/run.py _handle_model_command: replace hardcoded f-strings in
  the picker callback, the text-list fallback, and the direct-switch
  confirmation block with t("gateway.model.<key>", ...).

What stays English:
- model IDs, provider slugs, capability strings, cost figures, and the
  "[Note: model was just switched...]" prepended to the model's next
  prompt (LLM-facing, not user-facing).
- The two slightly-different session-only hints unify on a single key
  with the em-dash phrasing.

Validation: tests/agent/test_i18n.py 27/27 passing (parity contract
holds), tests/gateway/ -k 'model or i18n' 74/74 passing.

* feat(i18n): localize all gateway slash command outputs

Expands the i18n catalog from 7 strings to 234 keys across 35 gateway
slash command handlers, so non-English users see localized output for
\`/profile\`, \`/status\`, \`/help\`, \`/personality\`, \`/voice\`, \`/reset\`,
\`/agents\`, \`/restart\`, \`/commands\`, \`/goal\`, \`/retry\`, \`/undo\`,
\`/sethome\`, \`/title\`, \`/yolo\`, \`/background\`, \`/approve\`, \`/deny\`,
\`/insights\`, \`/debug\`, \`/rollback\`, \`/reasoning\`, \`/fast\`,
\`/verbose\`, \`/footer\`, \`/compress\`, \`/topic\`, \`/kanban\`,
\`/resume\`, \`/branch\`, \`/usage\`, \`/reload-mcp\`, \`/reload-skills\`,
\`/update\`, \`/stop\` (plus the \`/model\` block already added in the
previous commit).

Reported by @tianma8888 — Chinese users want command output prose in
their language, not just the labels we already had.

Translations are hand-written for all 8 supported locales (en, zh, ja,
de, es, fr, tr, uk), matching each catalog's existing style: full-width
punctuation in zh, em-dashes in zh/ja/uk, French spaced colons,
German noun capitalization, etc.

What stays English (unchanged):
- Identifiers/values: model IDs, file paths, profile names, session IDs,
  command flag names like --global, URLs, config keys.
- Backtick code spans: \`/foo\`, \`config.yaml\`.
- Log messages (logger.info/warning/error).
- LLM-facing system notes prepended to next prompt (e.g. [Note: model
  was just switched...]).
- Strings produced by external modules (gateway_help_lines,
  format_gateway, manual_compression_feedback) — those have their
  own surfaces.

New shared keys for cross-handler boilerplate:
- gateway.shared.session_db_unavailable (5 call sites: branch, title,
  resume, topic, _disable_telegram_topic_mode_for_chat)
- gateway.shared.session_not_found (1 site)
- gateway.shared.warn_passthrough (2 sites in /title's f"⚠️ {e}" pattern)

YAML gotcha fixed: \`yolo.on\` and \`yolo.off\` were originally written
unquoted, which YAML 1.1 parses as boolean True/False keys. Renamed to
\`yolo.enabled\` / \`yolo.disabled\` for both safety and clarity.

Test fix: tests/agent/test_i18n.py::test_t_missing_key_in_non_english_falls_back_to_english
now resets the catalog cache on teardown, so the fake "foo: English Foo"
locale doesn't poison the module-level cache for subsequent tests in
the same xdist worker. (Without this, every gateway slash command test
that shares a worker with the i18n suite would see the fake catalog.)

Validation:
- tests/agent/test_i18n.py: 27/27 (parity contract — every key in every
  locale, matching placeholder tokens).
- tests/gateway/: 5077 passed, 0 failed (full gateway suite).
- 180 t() call sites added across 35 handlers; 1872 catalog entries
  total (234 keys × 8 locales).

* feat(i18n): add 8 new locales — af, ko, it, ga, zh-hant, pt, ru, hu

Expands the static-message catalog from 8 → 16 languages, each with full
270-key parity against the English source-of-truth.  Every locale now
covers the same surface PR #22914 added: approval prompts plus all 35
gateway slash command outputs.

New locales:
- af  Afrikaans      (community ask in #21961 by @GodsBoy; PRs #21962, #21970)
- ko  Korean         (PRs #20297 by @tmdgusya, #22285 by @project820)
- it  Italian        (PR #20371 by @leprincep35700)
- ga  Irish/Gaeilge  (PR #20962 by @ryanmcc09-dot)
- zh-hant Traditional Chinese (PRs #20523 by @jackey8616, #13140 by @anomixer)
- pt  Portuguese     (PRs #20443 by @pedroborges, #15737 by @carloshenriquecarniatto, #22063 by @Magaav)
- ru  Russian        (PR #22770 by @DrMaks22)
- hu  Hungarian      (PR #22336 by @lunasec007)

Each locale uses native-quality translations matching the existing tone
and conventions of the older 8 locales:
- zh-hant uses 繁體 characters with TW/HK technical vocabulary (軟體
  not 软件, 連線 not 连接, 設定 not 设置, 訊息 not 消息, 工作階段 not 会话, 程式
  not 程序, 預設 not 默认, 伺服器 not 服务器), full-width punctuation 「:()」.
- ko uses formal 합니다체 (습니다/합니다) register throughout.
- pt uses European Portuguese as baseline with neutral PT/BR vocabulary
  where possible.
- ga uses standard An Caighdeán Oifigiúil; English loanwords retained
  for tech terms without good Irish equivalents (gateway, API, JSON).
- All preserve {placeholder} tokens, backtick code spans, slash commands,
  brand names (Hermes, MCP, TTS, YOLO, OpenAI, Telegram, etc.), and emoji.

Aliases added in agent/i18n.py:
- af-za, Afrikaans → af
- ko-kr, Korean, 한국어 → ko
- it-it, italiano → it
- ga-ie, Irish, Gaeilge → ga
- zh-tw, zh-hk, zh-mo, traditional-chinese → zh-hant (note: zh-tw used to
  alias to zh; now aliases to its own zh-hant catalog)
- zh-cn, zh-hans, zh-sg → zh (unchanged from before)
- pt-pt, pt-br, brazilian, portuguese → pt
- ru-ru, Russian, русский → ru
- hu-hu, Magyar → hu

The zh-tw alias re-routing is intentional: previously typing 'zh-TW' got
the Simplified Chinese catalog (wrong vocabulary for Taiwan/HK users).
Now those users get the proper Traditional Chinese catalog.

Validation:
- tests/agent/test_i18n.py: 43/43 (parity contract holds for all 16
  languages × 270 keys = 4320 catalog entries, with matching placeholder
  tokens).
- E2E alias resolution verified for all 19 alias inputs (Afrikaans, ko-KR,
  한국어, italiano, Gaeilge, zh-TW, zh-HK, traditional-chinese, pt-BR,
  brazilian, Magyar, etc.).
- tests/gateway/: 5198 passed (3 pre-existing TTS routing failures
  unrelated to i18n).

Credit to all contributors whose PRs surfaced these language requests.
Their original PRs may now be closed as superseded with credit.

* feat(dashboard-i18n): add 14 web dashboard locales matching the static catalog

Brings the React dashboard (web/src/) up to the same 16-language
coverage the static catalog already has after the previous commits in
this PR. The Translations interface is TypeScript-typed, so every new
locale must provide every key — tsc -b is the parity guard.

Languages added (each is a complete 429-line locale file):
- af  Afrikaans
- ja  Japanese        (PR #22513 by @snuffxxx surfaced this)
- de  German          (PR #21749 by @mag1art)
- es  Spanish         (PR #21749)
- fr  French          (PRs #21749, #10310 by @foXaCe)
- tr  Turkish
- uk  Ukrainian
- ko  Korean          (PRs #21749, #18894 by @ovstng, #22285 by @project820)
- it  Italian
- ga  Irish (Gaeilge)
- zh-hant Traditional Chinese (PR #13140 by @anomixer)
- pt  Portuguese      (PRs #22063 by @Magaav, #22182 by @wesleysimplicio, #15737 by @carloshenriquecarniatto)
- ru  Russian         (PRs #21749, #22770 by @DrMaks22)
- hu  Hungarian       (PR #22336 by @lunasec007)

Each translation covers all 15 namespaces with full key parity vs en.ts,
preserves every {placeholder} token verbatim, keeps identifiers
untranslated (brand names, file paths, cron expressions, code spans),
translates the language.switchTo tooltip into the target language, and
matches existing tone conventions (zh-hant uses TW/HK vocab; ja uses
formal desu/masu; ko uses formal seumnida register; ga uses An
Caighdean Oifigiuil with English loanwords for tech vocab without good
Irish equivalents).

Plumbing:
- web/src/i18n/types.ts: Locale union expanded to all 16 codes.
- web/src/i18n/context.tsx: imports all 16 catalogs; exports
  LOCALE_META (endonym + flag per locale); isLocale() type guard.
- web/src/i18n/index.ts: re-export LOCALE_META.
- web/src/components/LanguageSwitcher.tsx: replaced two-state EN-ZH
  toggle with a click-to-open dropdown listing all 16 languages.

Note: zh-hant.ts exports zhHant (camelCase) since hyphen is invalid in
a JS identifier; the canonical 'zh-hant' string keys it in TRANSLATIONS.

Validation:
- npx tsc -b: 0 errors. Every locale satisfies Translations.
- npm run build (tsc + vite production): green, 2062 modules.
- Each locale file is exactly 429 lines.

Out of scope: plugin dashboards (kanban/achievements ship as prebuilt
bundles with no source in repo); Docusaurus docs (separate surface);
TUI (no i18n yet).

* feat(plugin-i18n): localize achievements + kanban plugin dashboards across all 16 locales

Brings the two shipped plugin dashboards (hermes-achievements, kanban)
under the same i18n umbrella as the core dashboard PR #22914 just
established.  Both bundles now read user-facing strings from the host's
i18n catalog via SDK.useI18n() instead of hardcoded English.

## Approach

Plugin dashboards ship as prebuilt IIFE bundles in
plugins/<name>/dashboard/dist/index.js — no build step, no source in
repo (upstream-authored, vendored as compiled JS).  Earlier contributor
PRs (#22594, #22595, #18747) tried direct edits but didn't actually
wire the bundles to read translations.

This change does the wiring properly:

1.  Each bundle gets a useI18n shim at IIFE scope:
        const useI18n = SDK.useI18n
          || function () { return { t: { kanban: null }, locale: "en" }; };
    Older host SDKs without useI18n still load the bundle and render
    English fallbacks.

2.  A small tx(t, path, fallback, vars) helper resolves dotted keys
    under the plugin's namespace (t.kanban.* or t.achievements.*) and
    interpolates {placeholder} tokens.

3.  Every React component starts with const { t } = useI18n() and
    each user-visible string is wrapped in tx(t, "key", "English fallback").
    Helpers called outside React components (window.prompt callers,
    constants used during init) take t as a parameter.

4.  Top-level constants that were English dictionaries (COLUMN_LABEL,
    COLUMN_HELP, DESTRUCTIVE_TRANSITIONS, DIAGNOSTIC_EVENT_LABELS in
    kanban) become getColumnLabel(t, status)-style functions backed by
    FALLBACK_* dictionaries.

## Translations added

Two new top-level namespaces added to the dashboard's TypeScript-typed
Translations interface:

- achievements: ~70 keys covering the hero, scan banner, achievement
  card, share dialog, stats, filters, and empty states.
- kanban: ~145 keys covering the board, columns (with nested
  columnLabels and columnHelp sub-dicts), card detail panel,
  bulk-actions toolbar, dependency editor, board switcher, and
  diagnostic callouts.

Each key is provided across all 16 supported locales:
en, zh, zh-hant, ja, de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu.

Total new translation entries: ~3,440 (215 keys × 16 locales).

## What stays English (deliberate)

- API paths, CSS class names, data-* attributes, JSON keys, regex
  strings, URLs, file paths (~/.hermes/kanban.db, boards/_archived/).
- State identifier strings used as lookup keys (triage / todo / ready /
  running / blocked / done / archived) — labels translate, key strings
  don't.
- The PNG share-card text rendered to canvas in the achievements
  ShareDialog (HERMES AGENT watermark, UNLOCKED stamp, tier names) —
  these become part of a globally-shared image and stay English.
- localStorage keys (hermes.kanban.selectedBoard).
- Brand names (Kanban, Hermes, WebSocket, Nous Research).

## Contributor credit

PR #22594 by @02356abc and PR #22595 by @02356abc supplied the
en + zh kanban namespace skeleton (145 keys); used as the en source-
of-truth in this commit and translated to the other 14 locales.

PR #18747 by @laolaoshiren first surfaced the achievements
localization request.

## Validation

- npx tsc -b: 0 errors. All 16 locale .ts files satisfy the
  Translations type with full key parity.
- npm run build (tsc + vite production build): green, 2062 modules,
  1.56MB JS / 95KB CSS, ~2.5s build.
- node --check on both plugin bundles: parse cleanly.
- 126 tx() call sites in kanban, 46 in achievements.

## Out of scope

- TUI (ui-tui/) has no i18n infrastructure yet.
- Docusaurus docs (website/i18n/) — already had zh-Hans; expanding
  is a separate translation workstream (Thai / Korean / Hindi PRs).
2026-05-10 07:14:14 -07:00
kshitijk4poor
96dc272623 fix(cron): use getJobState helper in handlePauseResume
Self-review follow-up: handlePauseResume read job.state directly while
the rest of the page goes through getJobState(), which falls back to
the enabled flag when state is null/undefined. With the backend
normalizer in this PR, state is always populated on the wire, so this
has no observable effect today — but using the helper keeps the page
consistent and resilient against older Hermes backends that don't run
the normalizer.
2026-05-09 01:11:41 -07:00
LeonSGP43
e572737274 Fix cron dashboard rendering for partial jobs 2026-05-09 01:11:41 -07:00
Teknium
12a0f5901c
fix(dashboard): finish resumeId -> resumeParam rename in ChatPage (#21317)
Commit b12a5a72b renamed the local variable resumeId -> resumeParam at
line 157 but left two call sites referencing the old name at lines 555
and 660. tsc -b fails with two TS2304 errors, which tanks npm run build,
which makes `hermes dashboard` print "Web UI build failed" with no
further detail.

Finishes the rename at both call sites instead of re-introducing the
old name via an alias.

Co-authored-by: qiuqfang <qiuqfang98@qq.com>
2026-05-07 07:05:03 -07:00
cmcgrabby-hue
52e2777821 feat(dashboard): support serving under URL prefix via X-Forwarded-Prefix
The Hermes dashboard previously assumed it was served at the root of its
host (e.g. https://kanban.tilos.com/). When mounted behind a path-prefix
reverse proxy (e.g. https://mission-control.tilos.com/hermes/), the SPA
404'd because:

- index.html shipped absolute /assets/index-*.js URLs
- React Router had no basename
- The plugin loader hit /dashboard-plugins/<name>/... at the root host
- CSS in the bundle had absolute url(/fonts/...) references

This patch makes the dashboard prefix-aware at runtime, no rebuild
required. The proxy injects 'X-Forwarded-Prefix: /hermes' on every
request and the Python server:

- Rewrites href/src in served index.html to '${prefix}/assets/...'
- Injects 'window.__HERMES_BASE_PATH__="${prefix}"' for the SPA to read
- Rewrites url() refs in CSS at serve time

The SPA reads window.__HERMES_BASE_PATH__ once at boot and:

- Prefixes all /api/... fetches via api.ts
- Prefixes all /dashboard-plugins/... script/css URLs in usePlugins
- Sets <BrowserRouter basename={...}> so client-side routing works

When no X-Forwarded-Prefix header is present, behavior is unchanged
(empty prefix => serves at root, kanban.tilos.com keeps working).

Refs: MC-AUTO-13
2026-05-07 06:39:18 -07:00
memosr
5909526a06 fix(security): support SRI integrity verification for dashboard plugin scripts 2026-05-07 06:26:09 -07:00
CCClelo
b12a5a72b0 Follow latest child session on dashboard resume 2026-05-07 05:45:40 -07:00
nouseman666
7cbef2bd42 fix(dashboard): route browser wheel into inner TUI scrolling 2026-05-07 05:24:43 -07:00
nouseman666
8aceef539f fix(dashboard): let embedded chat use a single scroll system 2026-05-07 05:24:43 -07:00
nouseman666
a0758cd1e9 fix(dashboard): stabilize embedded chat resume and scrollback 2026-05-07 05:24:43 -07:00
Harish Kukreja
a5c9c83b78 fix(web): force light color-scheme on docs iframe
The Documentation tab embeds the public Hermes Agent docs site via an
<iframe>. On any system where the browser's prefers-color-scheme
resolves to dark — the default on macOS with system dark mode, and
common on Linux/Windows too — the docs body text rendered nearly
invisible against its own background.

Cause: Docusaurus intentionally leaves <html> and <body> transparent
and relies on the browser's Canvas color to fill the viewport. Inside
our iframe, the iframe element had bg-background (the dashboard's dark
canvas) AND inherited the dashboard's dark color-scheme, so the
browser set the iframe's Canvas to a dark value. Docusaurus's
transparent body exposed that dark Canvas, and the docs body text
(tuned for a light Canvas) became near-illegible. Affects every
built-in dashboard theme.

Fix: replace bg-background on the iframe with [color-scheme:light]
(spec-blessed cross-origin override of the inherited color-scheme;
forces the iframe's Canvas to light) and bg-white (belt-and-suspenders
fallback during the brief paint window before content loads). The
docs site's own theme toggle keeps working — Docusaurus stores its
choice in localStorage and applies opaque dark backgrounds to its
layout elements that cover the white Canvas we forced.
2026-05-07 04:55:47 -07:00
Teknium
6388aafbd6
feat(dashboard): add 'default-large' built-in theme with 18px base size (#20820)
Same Hermes Teal palette as the default theme, but with baseSize 18px,
lineHeight 1.65, and spacious density so the whole dashboard scales up.
Gives users a one-click bigger-text preset and a copyable reference for
authoring custom YAML themes with their own typography settings.
2026-05-06 09:10:44 -07:00
MichaelWDanko
da8654bb41 fix(dashboard): show custom theme palette swatches 2026-05-04 04:43:27 -07:00