From d278563e00457f102924659838642233639b1fb1 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Fri, 3 Apr 2026 10:09:36 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20Sprint=2021=20=E2=80=94=20mobile=20resp?= =?UTF-8?q?onsive=20layout=20+=20Docker=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Dockerfile | 20 +++++++++++++ README.md | 27 +++++++++++++++++ docker-compose.yml | 22 ++++++++++++++ static/boot.js | 37 +++++++++++++++++++++++ static/index.html | 27 +++++++++++++++++ static/sessions.js | 1 + static/style.css | 75 ++++++++++++++++++++++++++++++++++++++-------- 7 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..295d86e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.12-slim + +LABEL maintainer="nesquena" +LABEL description="Hermes Web UI — browser interface for Hermes Agent" + +WORKDIR /app + +# Copy source +COPY . /app + +# Default to binding all interfaces (required for container networking) +ENV HERMES_WEBUI_HOST=0.0.0.0 +ENV HERMES_WEBUI_PORT=8787 + +# State directory (mount as volume for persistence) +ENV HERMES_WEBUI_STATE_DIR=/data + +EXPOSE 8787 + +CMD ["python", "server.py"] diff --git a/README.md b/README.md index 243dfd8..21fc939 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,33 @@ That is it. The script will: --- +## Docker + +Run with Docker Compose (recommended): + +```bash +docker compose up -d +``` + +Or build and run manually: + +```bash +docker build -t hermes-webui . +docker run -d -p 8787:8787 -v ~/.hermes:/root/.hermes:ro hermes-webui +``` + +Open http://localhost:8787 in your browser. + +To enable password protection: + +```bash +docker run -d -p 8787:8787 -e HERMES_WEBUI_PASSWORD=your-secret -v ~/.hermes:/root/.hermes:ro hermes-webui +``` + +Session data persists in a named volume (`hermes-data`) across restarts. + +--- + ## What start.sh discovers automatically | Thing | How it finds it | diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c020fdd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.8" + +services: + hermes-webui: + build: . + ports: + - "8787:8787" + volumes: + # Persist session data, settings, and projects across restarts + - hermes-data:/data + # Mount hermes-agent for full agent features (optional) + - ${HERMES_HOME:-~/.hermes}:/root/.hermes:ro + environment: + - HERMES_WEBUI_HOST=0.0.0.0 + - HERMES_WEBUI_PORT=8787 + - HERMES_WEBUI_STATE_DIR=/data + # Optional: set a password for remote access + # - HERMES_WEBUI_PASSWORD=your-secret-password + restart: unless-stopped + +volumes: + hermes-data: diff --git a/static/boot.js b/static/boot.js index 2aa4b09..c1fcde6 100644 --- a/static/boot.js +++ b/static/boot.js @@ -8,6 +8,43 @@ async function cancelStream(){ }catch(e){setStatus('Cancel failed: '+e.message);} } +// ── Mobile navigation ────────────────────────────────────────────────────── +function toggleMobileSidebar(){ + const sidebar=document.querySelector('.sidebar'); + const overlay=$('mobileOverlay'); + if(!sidebar)return; + const isOpen=sidebar.classList.contains('mobile-open'); + if(isOpen){closeMobileSidebar();} + else{sidebar.classList.add('mobile-open');if(overlay)overlay.classList.add('visible');} +} +function closeMobileSidebar(){ + const sidebar=document.querySelector('.sidebar'); + const overlay=$('mobileOverlay'); + if(sidebar)sidebar.classList.remove('mobile-open'); + if(overlay)overlay.classList.remove('visible'); +} +function toggleMobileFiles(){ + const panel=document.querySelector('.rightpanel'); + if(!panel)return; + panel.classList.toggle('mobile-open'); +} +function mobileSwitchPanel(name){ + // Close sidebar if open, then switch panel + closeMobileSidebar(); + // Open sidebar for the selected panel, then close after a moment + const sidebar=document.querySelector('.sidebar'); + const overlay=$('mobileOverlay'); + if(sidebar){ + sidebar.classList.add('mobile-open'); + if(overlay)overlay.classList.add('visible'); + } + switchPanel(name); + // Update bottom nav active state + document.querySelectorAll('.mobile-nav-btn').forEach(btn=>{ + btn.classList.toggle('active',btn.dataset.panel===name); + }); +} + $('btnSend').onclick=()=>{if(window._micActive)_stopMic();send();}; $('btnAttach').onclick=()=>$('fileInput').click(); diff --git a/static/index.html b/static/index.html index 0a6ab94..af7aa8d 100644 --- a/static/index.html +++ b/static/index.html @@ -143,6 +143,9 @@
+
Hermes
Start a new conversation
GPT-5.4 Mini
@@ -152,6 +155,7 @@
+
@@ -301,6 +305,29 @@
+
+
diff --git a/static/sessions.js b/static/sessions.js index 63dc758..067a996 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -346,6 +346,7 @@ function renderSessionListFromCache(){ _clickTimer=null; if(_renamingSid) return; await loadSession(s.session_id);renderSessionListFromCache(); + if(typeof closeMobileSidebar==='function')closeMobileSidebar(); }, 220); }; el.ondblclick=async(e)=>{ diff --git a/static/style.css b/static/style.css index 0269f41..375f9f1 100644 --- a/static/style.css +++ b/static/style.css @@ -260,38 +260,87 @@ ::-webkit-scrollbar-track{background:transparent} ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:99px;transition:background .2s} ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22)} - @media(max-width:900px){.rightpanel{display:none}} + /* ── Desktop: hide mobile-only elements ── */ + .mobile-hamburger{display:none;} + .mobile-files-btn{display:none!important;} + .mobile-overlay{display:none;} + .mobile-bottom-nav{display:none;} + + @media(max-width:900px){.rightpanel{display:none}.mobile-files-btn{display:inline-flex!important;}} + @media(max-width:640px){ - .sidebar{display:none} - /* Topbar: stack title + chips vertically, allow wrapping */ - .topbar{padding:8px 12px;gap:6px;flex-wrap:wrap;} - .topbar-left{min-width:0;flex:1 1 100%;} + /* ── Sidebar: slide-in overlay instead of hidden ── */ + .sidebar{position:fixed;left:-300px;top:0;bottom:0;width:280px;z-index:200; + transition:left .25s ease;box-shadow:4px 0 24px rgba(0,0,0,.4);} + .sidebar.mobile-open{left:0;} + .sidebar .resize-handle{display:none;} + /* Hamburger button */ + .mobile-hamburger{display:flex;align-items:center;justify-content:center; + background:none;border:none;color:var(--muted);cursor:pointer;padding:4px; + flex-shrink:0;-webkit-tap-highlight-color:transparent;} + .mobile-hamburger:hover{color:var(--text);} + /* Overlay backdrop */ + .mobile-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5); + z-index:199;-webkit-tap-highlight-color:transparent;} + .mobile-overlay.visible{display:block;} + /* Files button in topbar */ + .mobile-files-btn{display:inline-flex!important;} + /* Right panel: slide-over from right */ + .rightpanel{display:flex!important;position:fixed;right:-320px;top:0;bottom:0; + width:300px;z-index:200;transition:right .25s ease; + 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;} + /* Hide sidebar bottom section on mobile (model select, workspace) */ + .sidebar-bottom{display:none;} + /* Topbar adjustments */ + .topbar{padding:8px 12px;gap:8px;} .topbar-title{font-size:14px;} - .topbar-meta{font-size:10px;} - .topbar-chips{flex-wrap:wrap;gap:4px;} - .topbar-chips .chip,.topbar-chips .ws-chip,.topbar-chips button{font-size:11px!important;padding:3px 8px!important;} - /* Messages area */ + .topbar-meta{display:none;} + .topbar-chips{flex-wrap:nowrap;gap:4px;overflow-x:auto;-webkit-overflow-scrolling:touch;} + .topbar-chips .chip,.topbar-chips .ws-chip,.topbar-chips button{font-size:11px!important;padding:3px 8px!important;white-space:nowrap;} + /* 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 */ - .composer-wrap{padding:8px 10px 12px!important;} + /* Composer — above bottom nav */ + .composer-wrap{padding:8px 10px 12px!important;margin-bottom:56px;} .composer-box{border-radius:12px;} .composer-box textarea{font-size:16px;min-height:40px;} .send-btn{width:32px;height:32px;} + /* Touch targets — minimum 44px */ + .icon-btn,.mic-btn{min-width:44px;min-height:44px;} + .session-item{min-height:44px;padding:10px 12px;} /* Empty state */ .empty-state h2{font-size:18px;} .empty-state p{font-size:13px;} .suggestion-grid{max-width:100%!important;} - .suggestion-btn{font-size:12px;padding:8px 10px;} + .suggestion{font-size:12px;padding:10px 12px;} /* Approval card */ .approval-card{padding:0 10px 8px;} .approval-btns{gap:6px;} - .approval-btn{padding:5px 10px;font-size:11px;} + .approval-btn{padding:8px 12px;font-size:12px;min-height:44px;} /* Tool cards */ .tool-card{margin-left:0!important;font-size:12px;} /* Settings modal */ .settings-panel{width:95vw;max-width:95vw;} + /* Login page responsive */ + .card{width:90vw;max-width:320px;padding:28px 24px;} } /* ── Workspace dropdown (topbar) ── */