mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(skins): add built-in daylight skin
This commit is contained in:
24
cli.py
24
cli.py
@@ -988,19 +988,19 @@ def _prune_orphaned_branches(repo_root: str) -> None:
|
||||
# ANSI building blocks for conversation display
|
||||
_ACCENT_ANSI_DEFAULT = "\033[1;38;2;255;215;0m" # True-color #FFD700 bold — fallback
|
||||
_BOLD = "\033[1m"
|
||||
_DIM = "\033[2m"
|
||||
_RST = "\033[0m"
|
||||
|
||||
|
||||
def _hex_to_ansi_bold(hex_color: str) -> str:
|
||||
"""Convert a hex color like '#268bd2' to a bold true-color ANSI escape."""
|
||||
def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:
|
||||
"""Convert a hex color like '#268bd2' to a true-color ANSI escape."""
|
||||
try:
|
||||
r = int(hex_color[1:3], 16)
|
||||
g = int(hex_color[3:5], 16)
|
||||
b = int(hex_color[5:7], 16)
|
||||
return f"\033[1;38;2;{r};{g};{b}m"
|
||||
prefix = "1;" if bold else ""
|
||||
return f"\033[{prefix}38;2;{r};{g};{b}m"
|
||||
except (ValueError, IndexError):
|
||||
return _ACCENT_ANSI_DEFAULT
|
||||
return _ACCENT_ANSI_DEFAULT if bold else "\033[38;2;184;134;11m"
|
||||
|
||||
|
||||
class _SkinAwareAnsi:
|
||||
@@ -1010,20 +1010,22 @@ class _SkinAwareAnsi:
|
||||
force re-resolution after a ``/skin`` switch.
|
||||
"""
|
||||
|
||||
def __init__(self, skin_key: str, fallback_hex: str = "#FFD700"):
|
||||
def __init__(self, skin_key: str, fallback_hex: str = "#FFD700", *, bold: bool = False):
|
||||
self._skin_key = skin_key
|
||||
self._fallback_hex = fallback_hex
|
||||
self._bold = bold
|
||||
self._cached: str | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self._cached is None:
|
||||
try:
|
||||
from hermes_cli.skin_engine import get_active_skin
|
||||
self._cached = _hex_to_ansi_bold(
|
||||
get_active_skin().get_color(self._skin_key, self._fallback_hex)
|
||||
self._cached = _hex_to_ansi(
|
||||
get_active_skin().get_color(self._skin_key, self._fallback_hex),
|
||||
bold=self._bold,
|
||||
)
|
||||
except Exception:
|
||||
self._cached = _hex_to_ansi_bold(self._fallback_hex)
|
||||
self._cached = _hex_to_ansi(self._fallback_hex, bold=self._bold)
|
||||
return self._cached
|
||||
|
||||
def __add__(self, other: str) -> str:
|
||||
@@ -1037,7 +1039,8 @@ class _SkinAwareAnsi:
|
||||
self._cached = None
|
||||
|
||||
|
||||
_ACCENT = _SkinAwareAnsi("response_border", "#FFD700")
|
||||
_ACCENT = _SkinAwareAnsi("response_border", "#FFD700", bold=True)
|
||||
_DIM = _SkinAwareAnsi("banner_dim", "#B8860B")
|
||||
|
||||
|
||||
def _accent_hex() -> str:
|
||||
@@ -6156,6 +6159,7 @@ class HermesCLI:
|
||||
|
||||
set_active_skin(new_skin)
|
||||
_ACCENT.reset() # Re-resolve ANSI color for the new skin
|
||||
_DIM.reset() # Re-resolve dim/secondary ANSI color for the new skin
|
||||
if save_config_value("display.skin", new_skin):
|
||||
print(f" Skin set to: {new_skin} (saved)")
|
||||
else:
|
||||
|
||||
@@ -41,6 +41,14 @@ colors:
|
||||
session_label: "#DAA520" # Session label
|
||||
session_border: "#8B8682" # Session ID dim color
|
||||
|
||||
# TUI surfaces
|
||||
status_bar_bg: "#1a1a2e" # Status / usage bar background
|
||||
voice_status_bg: "#1a1a2e" # Voice-mode badge background
|
||||
completion_menu_bg: "#1a1a2e" # Completion list background
|
||||
completion_menu_current_bg: "#333355" # Active completion row background
|
||||
completion_menu_meta_bg: "#1a1a2e" # Completion meta column background
|
||||
completion_menu_meta_current_bg: "#333355" # Active completion meta background
|
||||
|
||||
# ── Spinner ─────────────────────────────────────────────────────────────────
|
||||
# Customize the animated spinner shown during API calls and tool execution.
|
||||
spinner:
|
||||
|
||||
@@ -32,6 +32,12 @@ All fields are optional. Missing values inherit from the ``default`` skin.
|
||||
response_border: "#FFD700" # Response box border (ANSI)
|
||||
session_label: "#DAA520" # Session label color
|
||||
session_border: "#8B8682" # Session ID dim color
|
||||
status_bar_bg: "#1a1a2e" # TUI status/usage bar background
|
||||
voice_status_bg: "#1a1a2e" # TUI voice status background
|
||||
completion_menu_bg: "#1a1a2e" # Completion menu background
|
||||
completion_menu_current_bg: "#333355" # Active completion row background
|
||||
completion_menu_meta_bg: "#1a1a2e" # Completion meta column background
|
||||
completion_menu_meta_current_bg: "#333355" # Active completion meta background
|
||||
|
||||
# Spinner: customize the animated spinner during API calls
|
||||
spinner:
|
||||
@@ -87,6 +93,7 @@ BUILT-IN SKINS
|
||||
- ``ares`` — Crimson/bronze war-god theme with custom spinner wings
|
||||
- ``mono`` — Clean grayscale monochrome
|
||||
- ``slate`` — Cool blue developer-focused theme
|
||||
- ``daylight`` — Light background theme with dark text and blue accents
|
||||
|
||||
USER SKINS
|
||||
==========
|
||||
@@ -304,6 +311,43 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
},
|
||||
"tool_prefix": "┊",
|
||||
},
|
||||
"daylight": {
|
||||
"name": "daylight",
|
||||
"description": "Light theme for bright terminals with dark text and cool blue accents",
|
||||
"colors": {
|
||||
"banner_border": "#2563EB",
|
||||
"banner_title": "#0F172A",
|
||||
"banner_accent": "#1D4ED8",
|
||||
"banner_dim": "#475569",
|
||||
"banner_text": "#111827",
|
||||
"ui_accent": "#2563EB",
|
||||
"ui_label": "#0F766E",
|
||||
"ui_ok": "#15803D",
|
||||
"ui_error": "#B91C1C",
|
||||
"ui_warn": "#B45309",
|
||||
"prompt": "#111827",
|
||||
"input_rule": "#93C5FD",
|
||||
"response_border": "#2563EB",
|
||||
"session_label": "#1D4ED8",
|
||||
"session_border": "#64748B",
|
||||
"status_bar_bg": "#E5EDF8",
|
||||
"voice_status_bg": "#E5EDF8",
|
||||
"completion_menu_bg": "#F8FAFC",
|
||||
"completion_menu_current_bg": "#DBEAFE",
|
||||
"completion_menu_meta_bg": "#EEF2FF",
|
||||
"completion_menu_meta_current_bg": "#BFDBFE",
|
||||
},
|
||||
"spinner": {},
|
||||
"branding": {
|
||||
"agent_name": "Hermes Agent",
|
||||
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
|
||||
"goodbye": "Goodbye! ⚕",
|
||||
"response_label": " ⚕ Hermes ",
|
||||
"prompt_symbol": "❯ ",
|
||||
"help_header": "[?] Available Commands",
|
||||
},
|
||||
"tool_prefix": "│",
|
||||
},
|
||||
"poseidon": {
|
||||
"name": "poseidon",
|
||||
"description": "Ocean-god theme — deep blue and seafoam",
|
||||
@@ -685,6 +729,12 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
|
||||
label = skin.get_color("ui_label", title)
|
||||
warn = skin.get_color("ui_warn", "#FF8C00")
|
||||
error = skin.get_color("ui_error", "#FF6B6B")
|
||||
status_bg = skin.get_color("status_bar_bg", "#1a1a2e")
|
||||
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")
|
||||
menu_meta_bg = skin.get_color("completion_menu_meta_bg", menu_bg)
|
||||
menu_meta_current_bg = skin.get_color("completion_menu_meta_current_bg", menu_current_bg)
|
||||
|
||||
return {
|
||||
"input-area": prompt,
|
||||
@@ -692,13 +742,20 @@ 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",
|
||||
"input-rule": input_rule,
|
||||
"image-badge": f"{label} bold",
|
||||
"completion-menu": f"bg:#1a1a2e {text}",
|
||||
"completion-menu.completion": f"bg:#1a1a2e {text}",
|
||||
"completion-menu.completion.current": f"bg:#333355 {title}",
|
||||
"completion-menu.meta.completion": f"bg:#1a1a2e {dim}",
|
||||
"completion-menu.meta.completion.current": f"bg:#333355 {label}",
|
||||
"completion-menu": f"bg:{menu_bg} {text}",
|
||||
"completion-menu.completion": f"bg:{menu_bg} {text}",
|
||||
"completion-menu.completion.current": f"bg:{menu_current_bg} {title}",
|
||||
"completion-menu.meta.completion": f"bg:{menu_meta_bg} {dim}",
|
||||
"completion-menu.meta.completion.current": f"bg:{menu_meta_current_bg} {label}",
|
||||
"clarify-border": input_rule,
|
||||
"clarify-title": f"{title} bold",
|
||||
"clarify-question": f"{text} bold",
|
||||
@@ -716,4 +773,6 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
|
||||
"approval-cmd": f"{dim} italic",
|
||||
"approval-choice": dim,
|
||||
"approval-selected": f"{title} bold",
|
||||
"voice-status": f"bg:{voice_bg} {label}",
|
||||
"voice-status-recording": f"bg:{voice_bg} {error} bold",
|
||||
}
|
||||
|
||||
@@ -78,6 +78,20 @@ class TestBuiltinSkins:
|
||||
assert skin.name == "slate"
|
||||
assert skin.get_color("banner_title") == "#7eb8f6"
|
||||
|
||||
def test_daylight_skin_loads(self):
|
||||
from hermes_cli.skin_engine import load_skin
|
||||
|
||||
skin = load_skin("daylight")
|
||||
assert skin.name == "daylight"
|
||||
assert skin.tool_prefix == "│"
|
||||
assert skin.get_color("banner_title") == "#0F172A"
|
||||
assert skin.get_color("status_bar_bg") == "#E5EDF8"
|
||||
assert skin.get_color("voice_status_bg") == "#E5EDF8"
|
||||
assert skin.get_color("completion_menu_bg") == "#F8FAFC"
|
||||
assert skin.get_color("completion_menu_current_bg") == "#DBEAFE"
|
||||
assert skin.get_color("completion_menu_meta_bg") == "#EEF2FF"
|
||||
assert skin.get_color("completion_menu_meta_current_bg") == "#BFDBFE"
|
||||
|
||||
def test_unknown_skin_falls_back_to_default(self):
|
||||
from hermes_cli.skin_engine import load_skin
|
||||
skin = load_skin("nonexistent_skin_xyz")
|
||||
@@ -114,6 +128,7 @@ class TestSkinManagement:
|
||||
assert "ares" in names
|
||||
assert "mono" in names
|
||||
assert "slate" in names
|
||||
assert "daylight" in names
|
||||
for s in skins:
|
||||
assert "source" in s
|
||||
assert s["source"] == "builtin"
|
||||
@@ -242,6 +257,15 @@ class TestCliBrandingHelpers:
|
||||
"completion-menu.completion.current",
|
||||
"completion-menu.meta.completion",
|
||||
"completion-menu.meta.completion.current",
|
||||
"status-bar",
|
||||
"status-bar-strong",
|
||||
"status-bar-dim",
|
||||
"status-bar-good",
|
||||
"status-bar-warn",
|
||||
"status-bar-bad",
|
||||
"status-bar-critical",
|
||||
"voice-status",
|
||||
"voice-status-recording",
|
||||
"clarify-border",
|
||||
"clarify-title",
|
||||
"clarify-question",
|
||||
@@ -277,3 +301,9 @@ class TestCliBrandingHelpers:
|
||||
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"
|
||||
|
||||
set_active_skin("daylight")
|
||||
skin = get_active_skin()
|
||||
overrides = get_prompt_toolkit_style_overrides()
|
||||
assert overrides["status-bar"] == f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('banner_text')}"
|
||||
assert overrides["voice-status"] == f"bg:{skin.get_color('voice_status_bg')} {skin.get_color('ui_label')}"
|
||||
|
||||
@@ -36,6 +36,7 @@ display:
|
||||
| `ares` | War-god theme — crimson and bronze | `Ares Agent` | Deep crimson borders with bronze accents. Aggressive spinner verbs ("forging", "marching", "tempering steel"). Custom sword-and-shield ASCII art banner. |
|
||||
| `mono` | Monochrome — clean grayscale | `Hermes Agent` | All grays — no color. Borders are `#555555`, text is `#c9d1d9`. Ideal for minimal terminal setups or screen recordings. |
|
||||
| `slate` | Cool blue — developer-focused | `Hermes Agent` | Royal blue borders (`#4169e1`), soft blue text. Calm and professional. No custom spinner — uses default faces. |
|
||||
| `daylight` | Light theme for bright terminals with dark text and cool blue accents | `Hermes Agent` | Designed for white or bright terminals. Dark slate text with blue borders, pale status surfaces, and a light completion menu that stays readable in light terminal profiles. |
|
||||
| `poseidon` | Ocean-god theme — deep blue and seafoam | `Poseidon Agent` | Deep blue to seafoam gradient. Ocean-themed spinners ("charting currents", "sounding the depth"). Trident ASCII art banner. |
|
||||
| `sisyphus` | Sisyphean theme — austere grayscale with persistence | `Sisyphus Agent` | Light grays with stark contrast. Boulder-themed spinners ("pushing uphill", "resetting the boulder", "enduring the loop"). Boulder-and-hill ASCII art banner. |
|
||||
| `charizard` | Volcanic theme — burnt orange and ember | `Charizard Agent` | Warm burnt orange to ember gradient. Fire-themed spinners ("banking into the draft", "measuring burn"). Dragon-silhouette ASCII art banner. |
|
||||
@@ -63,6 +64,12 @@ Controls all color values throughout the CLI. Values are hex color strings.
|
||||
| `response_border` | Border around the agent's response box (ANSI escape) | `#FFD700` |
|
||||
| `session_label` | Session label color | `#DAA520` |
|
||||
| `session_border` | Session ID dim border color | `#8B8682` |
|
||||
| `status_bar_bg` | Background color for the TUI status / usage bar | `#1a1a2e` |
|
||||
| `voice_status_bg` | Background color for the voice-mode status badge | `#1a1a2e` |
|
||||
| `completion_menu_bg` | Background color for the completion menu list | `#1a1a2e` |
|
||||
| `completion_menu_current_bg` | Background color for the active completion row | `#333355` |
|
||||
| `completion_menu_meta_bg` | Background color for the completion meta column | `#1a1a2e` |
|
||||
| `completion_menu_meta_current_bg` | Background color for the active completion meta column | `#333355` |
|
||||
|
||||
### Spinner (`spinner:`)
|
||||
|
||||
@@ -129,6 +136,12 @@ colors:
|
||||
response_border: "#FFD700"
|
||||
session_label: "#DAA520"
|
||||
session_border: "#8B8682"
|
||||
status_bar_bg: "#1a1a2e"
|
||||
voice_status_bg: "#1a1a2e"
|
||||
completion_menu_bg: "#1a1a2e"
|
||||
completion_menu_current_bg: "#333355"
|
||||
completion_menu_meta_bg: "#1a1a2e"
|
||||
completion_menu_meta_current_bg: "#333355"
|
||||
|
||||
spinner:
|
||||
waiting_faces:
|
||||
|
||||
Reference in New Issue
Block a user