diff --git a/CHANGELOG.md b/CHANGELOG.md index 5136596..8c41f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,111 @@ # Hermes Web UI -- Changelog +## [v0.50.47] fix/feat: batch fixes — root workspace, custom providers, cron cache, system theme + +Synthesized from PRs #506, #507, #508, #509, #510, #514, #515, #519, #521. + +### Fixes + +**Allow /root as a workspace path** (PRs #510, #521 by @ccqqlo) +Removes `/root` from `_BLOCKED_SYSTEM_ROOTS` in `api/workspace.py`, so +deployments running as root (Docker, VPS) can set `/root` as their workspace +without a "system directory" rejection. + +**Guard against split on missing [Attached files:]** (PR #521 by @ccqqlo) +`base_text` extraction in `api/streaming.py` now guards: `msg_text.split(...)[0] +if ... in msg_text else msg_text`. Previously split on the empty case returned +an empty string, causing attachment-matching to silently fail on messages with +no attachments. + +**custom_providers models visible regardless of active provider** (#515, #519 by @shruggr, @cloudyun888) +`get_available_models()` in `api/config.py` no longer discards the 'custom' +provider from `detected_providers` when the user has `custom_providers` entries +in `config.yaml`. Previously, switching active_provider away from 'custom' +hid all custom model definitions from the picker. + +**Cron skill picker cache invalidated on form open and skill save** (PRs #507, #508 by @armorbreak001) +`toggleCronForm()` now unconditionally nulls `_cronSkillsCache` before fetching, +so skills created in the same session appear immediately. `submitSkillSave()` also +nulls `_cronSkillsCache` after a successful write, mirroring the existing +`_skillsData = null` pattern. Fixes #502. + +### Features + +**System (auto) theme following OS prefers-color-scheme** (#504 / PRs #506, #509, #514 by @armorbreak001, @cloudyun888) +New "System (auto)" option in the theme picker follows the OS dark/light preference +via `window.matchMedia`. Changes: +- `static/boot.js`: `_applyTheme(name)` helper resolves 'system' via matchMedia, + sets `data-theme`, and registers a MQ change listener for live OS tracking. + `loadSettings()` calls `_applyTheme()` instead of direct assignment. +- `static/index.html`: flicker-prevention script resolves 'system' before first + paint. Adds "System (auto)" as first theme option. onchange calls `_applyTheme()`. +- `static/commands.js`: adds 'system' to valid `/theme` names. +- `static/panels.js`: `_settingsThemeOnOpen` reads from localStorage (preserves + 'system' string). `_revertSettingsPreview` calls `_applyTheme()`. +- `static/i18n.js`: cmd_theme description lists 'system' first in all 5 locales. + +### Tests + +22 new tests in `tests/test_batch_fixes.py`. + +Total tests: 1268 (was 1246) + + +## [v0.50.46] feat: clarify dialog flow and refresh recovery (#520) + +Adds a full clarify dialog UX for interactive agent questions — modeled after +the approval card but for free-form clarification prompts. + +### Backend + +New `api/clarify.py` module with a per-session pending queue backed by +`threading.Event` unblocking, gateway notify callbacks, duplicate deduplication +while unresolved, and resolve/clear helpers. + +Three new HTTP endpoints in `api/routes.py`: +- `GET /api/clarify/pending` — poll for pending clarify prompt +- `POST /api/clarify/respond` — resolve the pending prompt +- `GET /api/clarify/inject_test` — loopback-only, for automated tests + +`api/streaming.py` wires `clarify_callback` into `AIAgent.run_conversation()`. +Emits `clarify` SSE events; blocks the tool flow until the user responds, times +out (120s), or the stream is cancelled. Also adds a 409 guard on `chat/start` so +page-refresh races return the active stream id instead of starting a duplicate. + +### Frontend + +`static/messages.js`: clarify card with numbered choices, Other button, and +free-text input. Composer is locked while clarify is active. DOM self-heals if +the card node is removed during a rerender. SSE `clarify` event listener plus +1.5s fallback polling. Session switch and reconnect start/stop clarify polling. +409 conflict flow reattaches to the active stream and queues the user message. +`CLARIFY_MIN_VISIBLE_MS = 30000` timer dedup mirrors the approval card pattern. + +`static/ui.js`: `lockComposerForClarify()` / `unlockComposerForClarify()` with +saved-state restore. `updateSendBtn()` respects the disabled state. + +`static/sessions.js`: `loadSession()` starts/stops clarify polling on switch +and inflight reattach. + +`static/index.html` / `static/style.css`: clarify card markup with ARIA roles +and full responsive/mobile styles. + +`static/i18n.js`: 6 new keys in all 5 locales (en, es, de, zh-Hans, zh-Hant). + +### Tests + +- `tests/test_clarify_unblock.py`: 14 new tests covering queue resolution, + notify callbacks, clear-on-cancel, and all three HTTP endpoints. +- `tests/test_sprint30.py`: 31 new clarify tests (HTML markup, CSS classes, + i18n keys, messages.js functions, streaming registration flags). +- `tests/test_sprint36.py`: expand search window for `setBusy` check after + additional `stopClarifyPolling()` calls push it past the old 800-char limit. + +Total tests: 1246 (was 1209) + +Co-authored-by: franksong2702 + + ## [v0.50.45] fix: suppress N/A source_tag in session list (#429) Feishu and WeChat sessions (and any session with an unrecognised or legacy diff --git a/api/config.py b/api/config.py index 55df1af..39101d6 100644 --- a/api/config.py +++ b/api/config.py @@ -949,8 +949,11 @@ def get_available_models() -> dict: # THAT provider, not to a separate "Custom" group. hermes_cli reports # 'custom' as authenticated whenever base_url is set, which would otherwise # build a phantom "Custom" bucket next to the real provider's group. Drop - # it unless the user explicitly chose 'custom' as their active provider. - if active_provider and active_provider != "custom": + # it unless (a) the user explicitly chose 'custom' as their active provider, + # or (b) the user has custom_providers entries in config.yaml (those models + # were already added above and should still be shown). + _has_custom_providers = isinstance(_custom_providers_cfg, list) and len(_custom_providers_cfg) > 0 + if active_provider and active_provider != "custom" and not _has_custom_providers: detected_providers.discard("custom") # 5. Build model groups diff --git a/api/streaming.py b/api/streaming.py index 8aa14f4..23189a8 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -595,7 +595,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta if m.get('role') == 'user': content = str(m.get('content', '')) # Match if content is part of the sent message or vice-versa - base_text = msg_text.split('\n\n[Attached files:')[0].strip() + base_text = msg_text.split('\n\n[Attached files:')[0].strip() if '\n\n[Attached files:' in msg_text else msg_text if base_text[:60] in content or content[:60] in msg_text: m['attachments'] = attachments break diff --git a/api/workspace.py b/api/workspace.py index 7096882..070b0c8 100644 --- a/api/workspace.py +++ b/api/workspace.py @@ -243,7 +243,7 @@ def resolve_trusted_workspace(path: str | Path | None = None) -> Path: _BLOCKED_SYSTEM_ROOTS = { # Linux / macOS Path('/etc'), Path('/usr'), Path('/var'), Path('/bin'), Path('/sbin'), - Path('/boot'), Path('/proc'), Path('/sys'), Path('/dev'), Path('/root'), + Path('/boot'), Path('/proc'), Path('/sys'), Path('/dev'), Path('/lib'), Path('/lib64'), Path('/opt/homebrew'), } diff --git a/static/boot.js b/static/boot.js index 1983b9b..54fc472 100644 --- a/static/boot.js +++ b/static/boot.js @@ -568,6 +568,21 @@ window.addEventListener('resize',()=>{ }; })(); +// ── System theme helper ────────────────────────────────────────────────────── +function _applyTheme(name){ + const resolved=(name==='system') + ?(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light') + :name; + document.documentElement.dataset.theme=resolved||'dark'; + // Re-register OS change listener whenever system theme is active + if(name==='system'){ + const mq=window.matchMedia('(prefers-color-scheme:dark)'); + const _onOsChange=()=>{ document.documentElement.dataset.theme=mq.matches?'dark':'light'; }; + mq.removeEventListener('change',_onOsChange); + mq.addEventListener('change',_onOsChange); + } +} + function applyBotName(){ const name=window._botName||'Hermes'; document.title=name; @@ -594,8 +609,8 @@ function applyBotName(){ window._notificationsEnabled=!!s.notifications_enabled; window._botName=s.bot_name||'Hermes'; const _theme=s.theme||'dark'; - document.documentElement.dataset.theme=_theme; localStorage.setItem('hermes-theme',_theme); + _applyTheme(_theme); document.body.classList.toggle('bubble-layout',!!s.bubble_layout); if(typeof setLocale==='function'){ const _lang=typeof resolvePreferredLocale==='function' diff --git a/static/commands.js b/static/commands.js index 75e4484..d6c38ac 100644 --- a/static/commands.js +++ b/static/commands.js @@ -121,14 +121,14 @@ async function cmdUsage(){ } async function cmdTheme(args){ - const themes=['dark','light','slate','solarized','monokai','nord','oled']; + const themes=['system','dark','light','slate','solarized','monokai','nord','oled']; if(!args||!themes.includes(args.toLowerCase())){ showToast(t('theme_usage')+themes.join('|')); return; } const themeName=args.toLowerCase(); - document.documentElement.dataset.theme=themeName; localStorage.setItem('hermes-theme',themeName); + _applyTheme(themeName); try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:themeName})});}catch(e){} // Update settings dropdown if panel is open const sel=$('settingsTheme'); diff --git a/static/i18n.js b/static/i18n.js index bca1e54..5172981 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -66,7 +66,7 @@ const LOCALES = { cmd_workspace: 'Switch workspace by name', cmd_new: 'Start a new chat session', cmd_usage: 'Toggle token usage display on/off', - cmd_theme: 'Switch theme (dark/light/slate/solarized/monokai/nord/oled)', + cmd_theme: 'Switch theme (system/dark/light/slate/solarized/monokai/nord/oled)', cmd_personality: 'Switch agent personality', cmd_skills: 'List available Hermes skills', available_commands: 'Available commands:', @@ -480,7 +480,7 @@ const LOCALES = { cmd_workspace: 'Cambiar de espacio de trabajo por nombre', cmd_new: 'Iniciar una nueva sesión de chat', cmd_usage: 'Activar o desactivar el uso de tokens', - cmd_theme: 'Cambiar tema (dark/light/slate/solarized/monokai/nord/oled)', + cmd_theme: 'Cambiar tema (system/dark/light/slate/solarized/monokai/nord/oled)', cmd_personality: 'Cambiar la personalidad del agente', cmd_skills: 'Listar las skills de Hermes disponibles', available_commands: 'Comandos disponibles:', @@ -884,7 +884,7 @@ const LOCALES = { cmd_workspace: 'Workspace nach Namen wechseln', cmd_new: 'Neue Chat-Sitzung starten', cmd_usage: 'Token-Verbrauchsanzeige umschalten', - cmd_theme: 'Theme wechseln (dark/light/slate/solarized/monokai/nord/oled)', + cmd_theme: 'Theme wechseln (system/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:', @@ -1096,7 +1096,7 @@ const LOCALES = { cmd_workspace: '\u6309\u540d\u79f0\u5207\u6362\u5de5\u4f5c\u533a', cmd_new: '\u65b0\u5efa\u804a\u5929\u4f1a\u8bdd', cmd_usage: '\u5207\u6362 token \u7528\u91cf\u663e\u793a', - cmd_theme: '\u5207\u6362\u4e3b\u9898\uff08dark/light/slate/solarized/monokai/nord/oled\uff09', + cmd_theme: '\u5207\u6362\u4e3b\u9898\uff08system/dark/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', @@ -1499,7 +1499,7 @@ const LOCALES = { cmd_workspace: '\u6309\u540d\u7a31\u5207\u63db\u5de5\u4f5c\u5340', cmd_new: '\u65b0\u5efa\u804a\u5929\u6703\u8a71', cmd_usage: '\u5207\u63db token \u7528\u91cf\u986f\u793a', - cmd_theme: '\u5207\u63db\u4e3b\u984c\uff08dark/light/slate/solarized/monokai/nord/oled\uff09', + cmd_theme: '\u5207\u63db\u4e3b\u984c\uff08system/dark/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', diff --git a/static/index.html b/static/index.html index 0297783..0ba9065 100644 --- a/static/index.html +++ b/static/index.html @@ -4,7 +4,7 @@