Two changes:
1. Workspace updates correctly on profile switch
switchToProfile() now applies data.default_workspace from the switch
response to the current session via /api/session/update, updates
S.session.workspace in-memory, and stores S._profileDefaultWorkspace
so the next new session also inherits the profile's workspace.
newSession() in sessions.js picks up S._profileDefaultWorkspace when
creating a new session after a profile switch.
2. Workspace chip removed from topbar
The workspace was shown in two places: the topbar chip (wsChip) AND
the sidebar bottom display (sidebarWsDisplay with name + full path).
The topbar chip was redundant, cluttered the topbar, and pushed other
chips (profile, model, clear, settings) off screen.
Removed wsChip from the topbar entirely. The sidebar display is now
the sole workspace UI, consistent and unambiguous.
Moved wsDropdown to live inside the sidebar position:relative wrapper
so it opens downward from sidebarWsDisplay. Updated the click-outside
listener to close on clicks outside sidebarWsDisplay/wsDropdown.
Removed stale wsChip update code from syncTopbar() in ui.js.
Tests: 426 passed, 0 failed.
Fix five coherence bugs in profile switching:
1. Model picker ignored profile default (localStorage stale key)
2. Workspace list was global (not profile-scoped)
3. DEFAULT_WORKSPACE was a boot-time singleton
4. Session list showed all profiles (no filtering)
5. switchToProfile() didn't refresh workspaces or sessions
Backend: workspace storage is now profile-local for named profiles,
switch_profile() returns default_model and default_workspace.
Frontend: switchToProfile() clears stale model pref, refreshes
workspace list and session list, sessions.js filters by active profile
with 'Show N from other profiles' toggle.
8 new tests. 400 pass / 23 fail (identical to baseline).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mobile responsive (Issue #21):
- Hamburger sidebar: slide-in overlay on mobile (<640px) with backdrop.
Tap hamburger in topbar to open, tap outside to close. Full session
list, project chips, all panel content accessible.
- Bottom navigation bar: 5-tab fixed bar (Chat, Tasks, Skills, Memory,
Spaces) replaces sidebar nav tabs on mobile. iOS-style layout.
Tapping a tab opens the sidebar overlay with that panel active.
- Right panel slide-over: Files button in topbar chips opens workspace
panel as a slide-over from the right on mobile/tablet.
- Touch targets: all interactive elements get min 44x44px touch areas.
Session items, approval buttons, composer buttons all sized for fingers.
- Composer positioned above bottom nav bar with proper spacing.
- Sidebar nav tabs and bottom section hidden on mobile (replaced by
bottom nav + topbar chips).
- Clicking a session auto-closes the sidebar overlay.
- Desktop layout completely unchanged — all mobile elements are
display:none by default, only shown inside @media(max-width:640px).
Docker (Issue #7):
- Dockerfile: python:3.12-slim, HERMES_WEBUI_HOST=0.0.0.0, port 8787.
- docker-compose.yml: named volume for state persistence, optional
~/.hermes mount for agent features, password env var documented.
- README: Docker quick start section with compose and manual commands.
Tests: 392 passed, 23 pre-existing failures, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
- index.html: add #btnMic (hidden by default, shown if browser supports
SpeechRecognition) and #micStatus listening indicator inside .composer-box
- boot.js: IIFE-scoped mic handler wired to Web Speech API
* recognition.continuous=false (auto-stops after ~2s silence)
* recognition.interimResults=true (live transcript preview in textarea)
* Toggles .recording class + shows #micStatus while active
* Handles 'not-allowed', 'no-speech', 'network' errors via showToast()
* btnSend.onclick stops active recognition before sending
* Entire feature disabled/hidden gracefully when API unavailable
- style.css: .mic-btn, .mic-btn.recording (red pulse animation),
.mic-status, .mic-dot, @keyframes mic-pulse
- tests/test_sprint20.py: 46 tests covering HTML structure, CSS rules,
JS logic, error handling, and regression checks (376 total, all pass)
No API keys, no external libraries, no server changes. Browser-only.
Works in Chrome, Edge, Safari (partial). Firefox unsupported (hides button).
The previous logic treated a blank password field as intent to clear auth,
which meant saving any other setting (model, send key, etc.) would silently
disable password protection.
New behavior:
- Blank password field + Save Settings = no change to auth (do nothing)
- Password field with content + Save = set/change password (unchanged)
- 'Disable Auth' button = explicit confirmation-gated clear (new)
UI changes:
- index.html: updated description text to 'Leave blank to keep current
setting'; added 'Disable Auth' button (amber, shown only when auth active)
- panels.js: saveSettings() skips password logic entirely when field is blank;
loadSettingsPanel() shows/hides both btnDisableAuth and btnSignOut based on
auth_enabled; new disableAuth() function sends _clear_password:true after
confirm() prompt and hides both auth buttons on success
Server: no logic changes needed; _clear_password handling in save_settings()
is now only triggered by the explicit Disable Auth action.
Auth system (off by default, zero friction for localhost):
- New api/auth.py module: password hashing (SHA-256 + STATE_DIR salt),
signed HMAC session cookies (24h TTL), auth middleware
- Enable via HERMES_WEBUI_PASSWORD env var or Settings panel
- Minimal dark-themed login page at /login (self-contained HTML)
- POST /api/auth/login, /api/auth/logout, GET /api/auth/status
- Settings panel: "Access Password" field + "Sign Out" button
- password_hash added to settings.json (null = auth disabled)
Security hardening:
- Security headers on all responses: X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin
- POST body size limit: 20MB cap in read_body() to prevent DoS
Closes#23. 9 new tests. Total: 304 passed, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix stale tree cache: clear _dirCache and _expandedDirs on root nav
- Fix clearPreview: prompt before discarding unsaved preview edits
- Update UI version label from v0.17.1 to v0.20
- Add Sprint 18 entry to CHANGELOG.md
- Update SPRINTS.md current state to v0.20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
1. Path traversal in _serve_static() [CRITICAL]
Sandbox resolved path to static/ directory using relative_to().
GET /static/../../../../etc/passwd now returns 404.
2. Skill category path traversal [HIGH]
Validate category param in skill save: reject values with '/' or '..'.
3. Gate /api/approval/inject_test to loopback only [HIGH]
Endpoint now returns 404 for any non-127.0.0.1 client,
preserving test functionality while blocking remote access.
4. Escape captured groups in renderMd() [HIGH]
All inline markdown regexes (bold, italic, headings, blockquote,
list items, table cells/headers, link labels) now run captured
text through esc() before inserting into innerHTML, preventing
XSS via AI-generated content.
5. SRI hashes for CDN resources + pin Mermaid version [MEDIUM]
Added integrity= + crossorigin= to all three PrismJS CDN tags.
Pinned Mermaid from floating @10 to @10.9.3 with SRI hash.
Tests: 224 passed, 0 failed.
- Use 100dvh with 100vh fallback to fix composer being cut off on
mobile browsers where the address bar affects viewport height
- Add comprehensive @media(max-width:640px) rules: topbar wrapping,
compact messages, full-width msg-body, smaller chips and buttons,
responsive composer, approval cards, tool cards, settings modal
- Use font-size:16px on textarea to prevent iOS/Android auto-zoom
on input focus (browsers zoom when font-size < 16px)
- Add .topbar-left class on title wrapper for responsive stacking