From b86ace6ce3b5ad5677eeb17bbe2be2f44fcfef54 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 11 Apr 2026 12:19:12 -0700 Subject: [PATCH] v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: custom provider with slash model name no longer rerouted to OpenRouter (#255) When base_url is configured in config.yaml, resolve_model_provider() now trusts the configured provider/base_url entirely and skips the slash-based OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom being silently routed to OpenRouter, resulting in 401 errors. Fixes #230 * test: mobile layout regression suite — 14 tests for every QA run (#254) Adds tests/test_mobile_layout.py with 14 static regression tests that run on every QA pass to catch mobile layout breakage before it reaches prod. Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, 100dvh, 44px touch targets, 16px font-size on textarea. * feat: /skills slash command lists and filters available Hermes skills (#257) Adds /skills [query] command to commands.js. Fetches from /api/skills, groups by category (alphabetically sorted), displays as a formatted assistant message. Optional query filters by name, description, or category. i18n keys added for en, de, zh, zh-Hant. 1 regression test added. Fixes #248 * feat: shared app dialogs replace native confirm()/prompt() calls (#251) Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt call sites across panels.js, sessions.js, ui.js, workspace.js. Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore, ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant. 5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of native dialog calls. Extracted from PR #242. * fix: Android Chrome mobile — workspace panel close + profile dropdown (#256) Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when toggling the right workspace panel. New closeMobileFiles() helper closes the panel with correct overlay state tracking. Overlay onclick calls both closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x) added to workspace panel header. Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at max-width:900px, escaping the overflow-x:auto stacking context that was clipping it on Android Chrome. Fix applied during review: closeMobileSidebar() now checks if the right panel is still open before hiding the overlay, preventing the overlay from disappearing when only the sidebar is closed. Fixes #247 Fixes #246 * feat: session ⋯ action dropdown replaces per-row buttons (#252) Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash) with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full keyboard (Escape), click-outside, scroll, and resize-reposition handling. Position:fixed prevents sidebar clipping. 5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete (danger style). Each with icon and descriptive subtitle. Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons asserts the new trigger and menu functions exist, old per-row classes are gone. Extracted from PR #242. * docs: v0.47.0 release notes, bump version, update test counts (645) --------- Co-authored-by: Nathan Esquenazi --- CHANGELOG.md | 19 +++ ROADMAP.md | 3 +- TESTING.md | 6 +- api/config.py | 5 + static/boot.js | 29 ++++- static/commands.js | 44 +++++++ static/i18n.js | 36 ++++++ static/index.html | 21 ++- static/panels.js | 15 ++- static/sessions.js | 241 ++++++++++++++++++++++++++--------- static/style.css | 59 +++++++-- static/ui.js | 149 +++++++++++++++++++++- static/workspace.js | 2 +- tests/test_mobile_layout.py | 177 +++++++++++++++++++++++++ tests/test_model_resolver.py | 43 +++++++ tests/test_regressions.py | 21 +++ tests/test_sprint16.py | 20 ++- tests/test_sprint33.py | 59 +++++++++ 18 files changed, 855 insertions(+), 94 deletions(-) create mode 100644 tests/test_mobile_layout.py create mode 100644 tests/test_sprint33.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d9e4c..0bc9ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ --- +## [v0.47.0] — 2026-04-11 + +### Features +- **`/skills [query]` slash command** (PR #257): Fetches from `/api/skills`, groups results by category (alphabetically), renders as a formatted assistant message. Optional query filters by name, description, or category. Shows in the `/` autocomplete dropdown. i18n for en/de/zh/zh-Hant. 1 regression test added. +- **Shared app dialogs replace native `confirm()`/`prompt()`** (PR #251, extracted from #242 by @aronprins): `showConfirmDialog()` and `showPromptDialog()` in `ui.js`, backed by `#appDialogOverlay`. Replaces all 11 native browser dialog call sites across panels.js, sessions.js, ui.js, workspace.js. Full keyboard focus trap (Tab/Escape/Enter), ARIA roles, danger mode, focus restore, mobile-responsive buttons. i18n for en/de/zh/zh-Hant. 5 new tests in `test_sprint33.py`. +- **Session `⋯` action dropdown** (PR #252, extracted from #242 by @aronprins): Replaces 5 per-row hover buttons (pin/move/archive/duplicate/delete) with a single `⋯` trigger. Menu uses `position:fixed` to avoid sidebar clipping. Full close handling: click-outside, scroll, Escape, resize-reposition. `test_sprint16.py` updated to assert the new trigger exists and old button classes are gone. + +### Bug Fixes +- **Custom provider with slash model name no longer rerouted to OpenRouter** (PR #255): `resolve_model_provider()` now returns immediately with the configured `provider`/`base_url` when `base_url` is set, before the slash-based OpenRouter heuristic runs. Fixes `google/gemma-4-26b-a4b` with `provider: custom` being silently routed to OpenRouter (401 errors). 1 regression test added. Fixes #230. +- **Android Chrome: workspace panel now closeable on mobile** (PR #256): `toggleMobileFiles()` now shows/hides the mobile overlay. New `closeMobileFiles()` helper closes the right panel with correct overlay tracking. Overlay tap-to-close calls both `closeMobileSidebar()` and `closeMobileFiles()`. Mobile-only `×` close button added to workspace panel header. Fix applied during review: `closeMobileSidebar()` now checks if the right panel is still open before hiding the overlay. Fixes #247. +- **Android Chrome: profile dropdown no longer clipped on mobile** (PR #256): `.profile-dropdown` switches to `position:fixed; top:56px; right:8px` at `max-width:900px`, escaping the `overflow-x:auto` stacking context that was making it invisible. Fixes #246. + +### Tests +- **Mobile layout regression suite** (PR #254): 14 static tests in `tests/test_mobile_layout.py` that run on every QA pass. Covers: CSS breakpoints at 900px/640px, right panel slide-over, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, `100dvh`, 44px touch targets, 16px textarea font. All pass against current and future master. + +**645 tests (up from 624 on v0.46.0 — +21 new tests)** + +--- + ## [v0.46.0] — 2026-04-11 ### Features diff --git a/ROADMAP.md b/ROADMAP.md index 0c5e014..5b909ce 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ > Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI. > Everything you can do from the CLI terminal, you can do from this UI. > -> Last updated: v0.46.0 (April 11, 2026) — 624 tests, 624 passing +> Last updated: v0.47.0 (April 11, 2026) — 645 tests, 645 passing > Tests: 604 total (604 passing, 0 failures) > Source: / @@ -43,6 +43,7 @@ | v0.44.0 patch | Fix batch: approval card, login CSP, update diagnostics, Lucide icons | PRs #221 #225 #226 #227 #228 | 579 | | v0.45.0 | Custom endpoint in new profile form | Base URL + API key fields; server-side URL validation; config.yaml merge; 9 new tests (PR #233, fixes #170) | 604 | | v0.46.0 | Security, Docker UID/GID, model discovery, i18n, cancel fix | Credential redaction in API responses (PR #243); Docker UID/GID matching (PR #237); custom model API key discovery (PR #238); HTML entity decode + zh/zh-Hant i18n (PR #239); cancel interrupts agent (PR #244); +20 tests | 624 | +| v0.47.0 | Dialogs, session menu, skills command, mobile fixes, mobile QA | Shared app dialogs (#251); session ⋯ menu (#252); mobile QA suite (#254); custom provider slash routing fix (#255); Android Chrome mobile fixes (#256); /skills command (#257); +21 tests | 645 | | 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 | | v0.34 | Sprint 26 — Pluggable themes | Dark, Light, Slate, Solarized, Monokai, Nord; settings unsaved-changes guard; /theme command | 433 | diff --git a/TESTING.md b/TESTING.md index 2874f3b..063da93 100644 --- a/TESTING.md +++ b/TESTING.md @@ -8,7 +8,7 @@ > Prerequisites: SSH tunnel is active on port 8786. Open http://localhost:8786 in browser. > Server health check: curl http://127.0.0.1:8786/health should return {"status":"ok"}. > -> Automated tests: 624 total (624 passing, 0 skipped, 0 known failures) +> Automated tests: 645 total (645 passing, 0 skipped, 0 known failures) > Run: `pytest tests/ -v --timeout=60` --- @@ -1708,8 +1708,8 @@ Each has automated API-level tests in `tests/test_sprint{N}.py`. --- -*Last updated: v0.46.0, April 11, 2026* -*Total automated tests: 624 (624 passing, 0 failures)* +*Last updated: v0.47.0, April 11, 2026* +*Total automated tests: 645 (645 passing, 0 failures)* *Regression gate: tests/test_regressions.py* *Run: pytest tests/ -v --timeout=60* *Source: /* diff --git a/api/config.py b/api/config.py index 874dcf2..9f63f83 100644 --- a/api/config.py +++ b/api/config.py @@ -448,6 +448,11 @@ def resolve_model_provider(model_id: str) -> tuple: # e.g. config=anthropic, model=anthropic/claude-... → bare name to anthropic API if config_provider and prefix == config_provider: return bare, config_provider, config_base_url + # If a custom endpoint base_url is configured, don't reroute through OpenRouter + # just because the model name contains a slash (e.g. google/gemma-4-26b-a4b). + # The user has explicitly pointed at a base_url, so trust their routing config. + if config_base_url: + return model_id, config_provider, config_base_url # If prefix does NOT match config provider, the user picked a cross-provider model # from the OpenRouter dropdown (e.g. config=anthropic but picked openai/gpt-5.4-mini). # In this case always route through openrouter with the full provider/model string. diff --git a/static/boot.js b/static/boot.js index 04c0dfe..ab075d2 100644 --- a/static/boot.js +++ b/static/boot.js @@ -21,12 +21,37 @@ function closeMobileSidebar(){ const sidebar=document.querySelector('.sidebar'); const overlay=$('mobileOverlay'); if(sidebar)sidebar.classList.remove('mobile-open'); - if(overlay)overlay.classList.remove('visible'); + // only hide overlay if right panel is also closed + const panel=document.querySelector('.rightpanel'); + if(!panel||!panel.classList.contains('mobile-open')){ + if(overlay)overlay.classList.remove('visible'); + } } function toggleMobileFiles(){ const panel=document.querySelector('.rightpanel'); + const overlay=$('mobileOverlay'); if(!panel)return; - panel.classList.toggle('mobile-open'); + if(panel.classList.contains('mobile-open')){ + panel.classList.remove('mobile-open'); + // only hide overlay if left sidebar is also closed + const sidebar=document.querySelector('.sidebar'); + if(!sidebar||!sidebar.classList.contains('mobile-open')){ + if(overlay)overlay.classList.remove('visible'); + } + } else { + panel.classList.add('mobile-open'); + if(overlay)overlay.classList.add('visible'); + } +} +function closeMobileFiles(){ + const panel=document.querySelector('.rightpanel'); + const overlay=$('mobileOverlay'); + if(panel)panel.classList.remove('mobile-open'); + // only hide overlay if left sidebar is also closed + const sidebar=document.querySelector('.sidebar'); + if(!sidebar||!sidebar.classList.contains('mobile-open')){ + if(overlay)overlay.classList.remove('visible'); + } } function mobileSwitchPanel(name){ // Switch the panel content view diff --git a/static/commands.js b/static/commands.js index cd7a00e..5231be1 100644 --- a/static/commands.js +++ b/static/commands.js @@ -12,6 +12,7 @@ const COMMANDS=[ {name:'usage', desc:t('cmd_usage'), fn:cmdUsage}, {name:'theme', desc:t('cmd_theme'), fn:cmdTheme, arg:'name'}, {name:'personality', desc:t('cmd_personality'), fn:cmdPersonality, arg:'name'}, + {name:'skills', desc:t('cmd_skills'), fn:cmdSkills, arg:'query'}, ]; function parseCommand(text){ @@ -140,6 +141,49 @@ async function cmdTheme(args){ showToast(t('theme_set')+themeName); } +async function cmdSkills(args){ + try{ + const data = await api('/api/skills'); + let skills = data.skills || []; + if(args){ + const q = args.toLowerCase(); + skills = skills.filter(s => + (s.name||'').toLowerCase().includes(q) || + (s.description||'').toLowerCase().includes(q) || + (s.category||'').toLowerCase().includes(q) + ); + } + if(!skills.length){ + const msg = {role:'assistant', content: args ? `No skills matching "${args}".` : 'No skills found.'}; + S.messages.push(msg); renderMessages(); return; + } + // Group by category + const byCategory = {}; + skills.forEach(s => { + const cat = s.category || 'General'; + if(!byCategory[cat]) byCategory[cat] = []; + byCategory[cat].push(s); + }); + const lines = []; + for(const [cat, items] of Object.entries(byCategory).sort()){ + lines.push(`**${cat}**`); + items.forEach(s => { + const desc = s.description ? ` — ${s.description.slice(0,80)}${s.description.length>80?'...':''}` : ''; + lines.push(` \`${s.name}\`${desc}`); + }); + lines.push(''); + } + const header = args + ? `Skills matching "${args}" (${skills.length}):\n\n` + : `Available skills (${skills.length}):\n\n`; + S.messages.push({role:'assistant', content: header + lines.join('\n')}); + renderMessages(); + showToast(t('type_slash')); + }catch(e){ + showToast('Failed to load skills: '+e.message); + } +} + async function cmdPersonality(args){ if(!S.session){showToast(t('no_active_session'));return;} if(!args){ diff --git a/static/i18n.js b/static/i18n.js index 0292d88..a7bc0d1 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -58,6 +58,7 @@ const LOCALES = { cmd_usage: 'Toggle token usage display on/off', cmd_theme: 'Switch theme (dark/light/slate/solarized/monokai/nord/oled)', cmd_personality: 'Switch agent personality', + cmd_skills: 'List available Hermes skills', available_commands: 'Available commands:', type_slash: 'Type / to see commands', conversation_cleared: 'Conversation cleared', @@ -136,6 +137,14 @@ const LOCALES = { login_btn: 'Sign in', login_invalid_pw: 'Invalid password', login_conn_failed: 'Connection failed', + dialog_confirm_title: 'Confirm action', + dialog_prompt_title: 'Enter a value', + dialog_confirm_btn: 'Confirm', + discard: 'Discard', + clear: 'Clear', + create: 'Create', + remove: 'Remove', + project_name_prompt: 'Project name:', // Sidebar & Tabs tab_chat: 'Chat', tab_tasks: 'Tasks', @@ -238,6 +247,7 @@ const LOCALES = { cmd_usage: 'Token-Verbrauchsanzeige umschalten', cmd_theme: 'Theme wechseln (dark/light/slate/solarized/monokai/nord/oled)', cmd_personality: 'Agenten-Persönlichkeit wechseln', + cmd_skills: 'Verfügbare Hermes-Skills auflisten', available_commands: 'Verfügbare Befehle:', type_slash: 'Tippe / für Befehle', conversation_cleared: 'Konversation gelöscht', @@ -316,6 +326,14 @@ const LOCALES = { login_btn: 'Anmelden', login_invalid_pw: 'Ungültiges Passwort', login_conn_failed: 'Verbindung fehlgeschlagen', + dialog_confirm_title: 'Aktion bestätigen', + dialog_prompt_title: 'Wert eingeben', + dialog_confirm_btn: 'Bestätigen', + discard: 'Verwerfen', + clear: 'Leeren', + create: 'Erstellen', + remove: 'Entfernen', + project_name_prompt: 'Projektname:', // Sidebar & Tabs tab_chat: 'Chat', tab_tasks: 'Aufgaben', @@ -418,6 +436,7 @@ const LOCALES = { cmd_usage: '\u5207\u6362 token \u7528\u91cf\u663e\u793a', cmd_theme: '\u5207\u6362\u4e3b\u9898\uff08dark/light/slate/solarized/monokai/nord/oled\uff09', cmd_personality: '\u5207\u6362 Agent \u4eba\u8bbe', + cmd_skills: '\u5217\u51fa\u53ef\u7528\u7684 Hermes \u6280\u80fd', available_commands: '\u53ef\u7528\u547d\u4ee4\uff1a', type_slash: '\u8f93\u5165 / \u53ef\u67e5\u770b\u547d\u4ee4', conversation_cleared: '\u5bf9\u8bdd\u5df2\u6e05\u7a7a', @@ -496,6 +515,14 @@ const LOCALES = { login_btn: '\u767b\u5f55', login_invalid_pw: '\u5bc6\u7801\u9519\u8bef', login_conn_failed: '\u8fde\u63a5\u5931\u8d25', + dialog_confirm_title: '确认操作', + dialog_prompt_title: '输入内容', + dialog_confirm_btn: '确认', + discard: '放弃', + clear: '清空', + create: '创建', + remove: '移除', + project_name_prompt: '项目名称:', // missing keys from English tab_chat: '\u804a\u5929', tab_memory: '\u8a18\u61b6', @@ -596,6 +623,7 @@ const LOCALES = { cmd_usage: '\u5207\u63db token \u7528\u91cf\u986f\u793a', cmd_theme: '\u5207\u63db\u4e3b\u984c\uff08dark/light/slate/solarized/monokai/nord/oled\uff09', cmd_personality: '\u5207\u63db Agent \u4eba\u8a2d', + cmd_skills: '\u5217\u51fa\u53ef\u7528\u7684 Hermes \u6280\u80fd', available_commands: '\u53ef\u7528\u547d\u4ee4\uff1a', type_slash: '\u8f38\u5165 / \u53ef\u67e5\u770b\u547d\u4ee4', conversation_cleared: '\u5c0d\u8a71\u5df2\u6e05\u7a7a', @@ -675,6 +703,14 @@ const LOCALES = { login_invalid_pw: '\u5bc6\u78bc\u932f\u8aa4', login_conn_failed: '\u9023\u63a5\u5931\u6557', // missing keys from English + dialog_confirm_title: '確認操作', + dialog_prompt_title: '輸入內容', + dialog_confirm_btn: '確認', + discard: '放棄', + clear: '清空', + create: '建立', + remove: '移除', + project_name_prompt: '專案名稱:', tab_chat: '\u804a\u5929', tab_memory: '\u8a18\u61b6', tab_skills: '\u6280\u80fd', diff --git a/static/index.html b/static/index.html index 9b5b86d..80c852c 100644 --- a/static/index.html +++ b/static/index.html @@ -14,7 +14,7 @@
@@ -438,7 +439,7 @@ -
+