diff --git a/hermes_cli/skin_engine.py b/hermes_cli/skin_engine.py index 4222a966e..7efbbc685 100644 --- a/hermes_cli/skin_engine.py +++ b/hermes_cli/skin_engine.py @@ -30,6 +30,14 @@ All fields are optional. Missing values inherit from the ``default`` skin. prompt: "#FFF8DC" # Prompt text color input_rule: "#CD7F32" # Input area horizontal rule response_border: "#FFD700" # Response box border (ANSI) + status_bar_bg: "#1a1a2e" # Status bar background + status_bar_text: "#C0C0C0" # Status bar default text + status_bar_strong: "#FFD700" # Status bar highlighted text + status_bar_dim: "#8B8682" # Status bar separators/muted text + status_bar_good: "#8FBC8F" # Healthy context usage + status_bar_warn: "#FFD700" # Warning context usage + status_bar_bad: "#FF8C00" # High context usage + status_bar_critical: "#FF6B6B" # Critical context usage session_label: "#DAA520" # Session label color session_border: "#8B8682" # Session ID dim color status_bar_bg: "#1a1a2e" # TUI status/usage bar background @@ -170,9 +178,40 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#FFF8DC", "input_rule": "#CD7F32", "response_border": "#FFD700", + "status_bar_bg": "#1a1a2e", + "status_bar_text": "#C0C0C0", + "status_bar_strong": "#FFD700", + "status_bar_dim": "#8B8682", + "status_bar_good": "#8FBC8F", + "status_bar_warn": "#FFD700", + "status_bar_bad": "#FF8C00", + "status_bar_critical": "#FF6B6B", "session_label": "#DAA520", "session_border": "#8B8682", }, + "colors_light": { + "banner_border": "#7A5A00", + "banner_title": "#6B4C00", + "banner_accent": "#7A5500", + "banner_dim": "#8B7355", + "banner_text": "#3D2B00", + "prompt": "#3D2B00", + "ui_accent": "#7A5500", + "ui_label": "#01579B", + "ui_ok": "#1B5E20", + "input_rule": "#7A5A00", + "response_border": "#6B4C00", + "status_bar_bg": "#F2EEE3", + "status_bar_text": "#5C4300", + "status_bar_strong": "#6B4C00", + "status_bar_dim": "#8B7355", + "status_bar_good": "#1B5E20", + "status_bar_warn": "#7A5500", + "status_bar_bad": "#B85C00", + "status_bar_critical": "#B3261E", + "session_label": "#5C4300", + "session_border": "#8B7355", + }, "spinner": { # Empty = use hardcoded defaults in display.py }, @@ -203,9 +242,40 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#F1E6CF", "input_rule": "#9F1C1C", "response_border": "#C7A96B", + "status_bar_bg": "#2A1212", + "status_bar_text": "#F1E6CF", + "status_bar_strong": "#C7A96B", + "status_bar_dim": "#6E584B", + "status_bar_good": "#7BC96F", + "status_bar_warn": "#C7A96B", + "status_bar_bad": "#DD4A3A", + "status_bar_critical": "#EF5350", "session_label": "#C7A96B", "session_border": "#6E584B", }, + "colors_light": { + "banner_border": "#6B1010", + "banner_title": "#5C4300", + "banner_accent": "#8B1A1A", + "banner_dim": "#5C4030", + "banner_text": "#3A1800", + "prompt": "#3A1800", + "ui_accent": "#8B1A1A", + "ui_label": "#5C4300", + "ui_ok": "#1B5E20", + "input_rule": "#6B1010", + "response_border": "#7A1515", + "status_bar_bg": "#F6E7DB", + "status_bar_text": "#5C4300", + "status_bar_strong": "#7A1515", + "status_bar_dim": "#8A6F5A", + "status_bar_good": "#1B5E20", + "status_bar_warn": "#8B4000", + "status_bar_bad": "#8B1A1A", + "status_bar_critical": "#B3261E", + "session_label": "#5C4300", + "session_border": "#5C4A3A", + }, "spinner": { "waiting_faces": ["(⚔)", "(⛨)", "(▲)", "(<>)", "(/)"], "thinking_faces": ["(⚔)", "(⛨)", "(▲)", "(⌁)", "(<>)"], @@ -267,9 +337,41 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#c9d1d9", "input_rule": "#444444", "response_border": "#aaaaaa", + "status_bar_bg": "#1F1F1F", + "status_bar_text": "#C9D1D9", + "status_bar_strong": "#E6EDF3", + "status_bar_dim": "#777777", + "status_bar_good": "#B5B5B5", + "status_bar_warn": "#AAAAAA", + "status_bar_bad": "#D0D0D0", + "status_bar_critical": "#F0F0F0", "session_label": "#888888", "session_border": "#555555", }, + "colors_light": { + "banner_border": "#333333", + "banner_title": "#222222", + "banner_accent": "#333333", + "banner_dim": "#555555", + "banner_text": "#333333", + "prompt": "#222222", + "ui_accent": "#333333", + "ui_label": "#444444", + "ui_ok": "#444444", + "ui_error": "#333333", + "input_rule": "#333333", + "response_border": "#444444", + "status_bar_bg": "#EEEEEE", + "status_bar_text": "#333333", + "status_bar_strong": "#222222", + "status_bar_dim": "#666666", + "status_bar_good": "#444444", + "status_bar_warn": "#555555", + "status_bar_bad": "#333333", + "status_bar_critical": "#111111", + "session_label": "#444444", + "session_border": "#666666", + }, "spinner": {}, "branding": { "agent_name": "Hermes Agent", @@ -298,9 +400,40 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#c9d1d9", "input_rule": "#4169e1", "response_border": "#7eb8f6", + "status_bar_bg": "#151C2F", + "status_bar_text": "#C9D1D9", + "status_bar_strong": "#7EB8F6", + "status_bar_dim": "#4B5563", + "status_bar_good": "#63D0A6", + "status_bar_warn": "#E6A855", + "status_bar_bad": "#F7A072", + "status_bar_critical": "#FF7A7A", "session_label": "#7eb8f6", "session_border": "#4b5563", }, + "colors_light": { + "banner_border": "#1A3A7A", + "banner_title": "#1A3570", + "banner_accent": "#1E4090", + "banner_dim": "#3B4555", + "banner_text": "#1A2A50", + "prompt": "#1A2A50", + "ui_accent": "#1A3570", + "ui_label": "#1E3A80", + "ui_ok": "#1B5E20", + "input_rule": "#1A3A7A", + "response_border": "#2A4FA0", + "status_bar_bg": "#E9EEF8", + "status_bar_text": "#1A2A50", + "status_bar_strong": "#1A3570", + "status_bar_dim": "#5A6070", + "status_bar_good": "#1B5E20", + "status_bar_warn": "#8B5E00", + "status_bar_bad": "#B85C38", + "status_bar_critical": "#B3261E", + "session_label": "#1A3570", + "session_border": "#5A6070", + }, "spinner": {}, "branding": { "agent_name": "Hermes Agent", @@ -403,9 +536,40 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#EAF7FF", "input_rule": "#2A6FB9", "response_border": "#5DB8F5", + "status_bar_bg": "#0F2440", + "status_bar_text": "#EAF7FF", + "status_bar_strong": "#A9DFFF", + "status_bar_dim": "#496884", + "status_bar_good": "#6ED7B0", + "status_bar_warn": "#5DB8F5", + "status_bar_bad": "#2A6FB9", + "status_bar_critical": "#D94F4F", "session_label": "#A9DFFF", "session_border": "#496884", }, + "colors_light": { + "banner_border": "#0D3060", + "banner_title": "#0D3060", + "banner_accent": "#154080", + "banner_dim": "#2A4565", + "banner_text": "#0A2850", + "prompt": "#0A2850", + "ui_accent": "#0D3060", + "ui_label": "#0D3060", + "ui_ok": "#1B5E20", + "input_rule": "#0D3060", + "response_border": "#1A5090", + "status_bar_bg": "#E6F4FF", + "status_bar_text": "#0A2850", + "status_bar_strong": "#0D3060", + "status_bar_dim": "#3A5575", + "status_bar_good": "#1B5E20", + "status_bar_warn": "#1A5090", + "status_bar_bad": "#154080", + "status_bar_critical": "#B3261E", + "session_label": "#0D3060", + "session_border": "#3A5575", + }, "spinner": { "waiting_faces": ["(≈)", "(Ψ)", "(∿)", "(◌)", "(◠)"], "thinking_faces": ["(Ψ)", "(∿)", "(≈)", "(⌁)", "(◌)"], @@ -467,9 +631,42 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#F5F5F5", "input_rule": "#656565", "response_border": "#B7B7B7", + "status_bar_bg": "#202020", + "status_bar_text": "#D3D3D3", + "status_bar_strong": "#F5F5F5", + "status_bar_dim": "#656565", + "status_bar_good": "#B7B7B7", + "status_bar_warn": "#D3D3D3", + "status_bar_bad": "#E7E7E7", + "status_bar_critical": "#F5F5F5", "session_label": "#919191", "session_border": "#656565", }, + "colors_light": { + "banner_border": "#666666", + "banner_title": "#222222", + "banner_accent": "#333333", + "banner_dim": "#555555", + "banner_text": "#333333", + "prompt": "#222222", + "ui_accent": "#333333", + "ui_label": "#444444", + "ui_ok": "#444444", + "ui_error": "#333333", + "ui_warn": "#444444", + "input_rule": "#666666", + "response_border": "#555555", + "status_bar_bg": "#F0F0F0", + "status_bar_text": "#333333", + "status_bar_strong": "#222222", + "status_bar_dim": "#777777", + "status_bar_good": "#444444", + "status_bar_warn": "#555555", + "status_bar_bad": "#333333", + "status_bar_critical": "#111111", + "session_label": "#444444", + "session_border": "#777777", + }, "spinner": { "waiting_faces": ["(◉)", "(◌)", "(◬)", "(⬤)", "(::)"], "thinking_faces": ["(◉)", "(◬)", "(◌)", "(○)", "(●)"], @@ -532,9 +729,40 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = { "prompt": "#FFF0D4", "input_rule": "#C75B1D", "response_border": "#F29C38", + "status_bar_bg": "#2B160E", + "status_bar_text": "#FFF0D4", + "status_bar_strong": "#FFD39A", + "status_bar_dim": "#6C4724", + "status_bar_good": "#6BCB77", + "status_bar_warn": "#F29C38", + "status_bar_bad": "#E2832B", + "status_bar_critical": "#EF5350", "session_label": "#FFD39A", "session_border": "#6C4724", }, + "colors_light": { + "banner_border": "#7A3511", + "banner_title": "#5C2D00", + "banner_accent": "#8B4000", + "banner_dim": "#5A3A1A", + "banner_text": "#3A1E00", + "prompt": "#3A1E00", + "ui_accent": "#8B4000", + "ui_label": "#5C2D00", + "ui_ok": "#1B5E20", + "input_rule": "#7A3511", + "response_border": "#8B4513", + "status_bar_bg": "#F8EADF", + "status_bar_text": "#3A1E00", + "status_bar_strong": "#5C2D00", + "status_bar_dim": "#6B5540", + "status_bar_good": "#1B5E20", + "status_bar_warn": "#8B4513", + "status_bar_bad": "#8B4000", + "status_bar_critical": "#B3261E", + "session_label": "#5C2D00", + "session_border": "#6B5540", + }, "spinner": { "waiting_faces": ["(✦)", "(▲)", "(◇)", "(<>)", "(🔥)"], "thinking_faces": ["(✦)", "(▲)", "(◇)", "(⌁)", "(🔥)"], @@ -770,6 +998,13 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]: warn = skin.get_color("ui_warn", "#FF8C00") error = skin.get_color("ui_error", "#FF6B6B") status_bg = skin.get_color("status_bar_bg", "#1a1a2e") + status_text = skin.get_color("status_bar_text", text) + status_strong = skin.get_color("status_bar_strong", title) + status_dim = skin.get_color("status_bar_dim", dim) + status_good = skin.get_color("status_bar_good", skin.get_color("ui_ok", "#8FBC8F")) + status_warn = skin.get_color("status_bar_warn", warn) + status_bad = skin.get_color("status_bar_bad", skin.get_color("banner_accent", warn)) + status_critical = skin.get_color("status_bar_critical", error) voice_bg = skin.get_color("voice_status_bg", status_bg) menu_bg = skin.get_color("completion_menu_bg", "#1a1a2e") menu_current_bg = skin.get_color("completion_menu_current_bg", "#333355") @@ -782,13 +1017,13 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]: "prompt": prompt, "prompt-working": f"{dim} italic", "hint": f"{dim} italic", - "status-bar": f"bg:{status_bg} {text}", - "status-bar-strong": f"bg:{status_bg} {title} bold", - "status-bar-dim": f"bg:{status_bg} {dim}", - "status-bar-good": f"bg:{status_bg} {skin.get_color('ui_ok', '#8FBC8F')} bold", - "status-bar-warn": f"bg:{status_bg} {warn} bold", - "status-bar-bad": f"bg:{status_bg} {skin.get_color('banner_accent', warn)} bold", - "status-bar-critical": f"bg:{status_bg} {error} bold", + "status-bar": f"bg:{status_bg} {status_text}", + "status-bar-strong": f"bg:{status_bg} {status_strong} bold", + "status-bar-dim": f"bg:{status_bg} {status_dim}", + "status-bar-good": f"bg:{status_bg} {status_good} bold", + "status-bar-warn": f"bg:{status_bg} {status_warn} bold", + "status-bar-bad": f"bg:{status_bg} {status_bad} bold", + "status-bar-critical": f"bg:{status_bg} {status_critical} bold", "input-rule": input_rule, "image-badge": f"{label} bold", "completion-menu": f"bg:{menu_bg} {text}", diff --git a/tests/hermes_cli/test_skin_engine.py b/tests/hermes_cli/test_skin_engine.py index 3ce185b82..1d83fe830 100644 --- a/tests/hermes_cli/test_skin_engine.py +++ b/tests/hermes_cli/test_skin_engine.py @@ -267,8 +267,9 @@ class TestCliBrandingHelpers: assert get_active_goodbye() == "Farewell, warrior! ⚔" def test_prompt_toolkit_style_overrides_cover_tui_classes(self): - from hermes_cli.skin_engine import set_active_skin, get_prompt_toolkit_style_overrides + from hermes_cli.skin_engine import set_active_skin, set_theme_mode, get_prompt_toolkit_style_overrides + set_theme_mode("dark") set_active_skin("ares") overrides = get_prompt_toolkit_style_overrides() required = { @@ -277,6 +278,13 @@ class TestCliBrandingHelpers: "prompt", "prompt-working", "hint", + "status-bar", + "status-bar-strong", + "status-bar-dim", + "status-bar-good", + "status-bar-warn", + "status-bar-bad", + "status-bar-critical", "input-rule", "image-badge", "completion-menu", @@ -316,15 +324,26 @@ class TestCliBrandingHelpers: def test_prompt_toolkit_style_overrides_use_skin_colors(self): from hermes_cli.skin_engine import ( set_active_skin, + set_theme_mode, get_active_skin, get_prompt_toolkit_style_overrides, ) + set_theme_mode("dark") set_active_skin("ares") skin = get_active_skin() overrides = get_prompt_toolkit_style_overrides() assert overrides["prompt"] == skin.get_color("prompt") assert overrides["input-rule"] == skin.get_color("input_rule") + assert overrides["status-bar"] == ( + f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('status_bar_text')}" + ) + assert overrides["status-bar-strong"] == ( + f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('status_bar_strong')} bold" + ) + assert overrides["status-bar-critical"] == ( + f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('status_bar_critical')} bold" + ) assert overrides["clarify-title"] == f"{skin.get_color('banner_title')} bold" assert overrides["sudo-prompt"] == f"{skin.get_color('ui_error')} bold" assert overrides["approval-title"] == f"{skin.get_color('ui_warn')} bold" diff --git a/tests/test_cli_skin_integration.py b/tests/test_cli_skin_integration.py index 272a7bc5b..a4329b01a 100644 --- a/tests/test_cli_skin_integration.py +++ b/tests/test_cli_skin_integration.py @@ -59,6 +59,9 @@ class TestCliSkinPromptIntegration: def test_build_tui_style_dict_uses_skin_overrides(self): cli = _make_cli_stub() + from hermes_cli.skin_engine import set_theme_mode + + set_theme_mode("dark") set_active_skin("ares") skin = get_active_skin() style_dict = cli._build_tui_style_dict() @@ -66,6 +69,9 @@ class TestCliSkinPromptIntegration: assert style_dict["prompt"] == skin.get_color("prompt") assert style_dict["input-rule"] == skin.get_color("input_rule") assert style_dict["prompt-working"] == f"{skin.get_color('banner_dim')} italic" + assert style_dict["status-bar"] == ( + f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('status_bar_text')}" + ) assert style_dict["approval-title"] == f"{skin.get_color('ui_warn')} bold" def test_apply_tui_skin_style_updates_running_app(self):