From 44a544362feb5d206d535b933b5308d9ff333174 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 15 Apr 2026 07:45:20 +0000 Subject: [PATCH] feat: add System (auto) theme following OS prefers-color-scheme (#504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synthesized from PRs #506, #509, #514 (all by armorbreak001 and cloudyun888). Implementation: - static/index.html: flicker-prevention head script resolves 'system' to 'dark'/'light' via matchMedia before first paint. Adds 'System (auto)' as first option in theme picker. onchange calls _applyTheme(). - static/boot.js: new _applyTheme(name) helper — resolves 'system' via matchMedia, sets data-theme, registers a MQ change listener so the UI tracks OS switches live. loadSettings() now calls _applyTheme() instead of direct data-theme assignment. - static/commands.js: adds 'system' to valid /theme command names, delegates apply to _applyTheme(). - static/panels.js: _settingsThemeOnOpen reads from localStorage (preserves 'system' string, not the resolved 'dark'/'light'). _revertSettingsPreview calls _applyTheme() so reverting to 'system' correctly re-enables OS tracking. - static/i18n.js: cmd_theme description now lists 'system' first in all 5 locales (en, es, de, zh-Hans, zh-Hant). Design choices vs submitted PRs: - No separate system-theme.js file (unnecessary indirection). - matchMedia listener does NOT POST to /api/settings (OS can change rapidly; persisting on every OS switch would hammer the server). Co-authored-by: armorbreak001 Co-authored-by: cloudyun888 --- static/boot.js | 17 ++++++++++++++++- static/commands.js | 4 ++-- static/i18n.js | 10 +++++----- static/index.html | 5 +++-- static/panels.js | 5 +++-- 5 files changed, 29 insertions(+), 12 deletions(-) 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 e4a35c5..2ada45b 100644 --- a/static/index.html +++ b/static/index.html @@ -4,7 +4,7 @@ Hermes - + @@ -475,7 +475,8 @@
- + diff --git a/static/panels.js b/static/panels.js index d509fb7..201ad6e 100644 --- a/static/panels.js +++ b/static/panels.js @@ -1112,7 +1112,7 @@ function toggleSettings(){ if(!overlay) return; if(overlay.style.display==='none'){ _settingsDirty = false; - _settingsThemeOnOpen = document.documentElement.dataset.theme || 'dark'; + _settingsThemeOnOpen = localStorage.getItem('hermes-theme') || document.documentElement.dataset.theme || 'dark'; _settingsSection = 'conversation'; overlay.style.display=''; loadSettingsPanel(); @@ -1150,8 +1150,9 @@ function _closeSettingsPanel(){ // Revert live DOM/localStorage to what they were when the panel opened function _revertSettingsPreview(){ if(_settingsThemeOnOpen){ - document.documentElement.dataset.theme = _settingsThemeOnOpen; localStorage.setItem('hermes-theme', _settingsThemeOnOpen); + if(typeof _applyTheme==='function') _applyTheme(_settingsThemeOnOpen); + else document.documentElement.dataset.theme = _settingsThemeOnOpen; } }