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-10 01:44:04 +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.42.0< / div > < / div > < / div >
2026-03-31 03:40:19 +00:00
< div class = "sidebar-nav" >
2026-04-10 01:35:23 +00:00
< button class = "nav-tab active" data-panel = "chat" data-label = "Chat" onclick = "switchPanel('chat')" title = "Chat" data-i18n-title = "tab_chat" > 💬 < / button >
< button class = "nav-tab" data-panel = "tasks" data-label = "Tasks" onclick = "switchPanel('tasks')" title = "Tasks" data-i18n-title = "tab_tasks" > 📅 < / button >
< button class = "nav-tab" data-panel = "skills" data-label = "Skills" onclick = "switchPanel('skills')" title = "Skills" data-i18n-title = "tab_skills" > 🧩 < / button >
< button class = "nav-tab" data-panel = "memory" data-label = "Memory" onclick = "switchPanel('memory')" title = "Memory" data-i18n-title = "tab_memory" > 🧠 < / button >
< button class = "nav-tab" data-panel = "workspaces" data-label = "Spaces" onclick = "switchPanel('workspaces')" title = "Spaces" data-i18n-title = "tab_workspaces" > 📁 < / button >
< button class = "nav-tab" data-panel = "profiles" data-label = "Profiles" onclick = "switchPanel('profiles')" title = "Agent profiles" data-i18n-title = "tab_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 >
< button class = "nav-tab" data-panel = "todos" data-label = "Todos" onclick = "switchPanel('todos')" title = "Current task list" data-i18n-title = "tab_todos" > ✅ < / button >
2026-03-31 03:40:19 +00:00
< / 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 >
2026-04-10 01:35:23 +00:00
< span data-i18n = "new_conversation" > New conversation< / span > < span style = "font-size:10px;opacity:.5;margin-left:4px" > ⌘ K< / span >
2026-03-31 03:40:19 +00:00
< / button >
< / div >
2026-04-10 01:35:23 +00:00
< div class = "session-search" > < input id = "sessionSearch" placeholder = "Filter conversations..." data-i18n-placeholder = "filter_conversations" oninput = "filterSessions()" > < / div >
2026-03-31 03:40:19 +00:00
< 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" >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted)" data-i18n = "scheduled_jobs" > Scheduled jobs< / div >
< button class = "cron-btn run" style = "padding:3px 8px;font-size:10px" onclick = "toggleCronForm()" data-i18n = "new_job" > + New job< / button >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-10 01:35:23 +00:00
< button class = "cron-btn run" style = "flex:1" onclick = "submitCronCreate()" data-i18n = "create_job" > Create job< / button >
< button class = "cron-btn" style = "flex:1" onclick = "toggleCronForm()" data-i18n = "cancel" > Cancel< / button >
2026-03-31 03:40:19 +00:00
< / div >
< div id = "cronFormError" style = "font-size:11px;color:var(--accent);margin-top:6px;display:none" > < / div >
< / div >
2026-04-10 01:35:23 +00:00
< div class = "cron-list" id = "cronList" > < div style = "padding:12px;color:var(--muted);font-size:12px" data-i18n = "loading" > Loading...< / div > < / div >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-10 01:35:23 +00:00
< div class = "skills-search" style = "flex:1;padding:0" > < input id = "skillsSearch" placeholder = "Search skills..." data-i18n-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()" data-i18n = "new_skill" > + New skill< / button >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-10 01:35:23 +00:00
< button class = "cron-btn run" style = "flex:1" onclick = "submitSkillSave()" data-i18n = "save_skill" > Save skill< / button >
< button class = "cron-btn" style = "flex:1" onclick = "toggleSkillForm()" data-i18n = "cancel" > Cancel< / button >
2026-03-31 03:40:19 +00:00
< / div >
< div id = "skillFormError" style = "font-size:11px;color:var(--accent);margin-top:6px;display:none" > < / div >
< / div >
2026-04-10 01:35:23 +00:00
< div class = "skills-list" id = "skillsList" > < div style = "padding:12px;color:var(--muted);font-size:12px" data-i18n = "loading" > Loading...< / div > < / div >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-10 01:35:23 +00:00
< span style = "font-size:11px;color:var(--muted)" data-i18n = "personal_memory" > Personal memory< / span >
< button class = "cron-btn run" id = "memEditBtn" style = "padding:3px 8px;font-size:10px" onclick = "toggleMemoryEdit()" > ✎ < span data-i18n = "edit" > Edit< / span > < / button >
2026-03-31 03:40:19 +00:00
< / div >
2026-04-10 01:35:23 +00:00
< div class = "memory-panel" id = "memoryPanel" > < div style = "color:var(--muted);font-size:12px" data-i18n = "loading" > Loading...< / div > < / div >
2026-03-31 03:40:19 +00:00
<!-- Memory edit form (hidden by default) -->
< div id = "memoryEditForm" style = "display:none;padding:8px 12px;border-top:1px solid var(--border);flex-shrink:0" >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-bottom:4px" > < span data-i18n = "editing" > Editing< / span > : < span id = "memEditSection" > memory< / span > < / div >
2026-03-31 03:40:19 +00:00
< 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" >
2026-04-10 01:35:23 +00:00
< button class = "cron-btn run" style = "flex:1" onclick = "submitMemorySave()" data-i18n = "save" > Save< / button >
< button class = "cron-btn" style = "flex:1" onclick = "closeMemoryEdit()" data-i18n = "cancel" > Cancel< / button >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-10 01:35:23 +00:00
< div style = "padding:10px 12px 4px;font-size:11px;color:var(--muted);flex-shrink:0" data-i18n = "current_task_list" > Current task list< / div >
2026-03-31 03:40:19 +00:00
< div id = "todoPanel" style = "flex:1;overflow-y:auto;padding:8px 12px" > < / div >
< / div >
<!-- Workspaces panel -->
< div class = "panel-view" id = "panelWorkspaces" >
2026-04-10 01:35:23 +00:00
< div style = "padding:10px 12px 4px;font-size:11px;color:var(--muted)" data-i18n = "workspace_desc" > 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" data-i18n = "loading" > Loading...< / div > < / div >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted)" data-i18n = "tab_profiles" > Agent profiles< / div >
< button class = "cron-btn run" style = "padding:3px 8px;font-size:10px" onclick = "toggleProfileForm()" data-i18n = "new_profile" > + New profile< / 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
< / 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" >
2026-04-10 01:35:23 +00:00
< button class = "sm-btn" id = "btnDownload" title = "Download as Markdown" data-i18n-title = "download_transcript" > ↓ < span data-i18n = "transcript" > Transcript< / span > < / button >
2026-03-31 03:40:19 +00:00
< button class = "sm-btn" id = "btnExportJSON" title = "Export full session as JSON" > ❬ /❭ JSON< / button >
2026-04-10 01:35:23 +00:00
< button class = "sm-btn" id = "btnImportJSON" title = "Import session from JSON" > ↑ < span data-i18n = "import" > Import< / span > < / 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
< 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-10 01:35:23 +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" data-i18n = "new_conversation" > 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-04-10 01:35:23 +00:00
< button class = "chip clear-btn" id = "btnClearConv" onclick = "clearConversation()" title = "Clear all messages in this conversation" style = "display:none" > 🗑 < span data-i18n = "copy" > Clear< / span > < / button >
< button class = "chip gear-btn" id = "btnSettings" onclick = "toggleSettings()" title = "Settings" data-i18n-title = "settings_title" > ⚙ < / 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 >
2026-04-10 01:35:23 +00:00
< h2 data-i18n = "empty_title" > What can I help with?< / h2 >
< p data-i18n = "empty_subtitle" > Ask anything, run commands, explore files, or manage your scheduled tasks.< / p >
2026-03-31 03:40:19 +00:00
< div class = "suggestion-grid" >
2026-04-10 01:35:23 +00:00
< button class = "suggestion" data-msg = "What files are in this workspace?" data-i18n = "suggest_files" > 📁 What files are in this workspace?< / button >
< button class = "suggestion" data-msg = "What's on my schedule today?" data-i18n = "suggest_schedule" > 📋 What's on my schedule today?< / button >
< button class = "suggestion" data-msg = "Help me plan a small project." data-i18n = "suggest_plan" > 🗺 Help me plan a small project.< / button >
2026-03-31 03:40:19 +00:00
< / 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 >
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
< div class = "update-banner" id = "updateBanner" >
< span id = "updateMsg" > < / span >
< div style = "display:flex;gap:8px;flex-shrink:0" >
< button class = "update-btn" onclick = "dismissUpdate()" > Later< / button >
< button class = "update-btn update-primary" id = "btnApplyUpdate" onclick = "applyUpdates()" > Update Now< / button >
< / div >
< / div >
2026-03-31 03:40:19 +00:00
< 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 >
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
< div class = "approval-card" id = "approvalCard" role = "alertdialog" aria-labelledby = "approvalHeading" aria-describedby = "approvalDesc" >
2026-03-31 03:40:19 +00:00
< 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 >
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
< span id = "approvalHeading" data-i18n = "approval_heading" > Approval required< / span >
2026-03-31 03:40:19 +00:00
< / div >
< div class = "approval-desc" id = "approvalDesc" > < / div >
< div class = "approval-cmd" id = "approvalCmd" > < / div >
< div class = "approval-btns" >
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
< button class = "approval-btn once" id = "approvalBtnOnce" onclick = "respondApproval('once')" title = "Allow this one command (Enter)" data-i18n-title = "approval_btn_once_title" >
< span class = "approval-btn-icon" > ✓ < / span >
< span class = "approval-btn-label" data-i18n = "approval_btn_once" > Allow once< / span >
< kbd class = "approval-kbd" > ↵< / kbd >
< / button >
< button class = "approval-btn session" id = "approvalBtnSession" onclick = "respondApproval('session')" title = "Allow for this session" >
< span class = "approval-btn-icon" > 🔒 < / span >
< span class = "approval-btn-label" data-i18n = "approval_btn_session" > Allow session< / span >
< / button >
< button class = "approval-btn always" id = "approvalBtnAlways" onclick = "respondApproval('always')" title = "Always allow this command pattern" >
< span class = "approval-btn-icon" > ☆ < / span >
< span class = "approval-btn-label" data-i18n = "approval_btn_always" > Always allow< / span >
< / button >
< button class = "approval-btn deny" id = "approvalBtnDeny" onclick = "respondApproval('deny')" title = "Deny — do not run this command" >
< span class = "approval-btn-icon" > ✕ < / span >
< span class = "approval-btn-label" data-i18n = "approval_btn_deny" > Deny< / span >
< / button >
2026-03-31 03:40:19 +00:00
< / 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" >
2026-04-08 15:04:42 +00:00
< h3 style = "margin:0;font-size:16px" data-i18n = "settings_title" > Settings< / h3 >
2026-04-05 04:00:24 +00:00
< button class = "panel-icon-btn" onclick = "_closeSettingsPanel()" title = "Close" > ✕ < / 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 class = "settings-body" >
< div class = "settings-field" >
2026-04-08 15:04:42 +00:00
< label for = "settingsModel" data-i18n = "settings_label_model" > Default Model< / label >
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
< 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 >
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" >
2026-04-08 15:04:42 +00:00
< label for = "settingsSendKey" data-i18n = "settings_label_send_key" > Send Key< / label >
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
< 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" >
2026-04-08 15:04:42 +00:00
< label for = "settingsTheme" data-i18n = "settings_label_theme" > Theme< / label >
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
< 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 >
2026-04-05 04:13:01 +00:00
< option value = "light" > Light< / option >
2026-04-05 04:00:24 +00:00
< option value = "slate" > Slate (charcoal)< / option >
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
< option value = "solarized" > Solarized Dark< / option >
< option value = "monokai" > Monokai< / option >
< option value = "nord" > Nord< / option >
2026-04-07 17:56:57 +00:00
< option value = "oled" > OLED< / option >
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
< / select >
< / div >
2026-04-08 14:55:03 +00:00
< div class = "settings-field" >
2026-04-08 15:04:42 +00:00
< label for = "settingsLanguage" data-i18n = "settings_label_language" > Language< / label >
2026-04-08 14:55:03 +00:00
< select id = "settingsLanguage" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px" > < / select >
< / div >
2026-04-08 16:02:02 +00:00
< div class = "settings-field" >
< label style = "display:flex;align-items:center;gap:8px;cursor:pointer" >
< input type = "checkbox" id = "settingsSoundEnabled" style = "width:15px;height:15px;accent-color:var(--accent)" >
2026-04-10 01:35:23 +00:00
< span data-i18n = "settings_label_sound" > Notification sound< / span >
2026-04-08 16:02:02 +00:00
< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-top:4px" data-i18n = "settings_desc_sound" > Play a sound when the assistant finishes a response.< / div >
2026-04-08 16:02:02 +00:00
< / div >
< div class = "settings-field" >
< label style = "display:flex;align-items:center;gap:8px;cursor:pointer" >
< input type = "checkbox" id = "settingsNotificationsEnabled" style = "width:15px;height:15px;accent-color:var(--accent)" >
2026-04-10 01:35:23 +00:00
< span data-i18n = "settings_label_notifications" > Browser notifications< / span >
2026-04-08 16:02:02 +00:00
< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-top:4px" data-i18n = "settings_desc_notifications" > Show a system notification when a response completes while the tab is in the background.< / div >
2026-04-08 16:02:02 +00:00
< / 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)" >
2026-04-08 15:04:42 +00:00
< span data-i18n = "settings_label_token_usage" > Show token usage after responses< / span >
2026-04-04 02:04:41 +00:00
< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-top:4px" data-i18n = "settings_desc_token_usage" > Displays input/output token count below each assistant reply. Also toggled with < code > /usage< / code > .< / div >
2026-04-04 02:04:41 +00:00
< / 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)" >
2026-04-08 15:04:42 +00:00
< span data-i18n = "settings_label_cli_sessions" > Show CLI sessions in sidebar< / span >
2026-04-04 04:06:23 +00:00
< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-top:4px" data-i18n = "settings_desc_cli_sessions" > Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.< / div >
2026-04-04 04:06:23 +00:00
< / 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)" >
2026-04-08 15:04:42 +00:00
< span data-i18n = "settings_label_sync_insights" > Sync usage to /insights< / span >
2026-04-05 03:07:05 +00:00
< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-top:4px" data-i18n = "settings_desc_sync_insights" > Mirrors WebUI token usage to state.db so < code > hermes /insights< / code > includes browser session data. Off by default.< / div >
2026-04-05 03:07:05 +00:00
< / div >
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
< div class = "settings-field" >
< label style = "display:flex;align-items:center;gap:8px;cursor:pointer" >
< input type = "checkbox" id = "settingsCheckUpdates" style = "width:15px;height:15px;accent-color:var(--accent)" >
2026-04-08 15:04:42 +00:00
< span data-i18n = "settings_label_check_updates" > Check for updates< / span >
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-top:4px" data-i18n = "settings_desc_check_updates" > Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.< / div >
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
< / div >
2026-04-06 05:10:59 +00:00
< div class = "settings-field" >
2026-04-08 15:04:42 +00:00
< label for = "settingsBotName" data-i18n = "settings_label_bot_name" > Assistant Name< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-bottom:6px" data-i18n = "settings_desc_bot_name" > Display name for the assistant throughout the UI. Defaults to Hermes.< / div >
2026-04-06 15:06:16 +00:00
< input type = "text" id = "settingsBotName" placeholder = "Hermes" maxlength = "64" style = "width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px" >
2026-04-06 05:10:59 +00:00
< / 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" >
2026-04-08 15:04:42 +00:00
< label for = "settingsPassword" data-i18n = "settings_label_password" > Access Password< / label >
2026-04-10 01:35:23 +00:00
< div style = "font-size:11px;color:var(--muted);margin-bottom:6px" data-i18n = "settings_desc_password" > 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…" data-i18n-placeholder = "password_placeholder" 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 >
2026-04-08 15:04:42 +00:00
< button class = "sm-btn" onclick = "saveSettings()" style = "margin-top:12px;width:100%;padding:8px;font-weight:600" data-i18n = "settings_save_btn" > Save Settings< / button >
2026-04-10 01:35:23 +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" data-i18n = "disable_auth" > Disable Auth< / button >
< 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" data-i18n = "sign_out" > 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 >
2026-04-10 01:35:23 +00:00
< span data-i18n = "tab_chat" > Chat< / span >
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 >
< 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 >
2026-04-10 01:35:23 +00:00
< span data-i18n = "tab_tasks" > Tasks< / span >
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 >
< 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 >
2026-04-10 01:35:23 +00:00
< span data-i18n = "tab_skills" > Skills< / span >
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 >
< 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 >
2026-04-10 01:35:23 +00:00
< span data-i18n = "tab_memory" > Memory< / span >
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 >
< 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 >
2026-04-10 01:35:23 +00:00
< span data-i18n = "tab_workspaces" > Spaces< / span >
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 >
< / nav >
2026-03-31 03:40:19 +00:00
< div class = "toast" id = "toast" > < / div >
2026-04-08 14:55:03 +00:00
< script src = "/static/i18n.js" > < / script >
2026-03-31 03:40:19 +00:00
< 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 >