605f1eeab56c295729352e72ed252008b15f89a0
63
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb29e8a82e |
refactor(cron): rebrand Cron Recipes -> Automation Blueprints
Product rename across every surface: module/file names (blueprint_catalog, tools/blueprints, blueprint_cmd), slash command /cron-recipe -> /blueprint (alias /bp), dashboard API /api/cron/blueprints, desktop deep-link hermes://blueprint/<key>, docs catalog page + extract script, and the skill frontmatter block metadata.hermes.blueprint. No behavior change. |
||
|
|
1593ca5406 |
feat(cron): Cron Recipes — parameterized automation templates across every surface
A 'recipe' is a one-place definition of an automation that every surface renders natively. The slot schema (cron/recipe_catalog.py) is the single source of truth; four renderers consume it, and all paths end at the same cron.jobs.create_job — no second job engine. Form where there's a screen, conversation where there's a chat line: - Dashboard / GUI app: a Recipes sub-tab on the Cron page renders each recipe's typed slots as a form (time-picker, enum dropdown, free-text); submit POSTs /api/cron/recipes/instantiate which fills + creates the job. - CLI / TUI / messengers: /cron-recipe lists the catalog, shows a recipe's fields, or fills + creates from a pasted 'key slot=val' command. The shared handler (hermes_cli/cron_recipe_cmd.py) names any missing/invalid slot so the agent can ask a targeted follow-up. - Docs: a generated Cron Recipes catalog page (website, .mdx + React cards) shows each recipe with a copy-paste command and a 'Send to App' button. - Desktop: a hermes:// URL scheme (Electron single-instance lock + setAsDefaultProtocolClient + open-url/second-instance) routes hermes://cron-recipe/<key>?slot=val into the chat composer pre-filled. Typed slots (time/enum/text/weekdays) with defaults: users never type raw cron — recipes parameterize time-of-day and weekday sets and translate to cron expressions; a free-text 'schedule' slot is the full-flexibility escape hatch. Consent-first throughout: nothing schedules without an explicit submit or send. Core: - cron/recipe_catalog.py — CronRecipe + RecipeSlot, 5 curated recipes, recipe_form_schema / recipe_slash_command / recipe_deeplink / recipe_catalog_entry renderers, fill_recipe (validate + translate to create_job kwargs). - hermes_cli/cron_recipe_cmd.py — shared /cron-recipe handler (CLI + TUI + gateway never drift). CommandDef + dispatch in commands.py / cli.py / gateway/run.py. Dashboard: GET /api/cron/recipes + POST /api/cron/recipes/instantiate (web_server.py), CronRecipes.tsx gallery+form, Segmented sub-tab on CronPage, api.ts methods + types. Desktop: hermes:// scheme end to end (main.cjs deep-link router + ready-queue, preload onDeepLink/signalDeepLinkReady, global.d.ts types, desktop-controller composer prefill, electron-builder protocols key). Docs: extract-cron-recipes.py generator wired into prebuild.mjs, cron-recipes-catalog.mdx + CronRecipesCatalog React component, sidebar entry. Generated index json gitignored like skills.json. Tests: 23 core (catalog/slots/schedule-resolution/validation/renderers/command handler/generator) + 5 web_server endpoint tests. E2E verified end to end: slot fill -> create_job -> persisted job with correct schedule/deliver/origin. |
||
|
|
a09343cc96 |
feat(dashboard): SKILL.md editor on Skills page + attach-skill selector in cron modals (#44231)
Headless/VPS users (dashboard-over-Tailscale, no comfortable SSH) could list/toggle/install skills and create/edit cron jobs, but not author a custom skill or link one to a cron job — the UI set WHEN a job runs, but not WHICH skill it uses. - Skills page: 'New skill' button + per-row edit pencil open a SKILL.md editor dialog (frontmatter + body, server-side validation via the same _create_skill/_edit_skill path as the agent's skill_manage tool). - New endpoints: GET /api/skills/content, POST /api/skills, PUT /api/skills/content — all profile-scoped via _profile_scope(), which now also retargets tools.skill_manager_tool's import-time SKILLS_DIR binding. - Cron page: skills multi-select in both create and edit modals (parity with hermes cron --skill / edit --add-skill); CronJobCreate gains a skills field; job cards show an attached-skills badge. update_job already accepted skills in updates. - Tests: 17 new endpoint tests (content read, create/edit validation + profile scoping + auth gate, cron skills round-trip). |
||
|
|
875aa8f162 |
feat(dashboard): unify multi-profile management — one machine dashboard, global profile switcher (#44007)
* feat(dashboard): unify multi-profile management — one machine dashboard, global profile switcher The dashboard becomes a machine-level management surface with one write-target selector, replacing per-profile dashboard fragmentation. Backend: - profile param (query or body) on /api/config (get/put/raw), /api/env (get/put/delete/reveal), /api/mcp/servers (list/add/remove/test/enabled), /api/mcp/catalog (list/install), /api/model/info, /api/model/set — all scoped through the existing _profile_scope() context manager - model/set restructured: expensive-model warning (await) runs before the scope; the config write runs sync inside the scope in a worker thread - MCP catalog installs + git-bootstrap entries spawn 'hermes -p <profile>' - chat PTY: ?profile= on /api/pty points the child's HERMES_HOME at the profile dir (its own gateway subprocess, config/skills/memory/state.db all profile-bound); in-process gateway attach skipped when scoped CLI launch unification: - '<profile> dashboard' routes to the machine dashboard: attach (open browser at ?profile=) when one is listening, else re-exec pinned to the default profile with --open-profile preselecting the launcher - --isolated preserves the old dedicated per-profile server behavior - start_server(initial_profile=...) appends ?profile= to the auto-open URL Frontend: - ProfileProvider + sidebar ProfileSwitcher: ONE global selector, URL- persisted (?profile=), mirrored into fetchJSON which auto-appends the param to the scoped endpoint families (explicit params win) - app-wide amber banner names the managed profile - SkillsPage's page-local selector (from the skills-scoping PR) folded into the global context — single source of truth - ChatPage threads the scope into the PTY WS URL; switching profiles remounts the terminal into a fresh scoped session Omitted profile keeps legacy behavior everywhere. * docs(dashboard): document machine-level multi-profile management - web-dashboard.md: 'Managing multiple profiles' section (switcher, URL deep-links, unified launch, --isolated, scoped Chat, what stays per-profile) + --isolated in the options table - profiles.md: 'From the dashboard' subsection + set-as-active vs switcher clarification - cli-commands.md: --isolated flag + profile-alias launch example * fix(dashboard): address profile-unification review findings Review findings (dev review on PR #44007): 1. HIGH — stale page state on profile switch: pages load data on mount and didn't consume the profile scope, so a page opened under profile A kept showing A's state while writes silently targeted the newly selected B. Fixed structurally: ProfileKeyedRoutes wraps the routed page tree and keys it by the selected profile, remounting every page (fresh state + refetch) on switch. ChatPage keeps its own remount (channel keyed on scopedProfile). 2. HIGH — /api/model/auxiliary read was unscoped while /api/model/set wrote scoped (Models page could show default's aux pins while editing worker's). Endpoint now takes profile + _profile_scope, added to PROFILE_SCOPED_PREFIXES, HTTPException re-raise so ghost profiles 404 instead of 500. Regression test asserts read/write symmetry with differing worker/default aux config. 3. MEDIUM — tools post-setup spawned unscoped from the profile-aware drawer. Now spawns 'hermes -p <profile> tools post-setup <key>' (same mechanism as hub installs); drawer threads its profile prop. Most hooks install machine-level artifacts where the scope is inert, but hooks reading config/env now see the drawer's HERMES_HOME. 4. LOW — ty warnings: env Optional asserts before subscript/membership, fastapi import replaced with web_server.HTTPException re-use. 298 tests green across the four affected suites; tsc -b + vite build green; aux scoping E2E-verified with real imports. * fix(dashboard): address second profile-unification review (gille) 1. BLOCKER — profile scope dropped on sidebar navigation: ProfileProvider derived the selection from the current URL, and nav links are bare paths, so clicking Config from /skills?profile=worker silently reset the write target. State is now the source of truth; an effect re-asserts ?profile= onto the new location after every navigation (URL stays a synchronized projection for deep links/refresh), and an incoming URL param (e.g. 'Manage skills & tools' links) still wins. 2. BLOCKER — /api/model/options unscoped while model/set wrote scoped: the picker context (current model/provider, custom providers, per-profile .env auth state) now loads inside _profile_scope; added to PROFILE_SCOPED_PREFIXES. Test: a worker-only current-model pin appears in the scoped payload and not the unscoped one. 3. BLOCKER — MCP test-server probe escaped the scope after the config read: the probe now re-enters _profile_scope inside the worker thread so env-placeholder expansion resolves against the selected profile's .env. Known limit (documented): the probe's dedicated MCP event-loop thread doesn't inherit the contextvar (OAuth token paths). Test asserts get_hermes_home() inside the probe == the worker profile dir. 4. BLOCKER — broad excepts swallowed unknown-profile 404s: /api/model/info degraded to 200-with-empty-model-info and /api/mcp/catalog to a silently-empty catalog. Both re-raise HTTPException; 404 regression tests added for info/options/catalog. Polish: scope banner clears the fixed mobile header (mt-14 lg:mt-0); --open-profile hidden via argparse.SUPPRESS (internal re-exec flag); attach-path test now asserts the opened ?profile= URL. (Stale-page-state + /api/model/auxiliary findings from this review were already fixed in |
||
|
|
914befa9aa |
feat(dashboard): profile-scoped skills & toolsets management
'Set as active' on the Profiles page only flips the sticky active_profile file (future CLI/gateway runs) — it never retargets the running dashboard process. The skills/toolsets endpoints called bare load_config()/ save_config(), so after 'activating' a profile in the web UI, deactivating a skill silently wrote into the dashboard's own profile and the activated profile was untouched. Backend: - _profile_scope() context manager on the skills/toolsets endpoints: context-local HERMES_HOME override for call-time config resolution + cron-style locked swap of tools.skills_tool's import-time SKILLS_DIR - profile param on /api/skills, /api/skills/toggle, /api/tools/toolsets* (list/toggle/config/provider/env), hub sources/search installed-state - hub install/uninstall/update spawn 'hermes -p <profile> skills ...' so the child rebinds skills_hub.SKILLS_DIR at import (the override cannot reach import-time globals); profile validated -> 404/400 before spawn Frontend: - Skills page: profile selector (deep-linkable /skills?profile=<name>), amber banner naming the managed profile, threaded through skill toggles, toolset drawer, and hub browser - Profiles page: 'Manage skills & tools' action per card; 'Set as active' toast now says it applies to new CLI/gateway runs only Omitted profile keeps legacy behavior (dashboard's own profile). |
||
|
|
af978ecb17 |
fix(model): require confirmation for expensive model selections
Rebased onto current main and re-ported across the restructured surfaces: model flows now thread confirm_provider/base_url/api_key through hermes_cli/model_setup_flows.py, the Discord picker lives in plugins/platforms/discord/adapter.py, and the web dashboard picker applies chat-mode switches via config.set so the expensive-model confirmation can ride the response. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ae94ed1728 |
fix(tui-gateway): reap leaked slash_worker sessions on disconnect + active_list liveness (re-scoped onto current main)
Salvaged from #35626 (banditburai) and re-scoped after maintainers landed the parent-death watchdog (slash_worker.py) and PTY process-group teardown (pty_bridge.py) directly on main. Those pieces are intentionally NOT included here — this carries only what is still missing: - C1 disconnect reap: ws.py's `finally` only re-pointed the dead transport at stdio. `_close_sessions_for_transport` now reaps `close_on_disconnect` sessions and schedules the grace-reap for the rest, offloaded via `asyncio.to_thread` so the blocking worker.close() + DB write never stalls the uvicorn loop. - C2 create/close orphan race: `_attach_worker` stores the worker iff `_sessions.get(sid) is session` under the lock (else closes it), applied at every spawn site incl. the post-turn `_restart_slash_worker`. - Single idempotent teardown funnel: session.close, WS disconnect, the generous-TTL idle reaper, shutdown, and the WS grace-reap all reach `_close_session_by_id` → `_teardown_session`; `_finalized`/`_closed` flags make concurrent/double teardown a no-op. `_sessions_lock` upgraded to RLock. - uvicorn `ws_ping_interval/timeout=20s` so a half-open socket (reverse-proxy 524) becomes a `WebSocketDisconnect` and the C1 path runs. Plus two review-driven hardening fixes (mine): - `session.active_list` now skips `_finalized` sessions so the footer "N sessions" count reflects attachable sessions instead of only ever growing until restart (#38950). Keys on `_finalized` only, NOT the stdio sentinel, so a standalone `hermes --tui` session stays visible. - `_schedule_ws_orphan_reap._reap` pops via `_close_session_by_id` (under `_sessions_lock`) instead of `_sessions.pop` under the unrelated `_session_resume_lock` (#39591); the resume_lock now only guards the orphan re-check against `session.resume`. - Float env knobs (`HERMES_SLASH_WATCHDOG_*`, `HERMES_TUI_SESSION_TTL_S`) parse with a fallback helper so a malformed value can't crash the worker at import. Fixes #32377 Fixes #38950 Addresses #22855 Co-authored-by: banditburai <123342691+banditburai@users.noreply.github.com> Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> |
||
|
|
9e63109522 |
feat(dashboard): change UI font from the theme picker, independent of theme (#41145)
The dashboard font is now selectable from the UI, not just YAML. A new Font section in the header theme picker overrides the UI font of whatever theme is active; the choice is orthogonal to the theme and survives theme switches. Each theme keeps its own font as the default — picking "Theme default" clears the override. - web/src/themes/fonts.ts: curated font catalog (system + Google Fonts across sans/serif/mono), each with a family stack and optional webfont URL. The catalog is the only injected-font surface — no free-text URL box, so the injected <link> origins stay fixed. - web/src/themes/context.tsx: font-override state (localStorage + server), applied after theme typography so it wins; theme apply re-asserts it, and clearing re-runs theme apply to restore the theme's own font. Mono is left to the theme so code/terminal are untouched. - web/src/components/ThemeSwitcher.tsx: Font section with grouped, self- previewing font rows and a "Theme default" clear option. - hermes_cli/web_server.py: GET/PUT /api/dashboard/font persisting to config.yaml dashboard.font, with a server-side id allow-list (unknown ids coerce to the theme sentinel). - i18n + types, api client methods, tests, and docs. Validation: 6 new backend endpoint tests pass; tsc + vite build clean; live browser test confirmed pick/persist/survive-theme-switch/clear all work. |
||
|
|
2bf0a6e760 |
feat(dashboard): full tool backend configuration in the GUI (#40418)
Replicate the `hermes tools` configurator in the dashboard Skills →
Toolsets view. Each toolset now opens a config drawer that covers the
full lifecycle the CLI offers: enable/disable, pick a provider/backend,
enter and save API keys, and run a provider's post-setup install hook
with a live log tail.
The toolset view was previously read+toggle only — the provider matrix
and key-status endpoints existed but the page never called them, and
there was no way to save a key or run a backend install (npm/pip/binary)
from the browser.
Backend:
- New CLI subcommand `hermes tools post-setup <KEY>` — non-interactive,
scriptable target that runs a provider's install hook (agent_browser,
camofox, cua_driver, kittentts, piper, ddgs, spotify, langfuse,
xai_grok). Validated against valid_post_setup_keys() so an arbitrary
key can't drive _run_post_setup.
- PUT /api/tools/toolsets/{name}/env — save API keys to ~/.hermes/.env
via save_env_value (same store the CLI writes), validated against the
toolset category's env-var allowlist; blank values skipped.
- POST /api/tools/toolsets/{name}/post-setup — spawn-action that runs
`hermes tools post-setup <key>`; frontend tails the log via the
existing /api/actions/tools-post-setup/status. Registered in
_ACTION_LOG_FILES.
Frontend:
- New ToolsetConfigDrawer component (provider radios, password key
inputs with saved-state, get-a-key links, Run-setup + live install
log). Toolset cards get a Configure button + the drawer also exposes
the enable toggle.
- api.ts: toggleToolset, getToolsetConfig, selectToolsetProvider,
saveToolsetEnv, runToolsetPostSetup + ToolsetConfig/Provider/EnvVar/
EnvResult types.
Validation: 56 admin-endpoint tests pass (10 new: env save w/ CLI
parity + allowlist reject + blank-skip, post-setup spawn validation,
auth gate); 232 web_server tests pass; web npm run build + eslint clean;
HTTP E2E exercises save-key (CLI reads it back) and spawn+poll
post-setup to exit 0.
|
||
|
|
6d14a24b79 |
feat(dashboard): nous-blue theme, bulk sessions, schedule picker (#37383)
* 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>
|
||
|
|
7527e7aeac |
feat: fuzzy search for the model picker (WebUI + TUI)
Adds fuzzy subsequence matching with quality ranking to the model pickers, replacing the WebUI's exact-substring filter and giving the TUI a search where it previously had none. - New fuzzy scorer (ui-tui/src/lib/fuzzy.ts + an identical copy at web/src/lib/fuzzy.ts, since the two are separate TS packages with no shared module). Matches a query as an ordered subsequence (so `g4o` matches `gpt-4o`), scores by quality (exact > prefix > word-boundary > contiguous > scattered) and returns matched character positions for highlighting. Multi-token AND semantics (`clad snnt` -> claude-sonnet). 15 vitest tests cover the algorithm. - WebUI ModelPickerDialog: ranked fuzzy filter on providers + models; matched characters in model rows are highlighted via <mark>. - TUI modelPicker: type-to-filter on the provider and model stages with live ranking. Backspace edits the filter, Ctrl+U clears it, Esc clears a non-empty filter before navigating back. Persist-global / disconnect shortcuts moved from g/d to Ctrl+G / Ctrl+D so letters feed the filter. Closes #30849 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ac1536b19f | fix(web): render object config values structurally (#10949) | ||
|
|
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. |
||
|
|
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 |
||
|
|
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).
|
||
|
|
a0758cd1e9 | fix(dashboard): stabilize embedded chat resume and scrollback | ||
|
|
da8654bb41 | fix(dashboard): show custom theme palette swatches | ||
|
|
ca7f46beb5 |
Merge upstream/main and address Copilot review feedback
Merge resolved conflicts in web/src/{i18n/{en,zh,types}.ts,lib/api.ts}
by keeping both this branch's `profiles` additions and upstream's new
`models` page additions.
Copilot review feedback:
- Implement POST /api/profiles/{name}/open-terminal endpoint (already
present); align Windows branch to `cmd.exe /c start "" <cmd>` so it
matches the new test and spawns a fresh window instead of /k reusing
the parent console.
- Move backslash escaping out of the macOS AppleScript f-string
expression (Python <3.12 disallows backslashes inside f-string
expression parts).
- Patch `_get_wrapper_dir` via monkeypatch in
test_profiles_create_creates_wrapper_alias_when_safe so the test no
longer writes to the real `~/.local/bin`.
- Extend test_dashboard_browser_safe_imports to scan `.ts` files in
addition to `.tsx`.
- Switch upstream's new ModelsPage.tsx away from the `@nous-research/ui`
root barrel onto per-component subpaths to satisfy the stricter scan.
- Fix NouiTypography `leading-1.4` -> `leading-[1.4]` so Tailwind
actually emits the line-height for the `sm` variant.
- Guard ProfilesPage.openSoulEditor against out-of-order responses by
tracking the latest requested profile via a ref.
- Replace ProfilesPage's hand-rolled setup command with a fetch to
`/api/profiles/{name}/setup-command` so the copied command always
matches what the backend would actually run (handles wrapper-alias
collisions and reserved names correctly).
- Wire SOUL.md textarea label `htmlFor` -> textarea `id` so screen
readers and clicking the label work as expected.
|
||
|
|
3c27efbb91 |
feat(dashboard): configure main + auxiliary models from Models page (#17802)
Dashboard Models page was analytics-only — no way to pick a model as main
for new sessions or override an auxiliary task slot without hand-editing
config.yaml or running a /model slash command inside a chat.
Changes:
- hermes_cli/web_server.py: three REST endpoints (GET /api/model/options,
GET /api/model/auxiliary, POST /api/model/set). Reuses
list_authenticated_providers() from model_switch.py so the REST path
surfaces the same curated model lists as the TUI-gateway model.options
JSON-RPC. POST /api/model/set writes model.provider + model.default for
scope=main, and auxiliary.<task>.{provider,model} for scope=auxiliary
(with task="" meaning 'all 8 slots' and task="__reset__" resetting them
to auto).
- web/src/components/ModelPickerDialog.tsx: accepts an optional loader +
onApply pair so it works without an open chat PTY. ChatSidebar's
gw-WebSocket path still works unchanged (back-compat).
- web/src/pages/ModelsPage.tsx: Model Settings panel at the top showing
main model + collapsible list of 8 auxiliary tasks with per-row Change
buttons and Reset all to auto. Every existing model card gets a
'Use as' dropdown for one-click assignment to main or any aux slot.
Cards badged 'main' or 'aux · <task>' when currently assigned.
- website/docs/user-guide/configuring-models.md: new docs page walking
through both UI paths, aux task override patterns, troubleshooting,
plus REST/CLI alternatives.
- Screenshots under website/static/img/docs/dashboard-models/.
Applies to new sessions only — running sessions keep their model (use
/model slash command to hot-swap a live session). No prompt-cache
invalidation on existing sessions.
|
||
|
|
4c0cc77e94 | fix(dashboard): keep ui imports browser-safe after rebase | ||
|
|
1745cfc6d7 | fix(dashboard): avoid node-only ui imports in browser | ||
|
|
47d4b6e31a | feat: add spinner, lowercase version | ||
|
|
912590a143 | fix: button sizes | ||
|
|
1285172aca | fix(components): refactor to use design system | ||
|
|
e1027134cd | chore: remove comments | ||
|
|
0348a69c51 | fix: migrate select to design system | ||
|
|
753a071491 | fix: badges | ||
|
|
e5601d1e85 | fix: update design language | ||
|
|
a9369fc193 | chore: more components | ||
|
|
e116957a63 | fix: replace all buttons for design system buttons | ||
|
|
850fac14e3 | chore: address copilot comments | ||
|
|
63975aa75b | fix: mobile chat in new layout | ||
|
|
f49afd3122 |
feat(web): add /api/pty WebSocket bridge to embed TUI in dashboard
Exposes hermes --tui over a PTY-backed WebSocket so the dashboard can
embed the real TUI rather than reimplement its surface. The browser
attaches xterm.js to the socket; keystrokes flow in, PTY output bytes
flow out.
Architecture:
browser <Terminal> (xterm.js)
│ onData ───► ws.send(keystrokes)
│ onResize ► ws.send('\x1b[RESIZE:cols;rows]')
│ write ◄── ws.onmessage (PTY bytes)
▼
FastAPI /api/pty (token-gated, loopback-only)
▼
PtyBridge (ptyprocess) ── spawns node ui-tui/dist/entry.js ──► tui_gateway + AIAgent
Components
----------
hermes_cli/pty_bridge.py
Thin wrapper around ptyprocess.PtyProcess: byte-safe read/write on the
master fd via os.read/os.write (not PtyProcessUnicode — ANSI is
inherently byte-oriented and UTF-8 boundaries may land mid-read),
non-blocking select-based reads, TIOCSWINSZ resize, idempotent
SIGHUP→SIGTERM→SIGKILL teardown, platform guard (POSIX-only; Windows
is WSL-supported only).
hermes_cli/web_server.py
@app.websocket("/api/pty") endpoint gated by the existing
_SESSION_TOKEN (via ?token= query param since browsers can't set
Authorization on WS upgrades). Loopback-only enforcement. Reader task
uses run_in_executor to pump PTY bytes without blocking the event
loop. Writer loop intercepts a custom \x1b[RESIZE:cols;rows] escape
before forwarding to the PTY. The endpoint resolves the TUI argv
through a _resolve_chat_argv hook so tests can inject fake commands
without building the real TUI.
Tests
-----
tests/hermes_cli/test_pty_bridge.py — 12 unit tests: spawn, stdout,
stdin round-trip, EOF, resize (via TIOCSWINSZ + tput readback), close
idempotency, cwd, env forwarding, unavailable-platform error.
tests/hermes_cli/test_web_server.py — TestPtyWebSocket adds 7 tests:
missing/bad token rejection (close code 4401), stdout streaming,
stdin round-trip, resize escape forwarding, unavailable-platform ANSI
error frame + 1011 close, resume parameter forwarding to argv.
96 tests pass under scripts/run_tests.sh.
(cherry picked from commit
|
||
|
|
0fdbfad2b0 | feat: embed docs | ||
|
|
e5d2815b41 | feat: add sidebar | ||
|
|
f593c367be |
feat(dashboard): reskin extension points for themes and plugins (#14776)
Themes and plugins can now pull off arbitrary dashboard reskins (cockpit
HUD, retro terminal, etc.) without touching core code.
Themes gain four new fields:
- layoutVariant: standard | cockpit | tiled — shell layout selector
- assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} —
artwork URLs exposed as --theme-asset-* CSS vars
- customCSS: raw CSS injected as a scoped <style> tag on theme apply
(32 KiB cap, cleaned up on theme switch)
- componentStyles: per-component CSS-var overrides (clipPath,
borderImage, background, boxShadow, ...) for card/header/sidebar/
backdrop/tab/progress/badge/footer/page
Plugin manifests gain three new fields:
- tab.override: replaces a built-in route instead of adding a tab
- tab.hidden: register component + slots without adding a nav entry
- slots: declares shell slots the plugin populates
10 named shell slots: backdrop, header-left/right/banner, sidebar,
pre-main, post-main, footer-left/right, overlay. Plugins register via
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A
<PluginSlot> React helper is exported on the plugin SDK.
Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML +
slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS
sidebar with live telemetry, COMPASS crest in header, notched card
corners via componentStyles, scanline overlay via customCSS, gold/cyan
palette, Orbitron typography.
Validation:
- 15 new tests in test_web_server.py covering every extended field
- tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures)
- tsc -b --noEmit: clean
- vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions
Co-authored-by: Teknium <p@nousresearch.com>
|
||
|
|
c1949e844b | fix: imports |