* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes#230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes#248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix#247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix#246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes#247Fixes#246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Introduces a locale bundle system that makes UI language switchable at
runtime and trivially extensible to any future language.
Architecture:
- static/i18n.js: LOCALES object with 'en' and 'zh' bundles, t(key)
helper with English fallback, setLocale()/loadLocale() for persistence
via localStorage. Adding a new language = adding one object.
- api/config.py: 'language' setting (default 'en'), BCP-47 validation
- api/routes.py: _LOGIN_LOCALE dict for server-rendered login page;
template placeholders substituted at request time from saved setting
- static/index.html: loads i18n.js first (before other scripts); adds
Language dropdown to Settings panel, auto-populated from LOCALES
Wiring:
- boot.js: applies server-persisted locale at startup (after /api/settings
fetch); speech recognition lang follows _locale._speech
- panels.js: populates Language dropdown from LOCALES on settings open;
saves + applies locale on Save Settings
- All JS files: hardcoded user-facing strings replaced with t() calls
Coverage:
- test_sprint20.py: relaxed recognition.lang assertion to accept dynamic
locale-driven assignment (behavior unchanged for English default)
- 499/499 tests pass
Closes#177 (incorporates Chinese translations as a proper locale bundle
rather than hardcoded strings, so English default is fully preserved)
True black background with subtle borders for OLED displays.
Pure #000 backgrounds, low-opacity borders, and warm accent colors
to minimize burn-in risk and maximize contrast.
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes#139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unsaved-changes guard:
- _closeSettingsPanel() intercepts all three close paths (X button, overlay
click, Escape key) and checks _settingsDirty before closing
- If dirty: shows inline 'Unsaved changes' bar with Save & Close / Discard
- Discard reverts the live theme preview to what it was when panel opened
- _markSettingsDirty() wired to all inputs via addEventListener in loadSettingsPanel()
- saveSettings() now resets dirty flag and hides the bar on successful save
Theme improvements:
- Add 'Slate' theme: warm charcoal (#2b2d30 bg), a softer/lighter dark option
that sits between Dark and the full light themes
- Rework 'Light' theme: replace pure white (#f5f5f7) with warm off-white
(#f0ede8) -- warmer, lower contrast, less harsh on most displays
- Update /theme command to include 'slate' in valid list
- Add test_settings_set_theme_slate() to test_sprint26.py
The agent's run_conversation() already triggers context compression
internally, but the WebUI was unaware of the side effects:
1. Session ID rotation: compression creates a new session_id inside
the agent. The WebUI kept writing to the old session file, causing
silent data loss. Fix: detect agent.session_id mismatch after
run_conversation(), rename the session file, and update in-memory
caches.
2. No user notification: compression was invisible. Fix: emit a
'compressed' SSE event when compression is detected. Frontend shows
a system message and toast.
3. No manual control: Fix: add /compact slash command that sends a
message to the agent requesting context compression. Shows in the
autocomplete dropdown.
Detection works two ways:
- agent.session_id != original session_id (ID rotation)
- agent.context_compressor.compression_count > 0 (compressor state)
Closes#90
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Token usage display:
- Add 'show_token_usage' boolean to settings (default: false, off by default)
- Settings panel: checkbox 'Show token usage after responses'
- /usage slash command: instant toggle with toast feedback, persists to
server, updates checkbox if settings panel is open, re-renders messages
- Boot: load show_token_usage alongside send_key on startup
- ui.js: gate usage badge on window._showTokenUsage flag
Timestamps:
- streaming.py: stamp 'timestamp' on every message that lacks one at
conversation completion; old messages (no timestamp field) now get a
wall-clock time the first time they're touched by a new turn
- messages.js: stamp _ts on the last assistant message at done-event time
so the time shows immediately on the current turn before next reload
- Timestamps already render in the UI (Sprint 14): faint time on each
role header line, full opacity on hover, full date in title tooltip
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes#26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>