Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

## [Unreleased]

### Added
- New **Zeus** appearance skin (opt-in, dark-only): OLED-near-black surfaces (`#0F0F0F`–`#181818`) paired with the default gold accent and gold-tinted borders/focus rings. Selectable via Settings → Appearance → Skin or `/theme skin:zeus`. Fills a niche distinct from Geist Contrast (which redefines the accent) and Nous (steel-blue accent) by preserving the default gold while replacing the navy-tinted surfaces with warm near-blacks.

## [v0.51.195] — 2026-06-01 — Release FO (stage-batch7 — hide attachment path markers in chat UI)

### Fixed
Expand Down
7 changes: 5 additions & 2 deletions THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ absent for light. System mode tracks the OS preference at runtime.
| **Catppuccin** | Catppuccin Latte/Mocha palette with Mauve accent. |
| **Nous** | Steel-blue accent with dashed technical surfaces. |
| **Geist Contrast** (`geist-contrast`) | Geist-inspired monochrome surfaces with a restrained dark-mode `#FFF175` accent. |
| **Zeus** | OLED-near-black surfaces with the default gold accent. Deep and warm. Defines dark overrides only; light mode falls back to the default skin. |

Each skin defines paired light + dark variants so it reads cleanly on either
theme. The skin is applied as `data-skin="<name>"` on `<html>` (the default
Most skins define paired light + dark variants so they read cleanly on either
theme. Dark-only skins (e.g. **Zeus**) intentionally omit a light variant; in
light mode the default skin takes over.
The skin is applied as `data-skin="<name>"` on `<html>` (the default
skin clears the attribute).

---
Expand Down
1 change: 1 addition & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4850,6 +4850,7 @@ def _get_session_agent_lock(session_id: str) -> threading.Lock:
"catppuccin",
"nous",
"geist-contrast",
"zeus",
}
_SETTINGS_LEGACY_THEME_MAP = {
# Legacy full themes now map onto the closest supported theme + accent skin pair.
Expand Down
Binary file added docs/pr-media/zeus/zeus-convo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/zeus/zeus-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@ const _SKINS=[
{name:'Nous', colors:['#4682B4','#3A6E9A','#2C5F88']},
{name:'Neon', colors:['#B347FF','#C76BFF','#00DDFF']},
{name:'Geist Contrast', value:'geist-contrast', colors:['#000000','#ffffff','#FFF175']},
{name:'Zeus', colors:['#FFD700','#FFBF00','#1A1A00']},
];
const _VALID_THEMES=new Set((_THEMES||[]).map(t=>t.value));
const _VALID_SKINS=new Set((_SKINS||[]).map(s=>(s.value||s.name).toLowerCase()));
Expand Down
20 changes: 10 additions & 10 deletions static/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ const LOCALES = {
cmd_terminal: 'Open the workspace terminal',
cmd_new: 'Start a new chat session',
cmd_usage: 'Toggle token usage display on/off',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing gap not introduced by this PR — hepburn and neon are already absent from cmd_theme in current master. Added zeus consistently with the existing pattern. Happy to fix the broader gap in a separate PR if useful.

cmd_personality: 'Switch agent personality',
cmd_skills: 'List available Hermes skills',
available_commands: 'Available commands:',
Expand Down Expand Up @@ -1522,7 +1522,7 @@ const LOCALES = {
cmd_terminal: 'Apri il terminale del workspace',
cmd_new: 'Avvia una nuova sessione di chat',
cmd_usage: 'Attiva/disattiva visualizzazione uso token',
cmd_theme: 'Cambia aspetto (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Cambia aspetto (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: "Cambia personalità dell'agente",
cmd_skills: 'Elenca le skill Hermes disponibili',
available_commands: 'Comandi disponibili:',
Expand Down Expand Up @@ -2834,7 +2834,7 @@ const LOCALES = {
cmd_terminal: 'ワークスペースのターミナルを開く',
cmd_new: '新しいチャットセッションを開始',
cmd_usage: 'トークン使用量表示の ON/OFF を切り替え',
cmd_theme: '外観を切り替え (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: '外観を切り替え (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'エージェントのパーソナリティを切り替え',
cmd_skills: '利用可能な Hermes スキルを一覧表示',
available_commands: '利用可能なコマンド:',
Expand Down Expand Up @@ -4108,7 +4108,7 @@ const LOCALES = {
cmd_terminal: 'Открыть терминал рабочей области',
cmd_new: 'Начать новую сессию чата',
cmd_usage: 'Показать или скрыть использование токенов',
cmd_theme: 'Переключить внешний вид (тема: system/dark/light, скин: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Переключить внешний вид (тема: system/dark/light, скин: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Переключить личность агента',
cmd_skills: 'Показать доступные навыки Hermes',
available_commands: 'Доступные команды:',
Expand Down Expand Up @@ -5391,7 +5391,7 @@ const LOCALES = {
cmd_terminal: 'Abrir terminal del espacio de trabajo',
cmd_new: 'Iniciar una nueva sesión de chat',
cmd_usage: 'Activar o desactivar el uso de tokens',
cmd_theme: 'Cambiar apariencia (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Cambiar apariencia (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Cambiar la personalidad del agente',
cmd_skills: 'Listar las skills de Hermes disponibles',
available_commands: 'Comandos disponibles:',
Expand Down Expand Up @@ -6615,7 +6615,7 @@ const LOCALES = {
cmd_terminal: 'Workspace-Terminal öffnen',
cmd_new: 'Neue Chat-Sitzung starten',
cmd_usage: 'Token-Verbrauchsanzeige umschalten',
cmd_theme: 'Darstellung wechseln (Theme: system/dark/light, Skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Darstellung wechseln (Theme: system/dark/light, Skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Agenten-Persönlichkeit wechseln',
cmd_skills: 'Verfügbare Hermes-Skills auflisten',
available_commands: 'Verfügbare Befehle:',
Expand Down Expand Up @@ -10388,7 +10388,7 @@ const LOCALES = {
cmd_workspace: 'Trocar workspace por nome',
cmd_new: 'Iniciar nova sessão de chat',
cmd_usage: 'Alternar exibição de uso de tokens',
cmd_theme: 'Trocar aparência (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Trocar aparência (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Trocar personalidade do agente',
cmd_skills: 'Listar skills disponíveis do Hermes',
available_commands: 'Comandos disponíveis:',
Expand Down Expand Up @@ -11586,7 +11586,7 @@ const LOCALES = {
cmd_terminal: '워크스페이스 터미널 열기',
cmd_new: '새 채팅 세션 시작',
cmd_usage: '토큰 사용량 표시 켜기/끄기',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Switch agent personality',
cmd_skills: 'List available Hermes skills',
available_commands: '사용 가능한 명령:',
Expand Down Expand Up @@ -12891,7 +12891,7 @@ const LOCALES = {
cmd_terminal: 'Ouvrez le terminal de l\'espace de travail',
cmd_new: 'Démarrer une nouvelle session de discussion',
cmd_usage: 'Activer/désactiver l\'affichage de l\'utilisation du jeton',
cmd_theme: 'Changer d\'apparence (thème : system/dark/light, skin : default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Changer d\'apparence (thème : system/dark/light, skin : default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Personnalité de l\'agent de commutation',
cmd_skills: 'Lister les compétences Hermès disponibles',
available_commands: 'Commandes disponibles :',
Expand Down Expand Up @@ -14148,7 +14148,7 @@ const LOCALES = {
cmd_terminal: 'Çalışma alanı terminalini açın',
cmd_new: 'Yeni bir sohbet oturumu başlatın',
cmd_usage: 'Token kullanımı ekranını aç/kapat',
cmd_theme: 'Görünümü değiştir (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_theme: 'Görünümü değiştir (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)',
cmd_personality: 'Temsilci kişiliğini değiştir',
cmd_skills: 'Mevcut Hermes becerilerini listele',
available_commands: 'Mevcut komutlar:',
Expand Down
2 changes: 1 addition & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Hermes">
<link rel="apple-touch-icon" sizes="512x512" href="static/apple-touch-icon.png">
<script>(function(){try{var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1,sienna:1,catppuccin:1,hepburn:1,nous:1,'geist-contrast':1,neon:1},legacy={slate:['dark','slate'],solarized:['dark','poseidon'],monokai:['dark','sisyphus'],nord:['dark','slate'],oled:['dark','default']},t=(localStorage.getItem('hermes-theme')||'dark').toLowerCase(),s=(localStorage.getItem('hermes-skin')||'').toLowerCase(),m=legacy[t],theme=m?m[0]:(themes[t]?t:'dark'),skin=skins[s]?s:(m?m[1]:'default');localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);if(theme==='system')theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(theme==='dark')document.documentElement.classList.add('dark');if(skin!=='default')document.documentElement.dataset.skin=skin;}catch(e){document.documentElement.classList.add('dark');}})()</script>
<script>(function(){try{var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1,sienna:1,catppuccin:1,hepburn:1,nous:1,'geist-contrast':1,neon:1,zeus:1},legacy={slate:['dark','slate'],solarized:['dark','poseidon'],monokai:['dark','sisyphus'],nord:['dark','slate'],oled:['dark','default']},t=(localStorage.getItem('hermes-theme')||'dark').toLowerCase(),s=(localStorage.getItem('hermes-skin')||'').toLowerCase(),m=legacy[t],theme=m?m[0]:(themes[t]?t:'dark'),skin=skins[s]?s:(m?m[1]:'default');localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);if(theme==='system')theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(theme==='dark')document.documentElement.classList.add('dark');if(skin!=='default')document.documentElement.dataset.skin=skin;}catch(e){document.documentElement.classList.add('dark');}})()</script>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarifying the framing: Zeus defines dark-mode overrides only, but light mode is fully functional — it inherits the complete default skin palette rather than landing in a partially styled state. The index.html allowlist entry is intentional and consistent with how other skins handle this. Happy to add a clamping guard in a follow-up if the project wants to standardise across all skins that don't define a light variant.

<script>(function(){try{var fs=localStorage.getItem('hermes-font-size');if(fs&&fs!=='default')document.documentElement.dataset.fontSize=fs;}catch(e){}})()</script>
<!-- theme-color: surfaces the active app chrome color to native status bars (Safari status bar, PWA, native WKWebView wrappers). Updated dynamically by boot.js when theme/skin changes. The light/dark default values match style.css :root --sidebar / :root.dark --sidebar. -->
<meta name="theme-color" content="#FAF7F0" media="(prefers-color-scheme: light)">
Expand Down
27 changes: 27 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,33 @@
:root[data-skin="geist-contrast"] textarea#msg{scrollbar-width:none;overflow-y:auto;}
:root[data-skin="geist-contrast"] textarea#msg::-webkit-scrollbar{display:none;}

/* ── Skin: Zeus ──
OLED-near-black skin: replaces the default dark-blue/navy surfaces with warm
near-blacks while keeping the gold accent unchanged. Borders are gold-tinted
for warmth so the accent feels connected to the chrome.
Dark-only — no light-mode variant. */
:root.dark[data-skin="zeus"]{
--bg:#0F0F0F;--sidebar:#111111;--surface:#181818;--code-bg:#181818;
--topbar-bg:rgba(15,15,15,.98);--main-bg:rgba(15,15,15,0.6);
--border:#2A2A1E;--border2:rgba(255,215,0,0.18);
--input-bg:rgba(255,255,255,.04);--hover-bg:rgba(255,215,0,.06);
--surface-subtle:rgba(255,255,255,.03);--surface-subtle-hover:rgba(255,255,255,.05);
--border-subtle:rgba(255,215,0,.08);--border-muted:rgba(255,215,0,.14);
--focus-ring:rgba(255,215,0,.4);--focus-glow:rgba(255,215,0,.1);
}
:root.dark[data-skin="zeus"] .topbar{background:#0F0F0F;border-bottom:1px solid rgba(255,215,0,0.15);}
:root.dark[data-skin="zeus"] .sidebar{background:#111111;border-right:1px solid rgba(255,215,0,0.12);}
:root.dark[data-skin="zeus"] .session-item.active{background:rgba(255,215,0,0.1);border-left:2px solid #FFD700;color:#FFD700;}
:root.dark[data-skin="zeus"] .session-item:hover:not(.active){background:rgba(255,215,0,0.05);}
:root.dark[data-skin="zeus"] .composer-box{background:#141414;border-color:rgba(255,215,0,0.2);}
:root.dark[data-skin="zeus"] .msg-row[data-role="assistant"] .msg-body{background:#181818;border-color:rgba(255,215,0,0.1);}
:root.dark[data-skin="zeus"] .msg-row[data-role="user"] .msg-body{background:#1C1600;border-color:rgba(255,215,0,0.25);}
:root.dark[data-skin="zeus"] .app-dialog{background:linear-gradient(180deg,rgba(24,24,24,.99),rgba(15,15,15,.99));border-color:rgba(255,215,0,0.2);}
:root.dark[data-skin="zeus"] .app-dialog-overlay{background:rgba(0,0,0,.72);}
:root.dark[data-skin="zeus"] .kanban-modal{background:linear-gradient(180deg,rgba(24,24,24,.99),rgba(15,15,15,.99));border-color:rgba(255,215,0,0.2);}
:root.dark[data-skin="zeus"] .kanban-modal-overlay{background:rgba(0,0,0,.72);}
:root.dark[data-skin="zeus"] .kanban-board-switcher-menu{background:linear-gradient(180deg,rgba(24,24,24,.99),rgba(15,15,15,.99));border-color:rgba(255,215,0,0.15);}

/* #594: app-dialog light mode overrides — base styles use hardcoded dark gradients */
:root:not(.dark) .app-dialog{
background:linear-gradient(180deg,rgba(240,237,232,.99),rgba(228,224,216,.99));
Expand Down
49 changes: 49 additions & 0 deletions tests/test_zeus_skin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Zeus skin registration and dark-surface affordances."""

from pathlib import Path

REPO = Path(__file__).parent.parent
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8")
Comment on lines +5 to +10

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matches the pattern used in every existing skin test in the repo (e.g. test_geist_contrast_skin.py, test_nous_skin.py) — keeping consistent for now. Happy to convert to a session-scoped fixture if the project adopts that pattern more broadly.



def test_zeus_skin_is_registered_in_all_files():
assert "{name:'Zeus'" in BOOT_JS
assert "zeus:1" in INDEX_HTML
assert '"zeus"' in CONFIG_PY


def test_zeus_dark_surfaces_are_near_black():
assert ':root.dark[data-skin="zeus"]' in CSS
assert "--bg:#0F0F0F" in CSS
assert "--sidebar:#111111" in CSS
assert "--surface:#181818" in CSS


def test_zeus_preserves_default_gold_accent():
# Zeus does NOT redefine --accent; it stays with the default gold.
# Verify gold-tinted border/focus vars are present instead.
assert "--border2:rgba(255,215,0,0.18)" in CSS
assert "--focus-ring:rgba(255,215,0,.4)" in CSS
assert "--hover-bg:rgba(255,215,0,.06)" in CSS


def test_zeus_active_session_uses_gold_highlight():
assert ':root.dark[data-skin="zeus"] .session-item.active' in CSS
assert "border-left:2px solid #FFD700" in CSS


def test_zeus_modals_are_not_navy():
# Modals/dialogs default to a hardcoded navy gradient — Zeus must override
assert ':root.dark[data-skin="zeus"] .app-dialog' in CSS
assert ':root.dark[data-skin="zeus"] .kanban-modal' in CSS
assert "rgba(24,24,24,.99)" in CSS


def test_zeus_i18n_lists_skin_in_all_locales():
# Zeus is the last skin in each locale's cmd_theme string, so it appears
# as `…/zeus)` rather than `/zeus/`. There are 10 locales.
assert I18N_JS.count("zeus)") == 10