committed by
Nathan Esquenazi
parent
b394efce17
commit
db392bd532
@@ -63,7 +63,7 @@ actions. The topbar remains focused on conversation context and the workspace/fi
|
||||
panels.js Cron, skills, memory, workspace, profiles, todo, settings (~974 lines)
|
||||
commands.js Slash command registry, parser, autocomplete dropdown (~156 lines)
|
||||
onboarding.js First-run wizard overlay, provider setup flow, and settings/workspace orchestration.
|
||||
boot.js Event wiring, mobile nav, voice input, boot IIFE (~338 lines)
|
||||
boot.js Event wiring, mobile sidebar/workspace nav, voice input, boot IIFE (~338 lines)
|
||||
tests/
|
||||
conftest.py Isolated test server (port 8788, separate HERMES_HOME) (~240 lines)
|
||||
test_sprint{1-20b}.py Feature tests per sprint (21 files, 415 test functions)
|
||||
|
||||
10
README.md
10
README.md
@@ -277,8 +277,8 @@ WireGuard. Install it on your server and your phone, and they join the same
|
||||
private network -- no port forwarding, no SSH tunnels, no public exposure.
|
||||
|
||||
The Hermes Web UI is fully responsive with a mobile-optimized layout
|
||||
(hamburger sidebar, bottom navigation bar, touch-friendly controls), so it
|
||||
works well as a daily-driver agent interface from your phone.
|
||||
(hamburger sidebar, sidebar top tabs in the drawer, touch-friendly controls),
|
||||
so it works well as a daily-driver agent interface from your phone.
|
||||
|
||||
**Setup:**
|
||||
|
||||
@@ -451,10 +451,10 @@ across 53 test files.
|
||||
|
||||
### Mobile responsive
|
||||
- Hamburger sidebar -- slide-in overlay on mobile (<640px)
|
||||
- Bottom navigation bar -- 5-tab iOS-style fixed bar
|
||||
- Sidebar top tabs stay available on mobile; no fixed bottom nav stealing chat height
|
||||
- Files slide-over panel from right edge
|
||||
- Touch targets minimum 44px on all interactive elements
|
||||
- Composer positioned above bottom nav
|
||||
- Full-height chat/composer on phones without bottom-nav spacing
|
||||
- Desktop layout completely unchanged
|
||||
|
||||
---
|
||||
@@ -542,7 +542,7 @@ A run of focused quality-of-life improvements: terminal tool approval prompts th
|
||||
Added the 7th built-in theme: pure black backgrounds with warm accents tuned to reduce burn-in risk. Small diff, big impact for anyone on an OLED display.
|
||||
|
||||
**[@Bobby9228](https://github.com/Bobby9228)** — Mobile Profiles button + Android Chrome fixes (PRs #253, #263, #265)
|
||||
Added the Profiles tab to the mobile bottom navigation bar, making profile switching reachable on phones, plus a set of Android Chrome-specific fixes for the profile dropdown.
|
||||
Added the Profiles entry to the mobile navigation flow, making profile switching reachable on phones, plus a set of Android Chrome-specific fixes for the profile dropdown.
|
||||
|
||||
**[@franksong2702](https://github.com/franksong2702)** — Session title guard + breadcrumb nav (PRs #301, #302)
|
||||
Two clean bug fixes / features: the session title guard that stops `title_from()` from overwriting user-renamed sessions after every turn, and clickable breadcrumb navigation in the workspace file preview panel.
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
| Sprint 18 | Thinking display + workspace tree | File preview auto-close, thinking/reasoning cards, expandable directory tree (#22) | 318 |
|
||||
| Sprint 19 | Auth + security hardening | Password auth (off by default), login page, security headers, 20MB body limit (#23) | 328 |
|
||||
| Sprint 20 | Voice input + send button | Voice input (Web Speech API), send button icon-circle with pop-in animation | 415 |
|
||||
| Sprint 21 | Mobile responsive + Docker | Hamburger sidebar, bottom nav, files slide-over, Docker support (#21, #7) | 415 |
|
||||
| Sprint 21 | Mobile responsive + Docker | Hamburger sidebar, mobile nav, files slide-over, Docker support (#21, #7) | 415 |
|
||||
| Sprint 22 | Multi-profile support | Profile picker, management panel, seamless switching, per-session tracking (#28) | 415 |
|
||||
| Sprint 23 | Agentic transparency | Token/cost display, subagent cards, skill picker in cron, skill linked files, workspace tree persistence, timestamp fixes | 424 |
|
||||
| v0.44.0 patch | Fix batch: approval card, login CSP, update diagnostics, Lucide icons | PRs #221 #225 #226 #227 #228 | 579 |
|
||||
@@ -50,7 +50,7 @@
|
||||
| v0.48.0 | Gateway session sync | Real-time Telegram/Discord/Slack sessions in sidebar via SSE + DB polling (#274 @bergeouss); +10 tests | 658 |
|
||||
| v0.48.1 | Table inline formatting | `inlineMd()` in table cells — **bold**, *italic*, `code`, links render correctly (PR #278); 0 new tests | 658 |
|
||||
| v0.48.2 | Provider mismatch warning | Toast warning + auth_mismatch error type for provider/model mismatches (#283, fixes #266); +21 tests | 679 |
|
||||
| v0.49.1 | Docker docs + mobile Profiles button | Two-container Docker compose (#291/#288); Profiles button in mobile bottom nav with mobileSwitchPanel, data-panel, correct SVG size and position (#297/#265 @gabogabucho); +3 tests | 700 |
|
||||
| v0.49.1 | Docker docs + mobile Profiles button | Two-container Docker compose (#291/#288); Profiles added to the mobile navigation flow with correct panel wiring and SVG sizing (#297/#265 @gabogabucho); +3 tests | 700 |
|
||||
| v0.49.0 | First-run onboarding wizard + self-update hardening | One-shot bootstrap + guided setup wizard; provider config persisted to config.yaml + .env; OpenRouter/Anthropic/OpenAI/Custom; wizard hidden after completion (#285); self-update stderr/split-ref/conflict fixes (#287); skip flaky redaction test (#289); +18 tests | 697 |
|
||||
| v0.32 | Auto-compaction handling | Compression detection, /compact command, real context window indicator | 424 |
|
||||
| v0.33 | /insights sync | Opt-in state.db sync so `hermes /insights` includes WebUI sessions | 424 |
|
||||
@@ -223,7 +223,7 @@
|
||||
- [x] Voice input via Web Speech API (Sprint 20)
|
||||
|
||||
### Mobile
|
||||
- [x] Mobile responsive layout — hamburger sidebar, bottom nav, files slide-over (Sprint 21)
|
||||
- [x] Mobile responsive layout — hamburger sidebar, sidebar tabs on phones, files slide-over (Sprint 21 + later mobile nav simplification)
|
||||
|
||||
### Profiles
|
||||
- [x] Multi-profile support — create, switch, delete profiles (Sprint 22, Issue #28)
|
||||
|
||||
@@ -1715,12 +1715,13 @@ Each has automated API-level tests in `tests/test_sprint{N}.py`.
|
||||
- Open on mobile viewport (<640px): hamburger icon visible in topbar.
|
||||
- Tap hamburger → sidebar slides in from left with backdrop overlay.
|
||||
- Tap outside sidebar → closes. Tap a session → closes and loads session.
|
||||
- Bottom navigation bar: 5 tabs (Chat, Tasks, Skills, Memory, Spaces).
|
||||
- Tap "Tasks" in bottom nav → sidebar opens showing Tasks panel.
|
||||
- Tap "Chat" in bottom nav → sidebar closes (chat is in main area).
|
||||
- Sidebar top nav remains visible inside the mobile drawer; includes Chat/Tasks/Skills/Memory/Spaces/Profile tabs.
|
||||
- Tap "Tasks" in the drawer nav → Tasks panel opens in the sidebar drawer.
|
||||
- Tap "Chat" in the drawer nav → sidebar closes and chat is unobstructed in the main area.
|
||||
- Files button in topbar → right panel slides in from right.
|
||||
- No fixed mobile bottom nav; chat transcript and composer use the reclaimed vertical space.
|
||||
- All touch targets are at least 44px (session items, buttons, icons).
|
||||
- Desktop viewport (>640px): no hamburger, no bottom nav, no mobile elements.
|
||||
- Desktop viewport (>640px): no hamburger or mobile overlay; desktop layout unchanged.
|
||||
- Docker: `docker compose up -d` starts server on port 8787.
|
||||
- Docker: session data persists across container restarts (named volume).
|
||||
|
||||
|
||||
@@ -155,11 +155,7 @@ function toggleWorkspacePanel(force){
|
||||
openWorkspacePanel(nextMode);
|
||||
}
|
||||
function mobileSwitchPanel(name){
|
||||
// Switch the panel content view
|
||||
switchPanel(name);
|
||||
// For non-chat panels (tasks, skills, memory, spaces), open the sidebar
|
||||
// so the panel is visible. For 'chat', the content is in the main area —
|
||||
// just close the sidebar so the chat view is unobstructed.
|
||||
if(name==='chat'){
|
||||
closeMobileSidebar();
|
||||
} else {
|
||||
@@ -170,10 +166,6 @@ function mobileSwitchPanel(name){
|
||||
if(overlay)overlay.classList.add('visible');
|
||||
}
|
||||
}
|
||||
// Update bottom nav active state
|
||||
document.querySelectorAll('.mobile-nav-btn').forEach(btn=>{
|
||||
btn.classList.toggle('active',btn.dataset.panel===name);
|
||||
});
|
||||
}
|
||||
|
||||
$('btnSend').onclick=()=>{
|
||||
|
||||
@@ -553,32 +553,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-overlay" id="mobileOverlay" onclick="closeMobileSidebar()"></div>
|
||||
<nav class="mobile-bottom-nav" id="mobileBottomNav">
|
||||
<button class="mobile-nav-btn active" data-panel="chat" onclick="mobileSwitchPanel('chat')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
<span data-i18n="tab_chat">Chat</span>
|
||||
</button>
|
||||
<button class="mobile-nav-btn" data-panel="tasks" onclick="mobileSwitchPanel('tasks')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
<span data-i18n="tab_tasks">Tasks</span>
|
||||
</button>
|
||||
<button class="mobile-nav-btn" data-panel="skills" onclick="mobileSwitchPanel('skills')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
<span data-i18n="tab_skills">Skills</span>
|
||||
</button>
|
||||
<button class="mobile-nav-btn" data-panel="memory" onclick="mobileSwitchPanel('memory')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.2 6H8.2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z"/><line x1="9" y1="17" x2="15" y2="17"/><line x1="10" y1="20" x2="14" y2="20"/></svg>
|
||||
<span data-i18n="tab_memory">Memory</span>
|
||||
</button>
|
||||
<button class="mobile-nav-btn" data-panel="workspaces" onclick="mobileSwitchPanel('workspaces')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h8l2 2h10v14H2z"/></svg>
|
||||
<span data-i18n="tab_workspaces">Spaces</span>
|
||||
</button>
|
||||
<button class="mobile-nav-btn" data-panel="profiles" onclick="mobileSwitchPanel('profiles')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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 data-i18n="tab_profiles">Profiles</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="app-dialog-overlay" id="appDialogOverlay" style="display:none" aria-hidden="true">
|
||||
<div class="app-dialog" id="appDialog" role="dialog" aria-modal="true" aria-labelledby="appDialogTitle" aria-describedby="appDialogDesc">
|
||||
<div class="app-dialog-header">
|
||||
|
||||
@@ -557,7 +557,6 @@
|
||||
.mobile-hamburger{display:none;}
|
||||
.mobile-files-btn{display:none!important;}
|
||||
.mobile-overlay{display:none;}
|
||||
.mobile-bottom-nav{display:none;}
|
||||
|
||||
@media(min-width:901px){
|
||||
.layout.workspace-panel-collapsed .rightpanel{width:0 !important;opacity:0;transform:translateX(14px);border-left-color:transparent;pointer-events:none;}
|
||||
@@ -593,20 +592,6 @@
|
||||
box-shadow:-4px 0 24px rgba(0,0,0,.4);}
|
||||
.rightpanel.mobile-open{right:0;}
|
||||
.rightpanel .resize-handle{display:none;}
|
||||
/* Bottom navigation bar */
|
||||
.mobile-bottom-nav{display:flex;position:fixed;bottom:0;left:0;right:0;
|
||||
background:var(--sidebar);border-top:1px solid var(--border);
|
||||
z-index:150;padding:4px 0 env(safe-area-inset-bottom,0);
|
||||
justify-content:space-around;align-items:center;}
|
||||
.mobile-nav-btn{display:flex;flex-direction:column;align-items:center;gap:2px;
|
||||
background:none;border:none;color:var(--muted);font-size:9px;padding:6px 4px;
|
||||
cursor:pointer;min-width:44px;min-height:44px;justify-content:center;
|
||||
-webkit-tap-highlight-color:transparent;transition:color .15s;}
|
||||
.mobile-nav-btn.active{color:var(--blue);}
|
||||
.mobile-nav-btn:hover{color:var(--text);}
|
||||
.mobile-nav-btn svg{flex-shrink:0;}
|
||||
/* Hide sidebar nav tabs (replaced by bottom nav) */
|
||||
.sidebar-nav{display:none;}
|
||||
/* Keep the Hermes control available at the bottom of the mobile sidebar */
|
||||
.sidebar-bottom{display:block;padding:10px;}
|
||||
/* Topbar adjustments */
|
||||
@@ -620,13 +605,10 @@
|
||||
.settings-tab{flex-shrink:0;}
|
||||
.settings-main{padding:18px 16px;}
|
||||
.hermes-action-grid{grid-template-columns:1fr;}
|
||||
/* Messages area — account for bottom nav */
|
||||
.messages{padding-bottom:60px;}
|
||||
.messages-inner{padding:12px 10px 20px;}
|
||||
.msg-body{padding-left:0;max-width:100%;}
|
||||
.msg-role{font-size:12px;}
|
||||
/* Composer — above bottom nav */
|
||||
.composer-wrap{padding:8px 10px 12px!important;margin-bottom:56px;}
|
||||
.composer-wrap{padding:8px 10px 12px!important;}
|
||||
.composer-box{border-radius:12px;}
|
||||
.composer-box textarea{font-size:16px;min-height:40px;}
|
||||
.composer-footer{padding:6px 8px 8px!important;gap:8px;}
|
||||
|
||||
25
tests/test_issue_code_syntax_highlight.py
Normal file
25
tests/test_issue_code_syntax_highlight.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Regression tests for fenced code block syntax highlighting."""
|
||||
from pathlib import Path
|
||||
|
||||
UI_JS = Path(__file__).resolve().parent.parent / "static" / "ui.js"
|
||||
|
||||
|
||||
def _read_ui_js() -> str:
|
||||
return UI_JS.read_text()
|
||||
|
||||
|
||||
def test_fenced_code_blocks_add_prism_language_class():
|
||||
js = _read_ui_js()
|
||||
assert 'class="language-${esc(normalizedLang)}"' in js, (
|
||||
"Fenced code blocks should add Prism language-* classes so syntax highlighting works"
|
||||
)
|
||||
|
||||
|
||||
def test_fenced_code_blocks_keep_existing_pre_header_layout():
|
||||
js = _read_ui_js()
|
||||
assert 'return `${h}<pre><code${langAttr}>${esc(code.replace(/\\n$/,' in js, (
|
||||
"The syntax-highlight fix should preserve the existing fenced code block layout"
|
||||
)
|
||||
assert '<div class="code-block">' not in js, (
|
||||
"This fix should not introduce a new wrapper around fenced code blocks"
|
||||
)
|
||||
@@ -9,7 +9,7 @@ They are static checks (no server needed) that catch common regressions:
|
||||
- Right panel slide-over markup and CSS intact
|
||||
- Profile dropdown not clipped by overflow on mobile
|
||||
- Composer footer chips scroll correctly on narrow viewports
|
||||
- Mobile bottom nav and overlay markup present
|
||||
- Mobile sidebar navigation stays available on phones
|
||||
- No full-viewport overflow that would break scroll
|
||||
|
||||
Run as part of the standard test suite:
|
||||
@@ -61,12 +61,20 @@ def test_mobile_overlay_present():
|
||||
".mobile-overlay CSS rule missing from style.css"
|
||||
|
||||
|
||||
def test_mobile_bottom_nav_present():
|
||||
"""Mobile bottom navigation bar must be present."""
|
||||
assert "mobile-bottom-nav" in HTML or "mobile-nav-btn" in HTML, \
|
||||
"Mobile bottom nav (.mobile-bottom-nav or .mobile-nav-btn) missing from index.html"
|
||||
assert "mobile-bottom-nav" in CSS, \
|
||||
".mobile-bottom-nav CSS rule missing from style.css"
|
||||
def test_sidebar_nav_present():
|
||||
"""Sidebar top navigation tabs must be present."""
|
||||
assert 'class="sidebar-nav"' in HTML, \
|
||||
".sidebar-nav missing from index.html"
|
||||
assert ".sidebar-nav{" in CSS or ".sidebar-nav {" in CSS, \
|
||||
".sidebar-nav CSS rule missing from style.css"
|
||||
|
||||
|
||||
def test_mobile_does_not_hide_sidebar_nav():
|
||||
"""Phone breakpoint must keep the sidebar top navigation visible."""
|
||||
mobile_block = re.search(r'@media\(max-width:640px\)\{(.*)\n\s*\}', CSS, re.DOTALL)
|
||||
assert mobile_block, "Missing @media(max-width:640px) block in style.css"
|
||||
assert ".sidebar-nav{display:none" not in mobile_block.group(1).replace(" ", ""), \
|
||||
".sidebar-nav must stay visible on mobile"
|
||||
|
||||
|
||||
def test_mobile_files_button_present():
|
||||
@@ -222,34 +230,20 @@ def test_composer_textarea_font_size_mobile():
|
||||
|
||||
|
||||
|
||||
# ── Profiles button in mobile bottom nav ─────────────────────────────────────
|
||||
# ── Sidebar tabs on mobile ───────────────────────────────────────────────────
|
||||
|
||||
def test_mobile_profiles_button_present():
|
||||
"""Mobile bottom nav must include a Profiles button (PR #265)."""
|
||||
assert 'data-panel="profiles"' in HTML and 'mobileSwitchPanel' in HTML, \
|
||||
"Mobile nav must have a Profiles button with data-panel='profiles' and mobileSwitchPanel"
|
||||
def test_profiles_sidebar_tab_present():
|
||||
"""Sidebar tab strip must include Profiles."""
|
||||
assert 'class="nav-tab" data-panel="profiles"' in HTML, \
|
||||
"Sidebar nav must have a Profiles tab"
|
||||
|
||||
|
||||
def test_mobile_profiles_button_uses_mobileSwitchPanel():
|
||||
"""Profiles mobile nav button must use mobileSwitchPanel, not raw switchPanel."""
|
||||
import re
|
||||
match = re.search(
|
||||
r'<button[^>]*mobile-nav-btn[^>]*data-panel="profiles"[^>]*>|'
|
||||
r'<button[^>]*data-panel="profiles"[^>]*mobile-nav-btn[^>]*>',
|
||||
HTML
|
||||
)
|
||||
assert match, "Could not find mobile-nav-btn with data-panel='profiles'"
|
||||
btn_html = HTML[match.start():match.start()+300]
|
||||
assert "mobileSwitchPanel('profiles')" in btn_html, \
|
||||
"Profiles mobile nav button must call mobileSwitchPanel('profiles')"
|
||||
|
||||
|
||||
def test_mobile_profiles_button_is_last_in_nav():
|
||||
"""Profiles button must appear after Spaces in the mobile bottom nav."""
|
||||
spaces_pos = HTML.find('data-panel="workspaces"')
|
||||
profiles_pos = HTML.rfind('data-panel="profiles"')
|
||||
assert spaces_pos > 0 and profiles_pos > spaces_pos, \
|
||||
"Profiles button must appear after Spaces button in the mobile nav"
|
||||
def test_mobile_bottom_nav_removed():
|
||||
"""The old fixed mobile bottom nav should not be present anymore."""
|
||||
assert "mobile-bottom-nav" not in HTML, \
|
||||
"mobile-bottom-nav markup should be removed from index.html"
|
||||
assert "mobile-bottom-nav" not in CSS, \
|
||||
"mobile-bottom-nav CSS should be removed from style.css"
|
||||
|
||||
|
||||
# ── Mobile Enter key inserts newline (PR #315, fixes #269) ───────────────────
|
||||
|
||||
Reference in New Issue
Block a user