2026-03-31 03:40:19 +00:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Hermes< / title >
feat: Sprint 26 — pluggable UI themes (dark, light, solarized, monokai, nord)
Five built-in themes with instant switching, persistent preference,
and zero-flicker loading. Custom themes are pure CSS additions.
Theme system:
- CSS variable overrides via :root[data-theme="name"] blocks
- Flicker prevention: inline <script> reads localStorage before
stylesheet parses, preventing dark-flash on light-mode users
- Server-side persistence via settings.json (theme field)
- Boot.js syncs server preference to DOM + localStorage
Built-in themes:
- Dark (default): deep navy/indigo, muted blue accents
- Light: clean white/gray, high contrast, scrollbar overrides
- Solarized Dark: teal background, warm accents
- Monokai: warm dark, green/pink accents
- Nord: arctic blue-gray, calm and minimal
UI integration:
- Settings panel: theme dropdown with instant live preview
- /theme slash command: /theme dark|light|solarized|monokai|nord
- No enum constraint on theme setting — custom themes just work
Documentation:
- THEMES.md: how to switch themes, create custom themes, contribute
8 new tests. All 408 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 03:48:05 +00:00
< script > ( function ( ) { var t = localStorage . getItem ( 'hermes-theme' ) ; if ( t && t !== 'dark' ) document . documentElement . dataset . theme = t ; } ) ( ) < / script >
2026-03-31 03:40:19 +00:00
< link rel = "stylesheet" href = "/static/style.css" >
<!-- Prism.js syntax highlighting (loaded async, non - blocking) -->
2026-04-02 06:46:40 +00:00
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" integrity = "sha384-wFjoQjtV1y5jVHbt0p35Ui8aV8GVpEZkyF99OXWqP/eNJDU93D3Ugxkoyh6Y2I4A" crossorigin = "anonymous" >
< script src = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js" integrity = "sha384-MXybTpajaBV0AkcBaCPT4KIvo0FzoCiWXgcihYsw4FUkEz0Pv3JGV6tk2G8vJtDc" crossorigin = "anonymous" defer > < / script >
< script src = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js" integrity = "sha384-Uq05+JLko69eOiPr39ta9bh7kld5PKZoU+fF7g0EXTAriEollhZ+DrN8Q/Oi8J2Q" crossorigin = "anonymous" defer > < / script >
2026-03-31 03:40:19 +00:00
< / head >
< body >
< div class = "layout" >
< aside class = "sidebar" >
2026-04-05 03:09:59 +00:00
< div class = "sidebar-header" > < div class = "logo" > H< / div > < div > < h1 style = "margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em" > Hermes< / h1 > < div style = "font-size:10px;color:var(--muted);opacity:.8;margin-top:1px" > v0.33< / div > < / div > < / div >
2026-03-31 03:40:19 +00:00
< div class = "sidebar-nav" >
< button class = "nav-tab active" data-panel = "chat" data-label = "Chat" onclick = "switchPanel('chat')" title = "Chat" > 💬 < / button >
< button class = "nav-tab" data-panel = "tasks" data-label = "Tasks" onclick = "switchPanel('tasks')" title = "Tasks" > 📅 < / button >
< button class = "nav-tab" data-panel = "skills" data-label = "Skills" onclick = "switchPanel('skills')" title = "Skills" > 🧩 < / button >
< button class = "nav-tab" data-panel = "memory" data-label = "Memory" onclick = "switchPanel('memory')" title = "Memory" > 🧠 < / button >
< button class = "nav-tab" data-panel = "workspaces" data-label = "Spaces" onclick = "switchPanel('workspaces')" title = "Spaces" > 📁 < / button >
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
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>
2026-04-03 17:50:21 +00:00
< button class = "nav-tab" data-panel = "profiles" data-label = "Profiles" onclick = "switchPanel('profiles')" title = "Agent profiles" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg > < / button >
2026-03-31 03:40:19 +00:00
< button class = "nav-tab" data-panel = "todos" data-label = "Todos" onclick = "switchPanel('todos')" title = "Current task list" > ✅ < / button >
< / div >
<!-- Chat panel -->
< div class = "panel-view active" id = "panelChat" >
< div class = "sidebar-section" >
< button class = "new-chat-btn" id = "btnNewChat" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg >
New conversation < span style = "font-size:10px;opacity:.5;margin-left:4px" > ⌘ K< / span >
< / button >
< / div >
< div class = "session-search" > < input id = "sessionSearch" placeholder = "Filter conversations..." oninput = "filterSessions()" > < / div >
< div class = "session-list" id = "sessionList" > < / div >
< / div >
<!-- Tasks (cron) panel -->
< div class = "panel-view" id = "panelTasks" >
< div class = "sidebar-section" style = "padding-bottom:4px;display:flex;align-items:center;justify-content:space-between" >
< div style = "font-size:11px;color:var(--muted)" > Scheduled jobs< / div >
< button class = "cron-btn run" style = "padding:3px 8px;font-size:10px" onclick = "toggleCronForm()" > + New job< / button >
< / div >
<!-- Create job form (hidden by default) -->
< div id = "cronCreateForm" style = "display:none;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0" >
< input id = "cronFormName" placeholder = "Job name (optional)" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px" >
< input id = "cronFormSchedule" placeholder = "Schedule: '0 9 * * *' or 'every 1h'" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px" >
< textarea id = "cronFormPrompt" rows = "3" placeholder = "Prompt (must be self-contained)" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;resize:none;font-family:inherit;margin-bottom:6px" > < / textarea >
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
< select id = "cronFormDeliver" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px" >
2026-03-31 03:40:19 +00:00
< option value = "local" > Local (save output only)< / option >
< option value = "discord" > Discord< / option >
< option value = "telegram" > Telegram< / option >
< / select >
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
< div class = "skill-picker-wrap" style = "margin-bottom:8px" >
< input id = "cronFormSkillSearch" placeholder = "Add skills (optional)..." style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none" autocomplete = "off" >
< div id = "cronFormSkillDropdown" class = "skill-picker-dropdown" style = "display:none" > < / div >
< div id = "cronFormSkillTags" class = "skill-picker-tags" > < / div >
< / div >
2026-03-31 03:40:19 +00:00
< div style = "display:flex;gap:6px" >
< button class = "cron-btn run" style = "flex:1" onclick = "submitCronCreate()" > Create job< / button >
< button class = "cron-btn" style = "flex:1" onclick = "toggleCronForm()" > Cancel< / button >
< / div >
< div id = "cronFormError" style = "font-size:11px;color:var(--accent);margin-top:6px;display:none" > < / div >
< / div >
< div class = "cron-list" id = "cronList" > < div style = "padding:12px;color:var(--muted);font-size:12px" > Loading...< / div > < / div >
< / div >
<!-- Skills panel -->
< div class = "panel-view" id = "panelSkills" >
< div class = "sidebar-section" style = "padding-bottom:4px;display:flex;align-items:center;justify-content:space-between" >
< div class = "skills-search" style = "flex:1;padding:0" > < input id = "skillsSearch" placeholder = "Search skills..." oninput = "filterSkills()" > < / div >
< button class = "cron-btn run" style = "padding:3px 8px;font-size:10px;flex-shrink:0;margin-left:6px" onclick = "toggleSkillForm()" > + New skill< / button >
< / div >
<!-- Skill create/edit form (hidden by default) -->
< div id = "skillCreateForm" style = "display:none;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0" >
< input id = "skillFormName" placeholder = "Skill name (e.g. my-skill)" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box" >
< input id = "skillFormCategory" placeholder = "Category (optional, e.g. devops)" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box" >
< textarea id = "skillFormContent" rows = "6" placeholder = "SKILL.md content (YAML frontmatter + markdown body)" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;resize:vertical;font-family:'SF Mono',ui-monospace,monospace;margin-bottom:6px;box-sizing:border-box" > < / textarea >
< div style = "display:flex;gap:6px" >
< button class = "cron-btn run" style = "flex:1" onclick = "submitSkillSave()" > Save skill< / button >
< button class = "cron-btn" style = "flex:1" onclick = "toggleSkillForm()" > Cancel< / button >
< / div >
< div id = "skillFormError" style = "font-size:11px;color:var(--accent);margin-top:6px;display:none" > < / div >
< / div >
< div class = "skills-list" id = "skillsList" > < div style = "padding:12px;color:var(--muted);font-size:12px" > Loading...< / div > < / div >
< / div >
<!-- Memory panel -->
< div class = "panel-view" id = "panelMemory" >
< div style = "padding:8px 12px 4px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0" >
< span style = "font-size:11px;color:var(--muted)" > Personal memory< / span >
< button class = "cron-btn run" id = "memEditBtn" style = "padding:3px 8px;font-size:10px" onclick = "toggleMemoryEdit()" > ✎ Edit< / button >
< / div >
< div class = "memory-panel" id = "memoryPanel" > < div style = "color:var(--muted);font-size:12px" > Loading...< / div > < / div >
<!-- Memory edit form (hidden by default) -->
< div id = "memoryEditForm" style = "display:none;padding:8px 12px;border-top:1px solid var(--border);flex-shrink:0" >
< div style = "font-size:11px;color:var(--muted);margin-bottom:4px" > Editing: < span id = "memEditSection" > memory< / span > < / div >
< textarea id = "memEditContent" rows = "10" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:11px;outline:none;resize:vertical;font-family:'SF Mono',ui-monospace,monospace;box-sizing:border-box;margin-bottom:6px;line-height:1.5" > < / textarea >
< div style = "display:flex;gap:6px" >
< button class = "cron-btn run" style = "flex:1" onclick = "submitMemorySave()" > Save< / button >
< button class = "cron-btn" style = "flex:1" onclick = "closeMemoryEdit()" > Cancel< / button >
< / div >
< div id = "memEditError" style = "font-size:11px;color:var(--accent);margin-top:6px;display:none" > < / div >
< / div >
< / div >
<!-- Todo panel -->
< div class = "panel-view" id = "panelTodos" >
< div style = "padding:10px 12px 4px;font-size:11px;color:var(--muted);flex-shrink:0" > Current task list< / div >
< div id = "todoPanel" style = "flex:1;overflow-y:auto;padding:8px 12px" > < / div >
< / div >
<!-- Workspaces panel -->
< div class = "panel-view" id = "panelWorkspaces" >
< div style = "padding:10px 12px 4px;font-size:11px;color:var(--muted)" > Add and switch workspaces for your sessions.< / div >
< div style = "flex:1;overflow-y:auto;padding:0 12px 12px" id = "workspacesPanel" > < div style = "color:var(--muted);font-size:12px" > Loading...< / div > < / div >
< / div >
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
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>
2026-04-03 17:50:21 +00:00
<!-- Profiles panel -->
< div class = "panel-view" id = "panelProfiles" >
< div class = "sidebar-section" style = "padding-bottom:4px;display:flex;align-items:center;justify-content:space-between" >
< div style = "font-size:11px;color:var(--muted)" > Agent profiles< / div >
< button class = "cron-btn run" style = "padding:3px 8px;font-size:10px" onclick = "toggleProfileForm()" > + New profile< / button >
< / div >
<!-- Profile create form (hidden by default) -->
< div id = "profileCreateForm" style = "display:none;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0" >
< input id = "profileFormName" placeholder = "Profile name (lowercase, a-z 0-9 hyphens)" style = "width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box" >
< label style = "display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);margin-bottom:8px;cursor:pointer" >
< input type = "checkbox" id = "profileFormClone" style = "accent-color:var(--accent)" > Clone config from active profile
< / label >
< div style = "display:flex;gap:6px" >
< button class = "cron-btn run" style = "flex:1" onclick = "submitProfileCreate()" > Create< / button >
< button class = "cron-btn" style = "flex:1" onclick = "toggleProfileForm()" > Cancel< / button >
< / div >
< div id = "profileFormError" style = "font-size:11px;color:var(--accent);margin-top:6px;display:none" > < / div >
< / div >
< div style = "flex:1;overflow-y:auto;padding:0 12px 12px" id = "profilesPanel" > < div style = "color:var(--muted);font-size:12px" > Loading...< / div > < / div >
< / div >
2026-03-31 03:40:19 +00:00
< div class = "sidebar-bottom" >
< div class = "field-label" style = "font-size:10px;letter-spacing:.07em;margin-bottom:4px" > MODEL< / div >
< select id = "modelSelect" >
< optgroup label = "OpenAI" >
< option value = "openai/gpt-5.4-mini" > GPT-5.4 Mini< / option >
< option value = "openai/gpt-4o" > GPT-4o< / option >
< option value = "openai/o3" > o3< / option >
< option value = "openai/o4-mini" > o4-mini< / option >
< / optgroup >
< optgroup label = "Anthropic" >
< option value = "anthropic/claude-sonnet-4.6" > Claude Sonnet 4.6< / option >
< option value = "anthropic/claude-sonnet-4-5" > Claude Sonnet 4.5< / option >
< option value = "anthropic/claude-haiku-3-5" > Claude Haiku 3.5< / option >
< / optgroup >
< optgroup label = "Other" >
< option value = "google/gemini-2.5-pro" > Gemini 2.5 Pro< / option >
< option value = "deepseek/deepseek-chat-v3-0324" > DeepSeek V3< / option >
< option value = "meta-llama/llama-4-scout" > Llama 4 Scout< / option >
< / optgroup >
< / select >
2026-04-03 19:38:33 +00:00
< div style = "position:relative" >
< div id = "sidebarWsDisplay" style = "display:flex;align-items:center;gap:7px;padding:0 0 8px;cursor:pointer;border-radius:8px;transition:background .15s" onclick = "toggleWsDropdown()" title = "Switch workspace" >
< span style = "font-size:14px;opacity:.7" > 📁 < / span >
< div style = "min-width:0;flex:1" >
< div style = "font-size:11px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" id = "sidebarWsName" > Workspace< / div >
< div style = "font-size:10px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:1px" id = "sidebarWsPath" > < / div >
< / div >
< span style = "font-size:10px;color:var(--muted);flex-shrink:0" > ▾ < / span >
2026-03-31 03:40:19 +00:00
< / div >
2026-04-03 19:38:33 +00:00
< div class = "ws-dropdown" id = "wsDropdown" > < / div >
2026-03-31 03:40:19 +00:00
< / div >
< div class = "sidebar-actions" >
< button class = "sm-btn" id = "btnDownload" title = "Download as Markdown" > ↓ Transcript< / button >
< button class = "sm-btn" id = "btnExportJSON" title = "Export full session as JSON" > ❬ /❭ JSON< / button >
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
< button class = "sm-btn" id = "btnImportJSON" title = "Import session from JSON" > ↑ Import< / button >
< input type = "file" id = "importFileInput" accept = ".json" style = "display:none" >
2026-03-31 03:40:19 +00:00
< / div >
< / div >
< div class = "resize-handle" id = "sidebarResize" > < / div >
< / aside >
< main class = "main" >
< div class = "topbar" >
feat: Sprint 21 — mobile responsive layout + Docker support
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>
2026-04-03 17:09:36 +00:00
< button class = "mobile-hamburger" id = "btnHamburger" onclick = "toggleMobileSidebar()" title = "Menu" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < line x1 = "3" y1 = "6" x2 = "21" y2 = "6" / > < line x1 = "3" y1 = "12" x2 = "21" y2 = "12" / > < line x1 = "3" y1 = "18" x2 = "21" y2 = "18" / > < / svg >
< / button >
2026-04-02 06:46:40 +00:00
< div style = "flex:1;min-width:0;overflow:hidden" > < div class = "topbar-title" id = "topbarTitle" > Hermes< / div > < div class = "topbar-meta" id = "topbarMeta" > Start a new conversation< / div > < / div >
2026-03-31 03:40:19 +00:00
< div class = "topbar-chips" >
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
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>
2026-04-03 17:50:21 +00:00
< div id = "profileChipWrap" style = "position:relative" >
< div class = "chip profile-chip" id = "profileChip" onclick = "toggleProfileDropdown()" title = "Switch profile" style = "cursor:pointer" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:3px" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg > < span id = "profileChipLabel" > default< / span > ▾ < / div >
< div class = "profile-dropdown" id = "profileDropdown" > < / div >
< / div >
2026-03-31 03:40:19 +00:00
< div class = "chip model" id = "modelChip" > GPT-5.4 Mini< / div >
2026-04-03 19:38:33 +00:00
2026-03-31 03:40:19 +00:00
< button class = "chip clear-btn" id = "btnClearConv" onclick = "clearConversation()" title = "Clear all messages in this conversation" style = "display:none" > 🗑 Clear< / button >
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
< button class = "chip gear-btn" id = "btnSettings" onclick = "toggleSettings()" title = "Settings" > ⚙ < / button >
feat: Sprint 21 — mobile responsive layout + Docker support
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>
2026-04-03 17:09:36 +00:00
< button class = "chip mobile-files-btn" id = "btnMobileFiles" onclick = "toggleMobileFiles()" title = "Files" > 📁 < / button >
2026-03-31 03:40:19 +00:00
< / div >
< / div >
< div class = "messages" id = "messages" >
< div class = "empty-state" id = "emptyState" >
< div class = "empty-logo" > 🦉< / div >
< h2 > What can I help with?< / h2 >
< p > Ask anything, run commands, explore files, or manage your scheduled tasks.< / p >
< div class = "suggestion-grid" >
< button class = "suggestion" data-msg = "What files are in this workspace?" > 📁 What files are in this workspace?< / button >
< button class = "suggestion" data-msg = "What's on my schedule today?" > 📋 What's on my schedule today?< / button >
< button class = "suggestion" data-msg = "Help me plan a small project." > 🗺 Help me plan a small project.< / button >
< / div >
< / div >
< div class = "messages-inner" id = "msgInner" > < / div >
< div id = "liveToolCards" style = "display:none;max-width:800px;margin:0 auto;width:100%;padding:0 24px;" > < / div >
< / div >
< div class = "reconnect-banner" id = "reconnectBanner" >
< span id = "reconnectMsg" > ⚠ A response may have been in progress when you last left. Reload messages?< / span >
< div style = "display:flex;gap:8px;flex-shrink:0" >
< button class = "reconnect-btn" onclick = "dismissReconnect()" > Dismiss< / button >
< button class = "reconnect-btn" onclick = "refreshSession()" > ↻ Reload< / button >
< / div >
< / div >
< div class = "approval-card" id = "approvalCard" >
< div class = "approval-inner" >
< div class = "approval-header" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" / > < line x1 = "12" y1 = "9" x2 = "12" y2 = "13" / > < line x1 = "12" y1 = "17" x2 = "12.01" y2 = "17" / > < / svg >
Dangerous command — approval required
< / div >
< div class = "approval-desc" id = "approvalDesc" > < / div >
< div class = "approval-cmd" id = "approvalCmd" > < / div >
< div class = "approval-btns" >
< button class = "approval-btn once" onclick = "respondApproval('once')" > ✓ Allow once< / button >
< button class = "approval-btn session" onclick = "respondApproval('session')" > 🔒 Allow this session< / button >
< button class = "approval-btn always" onclick = "respondApproval('always')" > ☆ Always allow< / button >
< button class = "approval-btn deny" onclick = "respondApproval('deny')" > ✕ Deny< / button >
< / div >
< / div >
< / div >
<!-- Activity bar: shows tool progress / status above composer (not inside input) -->
< div id = "activityBar" style = "display:none;max-width:800px;margin:0 auto;width:100%;padding:0 24px;" >
< div id = "activityBarInner" style = "display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);font-size:12px;color:var(--muted);animation:fadeIn .15s ease;" >
< span id = "activityIcon" style = "font-size:13px;opacity:.6" > ⚙ < / span >
< span id = "activityText" style = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" > < / span >
< button id = "btnCancel" onclick = "cancelStream()" style = "display:none;background:rgba(233,69,96,.12);border:1px solid rgba(233,69,96,.35);color:#e94560;font-size:11px;font-weight:600;padding:3px 10px;border-radius:6px;cursor:pointer;flex-shrink:0;transition:background .15s" title = "Cancel this task" > ■ Cancel< / button >
< button id = "btnDismissStatus" onclick = "setStatus('')" style = "display:none;background:none;border:none;color:var(--muted);font-size:14px;line-height:1;cursor:pointer;padding:0 2px;opacity:.5;flex-shrink:0" title = "Dismiss" > ✕ < / button >
< span id = "activityDots" style = "display:flex;gap:3px;align-items:center" >
< span style = "width:4px;height:4px;border-radius:50%;background:var(--blue);opacity:.3;animation:pulse 1.4s ease-in-out infinite" > < / span >
< span style = "width:4px;height:4px;border-radius:50%;background:var(--blue);opacity:.3;animation:pulse 1.4s ease-in-out .22s infinite" > < / span >
< span style = "width:4px;height:4px;border-radius:50%;background:var(--blue);opacity:.3;animation:pulse 1.4s ease-in-out .44s infinite" > < / span >
< / span >
< / div >
< / div >
< div class = "composer-wrap" id = "composerWrap" >
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
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>
2026-04-03 00:31:31 +00:00
< div class = "cmd-dropdown" id = "cmdDropdown" > < / div >
2026-03-31 03:40:19 +00:00
< div class = "composer-box" id = "composerBox" >
< div class = "drop-hint" id = "dropHint" >
< svg width = "28" height = "28" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
Drop files to upload to workspace
< / div >
< div class = "attach-tray" id = "attachTray" > < / div >
feat: add voice input mic button via Web Speech API
- 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).
2026-04-03 14:04:03 +00:00
< div class = "mic-status" id = "micStatus" style = "display:none" > < span class = "mic-dot" > < / span > Listening…< / div >
2026-03-31 03:40:19 +00:00
< textarea id = "msg" rows = "1" placeholder = "Message Hermes…" > < / textarea >
< div class = "composer-footer" >
< div class = "composer-left" >
< input type = "file" id = "fileInput" multiple accept = "image/*,text/*,application/pdf,application/json,.md,.py,.js,.ts,.yaml,.yml,.toml,.csv,.sh,.txt,.log,.env" style = "display:none" >
< button class = "icon-btn" id = "btnAttach" title = "Attach files" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" / > < / svg >
< / button >
feat: add voice input mic button via Web Speech API
- 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).
2026-04-03 14:04:03 +00:00
< button class = "icon-btn mic-btn" id = "btnMic" title = "Voice input" style = "display:none" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< rect x = "9" y = "1" width = "6" height = "12" rx = "3" / >
< path d = "M5 10a7 7 0 0 0 14 0" / >
< line x1 = "12" y1 = "19" x2 = "12" y2 = "23" / >
< line x1 = "8" y1 = "23" x2 = "16" y2 = "23" / >
< / svg >
< / button >
2026-03-31 03:40:19 +00:00
< / div >
2026-04-04 21:11:28 +00:00
< div class = "ctx-indicator" id = "ctxIndicator" style = "display:none" title = "Context window usage" >
< span class = "ctx-bar-wrap" > < span class = "ctx-bar" id = "ctxBar" > < / span > < / span >
< span class = "ctx-label" id = "ctxLabel" > < / span >
< / div >
2026-03-31 03:40:19 +00:00
< div class = "composer-right" >
feat: polish send button — hidden until content, icon-circle, pop-in animation
- 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).
2026-04-03 14:10:59 +00:00
< button class = "send-btn" id = "btnSend" title = "Send message" style = "display:none" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < line x1 = "12" y1 = "19" x2 = "12" y2 = "5" / > < polyline points = "5 12 12 5 19 12" / > < / svg >
2026-03-31 03:40:19 +00:00
< / button >
< / div >
< / div >
< div class = "upload-bar-wrap" id = "uploadBarWrap" > < div class = "upload-bar" id = "uploadBar" > < / div > < / div >
< / div >
< / div >
< / main >
< aside class = "rightpanel" >
< div class = "resize-handle" id = "rightpanelResize" > < / div >
< div class = "panel-header" >
< span > Workspace< / span >
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
< span class = "git-badge" id = "gitBadge" style = "display:none" > < / span >
2026-03-31 03:40:19 +00:00
< div class = "panel-actions" >
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
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>
2026-04-03 00:31:31 +00:00
< button class = "panel-icon-btn" id = "btnUpDir" title = "Parent directory" onclick = "navigateUp()" style = "display:none" > ↑ < / button >
2026-03-31 03:40:19 +00:00
< button class = "panel-icon-btn" id = "btnNewFile" title = "New file" onclick = "promptNewFile()" > + < / button >
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
< button class = "panel-icon-btn" id = "btnNewFolder" title = "New folder" onclick = "promptNewFolder()" > 📁 < / button >
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
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>
2026-04-03 00:31:31 +00:00
< button class = "panel-icon-btn" id = "btnRefreshPanel" title = "Refresh" onclick = "if(S.session)loadDir(S.currentDir)" > ↻ < / button >
2026-03-31 03:40:19 +00:00
< button class = "panel-icon-btn close-preview" id = "btnClearPreview" title = "Close preview" > ✕ < / button >
< / div >
< / div >
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
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>
2026-04-03 00:31:31 +00:00
< div class = "breadcrumb-bar" id = "breadcrumbBar" style = "display:none" > < / div >
2026-03-31 03:40:19 +00:00
< div class = "file-tree" id = "fileTree" > < / div >
< div class = "preview-area" id = "previewArea" >
< div class = "preview-path" id = "previewPath" >
< span id = "previewPathText" > < / span >
< span class = "preview-badge" id = "previewBadge" > < / span >
< button id = "btnDownloadFile" class = "panel-icon-btn" style = "margin-left:auto;font-size:12px;width:auto;padding:2px 8px" onclick = "downloadFile(_previewCurrentPath)" title = "Download file to your computer" > ⇩ Download< / button >
< button id = "btnEditFile" class = "panel-icon-btn" style = "font-size:12px;width:auto;padding:2px 8px;display:none" onclick = "toggleEditMode()" > ✎ Edit< / button >
< / div >
< pre class = "preview-code" id = "previewCode" > < / pre >
< div class = "preview-img-wrap" id = "previewImgWrap" style = "display:none" > < img class = "preview-img" id = "previewImg" src = "" alt = "" > < / div >
< div class = "preview-md" id = "previewMd" style = "display:none" > < / div >
< textarea id = "previewEditArea" style = "display:none;flex:1;width:100%;background:var(--code-bg);color:#e2e8f0;border:1px solid var(--border2);border-radius:8px;padding:12px;font-family:'SF Mono',ui-monospace,monospace;font-size:12px;line-height:1.6;resize:none;outline:none" oninput = "_previewDirty=true;updateEditBtn()" > < / textarea >
< / div >
< / aside >
< / div >
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
< div class = "settings-overlay" id = "settingsOverlay" style = "display:none" >
< div class = "settings-panel" >
< div class = "settings-header" >
< h3 style = "margin:0;font-size:16px" > Settings< / h3 >
< button class = "panel-icon-btn" onclick = "toggleSettings()" title = "Close" > ✕ < / button >
< / div >
< div class = "settings-body" >
< div class = "settings-field" >
< label for = "settingsModel" > Default Model< / label >
< select id = "settingsModel" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px" > < / select >
< / div >
< div class = "settings-field" >
< label for = "settingsWorkspace" > Default Workspace< / label >
< select id = "settingsWorkspace" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px" > < / select >
< / div >
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
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>
2026-04-03 00:31:31 +00:00
< div class = "settings-field" >
< label for = "settingsSendKey" > Send Key< / label >
< select id = "settingsSendKey" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px" >
< option value = "enter" > Enter (Shift+Enter for newline)< / option >
< option value = "ctrl+enter" > Ctrl+Enter (Enter for newline)< / option >
< / select >
< / div >
feat: Sprint 26 — pluggable UI themes (dark, light, solarized, monokai, nord)
Five built-in themes with instant switching, persistent preference,
and zero-flicker loading. Custom themes are pure CSS additions.
Theme system:
- CSS variable overrides via :root[data-theme="name"] blocks
- Flicker prevention: inline <script> reads localStorage before
stylesheet parses, preventing dark-flash on light-mode users
- Server-side persistence via settings.json (theme field)
- Boot.js syncs server preference to DOM + localStorage
Built-in themes:
- Dark (default): deep navy/indigo, muted blue accents
- Light: clean white/gray, high contrast, scrollbar overrides
- Solarized Dark: teal background, warm accents
- Monokai: warm dark, green/pink accents
- Nord: arctic blue-gray, calm and minimal
UI integration:
- Settings panel: theme dropdown with instant live preview
- /theme slash command: /theme dark|light|solarized|monokai|nord
- No enum constraint on theme setting — custom themes just work
Documentation:
- THEMES.md: how to switch themes, create custom themes, contribute
8 new tests. All 408 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 03:48:05 +00:00
< div class = "settings-field" >
< label for = "settingsTheme" > Theme< / label >
< select id = "settingsTheme" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px" onchange = "document.documentElement.dataset.theme=this.value;localStorage.setItem('hermes-theme',this.value)" >
< option value = "dark" > Dark (default)< / option >
< option value = "light" > Light< / option >
< option value = "solarized" > Solarized Dark< / option >
< option value = "monokai" > Monokai< / option >
< option value = "nord" > Nord< / option >
< / select >
< / div >
2026-04-04 02:04:41 +00:00
< div class = "settings-field" >
< label style = "display:flex;align-items:center;gap:8px;cursor:pointer" >
< input type = "checkbox" id = "settingsShowTokenUsage" style = "width:15px;height:15px;accent-color:var(--accent)" >
Show token usage after responses
< / label >
< div style = "font-size:11px;color:var(--muted);margin-top:4px" > Displays input/output token count below each assistant reply. Also toggled with < code > /usage< / code > .< / div >
< / div >
2026-04-04 04:06:23 +00:00
< div class = "settings-field" >
< label style = "display:flex;align-items:center;gap:8px;cursor:pointer" >
< input type = "checkbox" id = "settingsShowCliSessions" style = "width:15px;height:15px;accent-color:var(--accent)" >
Show CLI sessions in sidebar
< / label >
< div style = "font-size:11px;color:var(--muted);margin-top:4px" > Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.< / div >
< / div >
2026-04-05 03:07:05 +00:00
< div class = "settings-field" >
< label style = "display:flex;align-items:center;gap:8px;cursor:pointer" >
< input type = "checkbox" id = "settingsSyncInsights" style = "width:15px;height:15px;accent-color:var(--accent)" >
Sync usage to /insights
< / label >
< div style = "font-size:11px;color:var(--muted);margin-top:4px" > Mirrors WebUI token usage to state.db so < code > hermes /insights< / code > includes browser session data. Off by default.< / div >
< / div >
feat: Sprint 19 — password auth, security headers, login page
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>
2026-04-03 12:53:26 +00:00
< div class = "settings-field" style = "border-top:1px solid var(--border);padding-top:12px;margin-top:8px" >
< label for = "settingsPassword" > Access Password< / label >
2026-04-03 13:17:32 +00:00
< div style = "font-size:11px;color:var(--muted);margin-bottom:6px" > Enter a new password to set or change it. Leave blank to keep current setting.< / div >
< input type = "password" id = "settingsPassword" placeholder = "Enter new password…" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px" >
feat: Sprint 19 — password auth, security headers, login page
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>
2026-04-03 12:53:26 +00:00
< / div >
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
< button class = "sm-btn" onclick = "saveSettings()" style = "margin-top:12px;width:100%;padding:8px;font-weight:600" > Save Settings< / button >
2026-04-03 13:17:32 +00:00
< button class = "sm-btn" id = "btnDisableAuth" onclick = "disableAuth()" style = "margin-top:6px;width:100%;padding:8px;font-weight:600;color:#e8a030;border-color:rgba(232,160,48,.3);display:none" > Disable Auth< / button >
feat: Sprint 19 — password auth, security headers, login page
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>
2026-04-03 12:53:26 +00:00
< button class = "sm-btn" id = "btnSignOut" onclick = "signOut()" style = "margin-top:6px;width:100%;padding:8px;font-weight:600;color:var(--accent);border-color:rgba(233,69,96,.3);display:none" > Sign Out< / button >
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
< / div >
< / div >
< / div >
feat: Sprint 21 — mobile responsive layout + Docker support
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>
2026-04-03 17:09:36 +00:00
< div class = "mobile-overlay" id = "mobileOverlay" onclick = "closeMobileSidebar()" > < / div >
< nav class = "mobile-bottom-nav" id = "mobileBottomNav" >
< button class = "mobile-nav-btn active" data-panel = "chat" onclick = "mobileSwitchPanel('chat')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg >
< span > Chat< / span >
< / button >
< button class = "mobile-nav-btn" data-panel = "tasks" onclick = "mobileSwitchPanel('tasks')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < / svg >
< span > Tasks< / span >
< / button >
< button class = "mobile-nav-btn" data-panel = "skills" onclick = "mobileSwitchPanel('skills')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M12 2L2 7l10 5 10-5-10-5z" / > < path d = "M2 17l10 5 10-5" / > < path d = "M2 12l10 5 10-5" / > < / svg >
< span > Skills< / span >
< / button >
< button class = "mobile-nav-btn" data-panel = "memory" onclick = "mobileSwitchPanel('memory')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.2 6H8.2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z" / > < line x1 = "9" y1 = "17" x2 = "15" y2 = "17" / > < line x1 = "10" y1 = "20" x2 = "14" y2 = "20" / > < / svg >
< span > Memory< / span >
< / button >
< button class = "mobile-nav-btn" data-panel = "workspaces" onclick = "mobileSwitchPanel('workspaces')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M2 4h8l2 2h10v14H2z" / > < / svg >
< span > Spaces< / span >
< / button >
< / nav >
2026-03-31 03:40:19 +00:00
< div class = "toast" id = "toast" > < / div >
< script src = "/static/ui.js" > < / script >
< script src = "/static/workspace.js" > < / script >
< script src = "/static/sessions.js" > < / script >
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
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>
2026-04-03 00:31:31 +00:00
< script src = "/static/commands.js" > < / script >
2026-03-31 03:40:19 +00:00
< script src = "/static/messages.js" > < / script >
< script src = "/static/panels.js" > < / script >
< script src = "/static/boot.js" > < / script >
< / body >
< / html >