From 1d109f5be3514266ff30951c0bcdae2faba9abc4 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 15 May 2026 00:01:16 -0500 Subject: [PATCH] feat(cli): light-mode color remap covers all skin reads (Rich Panel borders, etc) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes that together make the response Panel readable in light Terminal.app mode: 1. Hook Skin.get_color() at module load so EVERY skin color read goes through _maybe_remap_for_light_mode(). Previously only _hex_to_ansi() and pt's style strings were remapped — Rich Panel borders and body text bypassed the remap and stayed as #FFF8DC (cornsilk on cream). 2. Prime the light-mode detection cache at import time when stdin is a tty. Ensures OSC 11 query happens before any banner/Panel render. 3. Drop status-bar fg colors (#C0C0C0 silver, #888888, #555555, #8B8682) from the remap table — those are paired with a dark navy bg, so remapping them to dark gray would make them invisible the OTHER direction (dark on dark). --- cli.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/cli.py b/cli.py index abb9776acd..29ffae5977 100644 --- a/cli.py +++ b/cli.py @@ -1423,6 +1423,11 @@ def _detect_light_mode() -> bool: # Light-mode equivalents of skin colors that are unreadable on cream # Terminal.app backgrounds. Used by _SkinAwareAnsi to remap colors # at resolution time when light mode is detected. +# +# IMPORTANT: only remap colors that are used as STANDALONE foregrounds +# on the terminal's background. Don't remap colors that are paired +# with a dark bg (e.g. status bar text on bg:#1a1a2e) — those would +# become invisible the OTHER direction (dark gray on dark navy). _LIGHT_MODE_REMAP: dict[str, str] = { # Original (dark-mode) -> Light-mode replacement (darker, readable) "#FFF8DC": "#1A1A1A", # cornsilk -> near-black @@ -1436,11 +1441,10 @@ _LIGHT_MODE_REMAP: dict[str, str] = { "#F5F5F5": "#1A1A1A", "#FFF0D4": "#1A1A1A", "#CD7F32": "#8A4F1A", # bronze -> darker bronze - "#C0C0C0": "#3A3A3A", # silver -> dark gray - "#888888": "#444444", - "#555555": "#444444", - "#8B8682": "#444444", "#FFEFB5": "#3A2A00", + # NOTE: skipping #C0C0C0/#888888/#555555/#8B8682 — those are + # status-bar foregrounds paired with dark navy bg, where dark + # remap values would become invisible. } @@ -1462,6 +1466,41 @@ def _maybe_remap_for_light_mode(hex_color: str) -> str: _LIGHT_MODE_REMAP_UPPER = {k.upper(): v for k, v in _LIGHT_MODE_REMAP.items()} +def _install_skin_light_mode_hook() -> None: + """Wrap Skin.get_color at import time so EVERY skin color read goes + through the light-mode remap. Idempotent.""" + try: + from hermes_cli.skin_engine import Skin # type: ignore[import] + except Exception: + return + if getattr(Skin, "_hermes_light_mode_hook_installed", False): + return + _orig_get_color = Skin.get_color + + def _wrapped_get_color(self, key, fallback=""): + value = _orig_get_color(self, key, fallback) + try: + return _maybe_remap_for_light_mode(value) + except Exception: + return value + + Skin.get_color = _wrapped_get_color # type: ignore[method-assign] + Skin._hermes_light_mode_hook_installed = True # type: ignore[attr-defined] + + +_install_skin_light_mode_hook() + + +# Prime the light-mode detection cache early (at module load) when +# we're running interactively so OSC 11 happens before pt grabs the +# tty. Skip for non-tty contexts (subagents, gateway, tests). +try: + if sys.stdin.isatty() and sys.stdout.isatty(): + _detect_light_mode() +except Exception: + pass + + class _SkinAwareAnsi: """Lazy ANSI escape that resolves from the skin engine on first use. @@ -8128,8 +8167,8 @@ class HermesCLI: from hermes_cli.skin_engine import get_active_skin _skin = get_active_skin() label = _skin.get_branding("response_label", "⚕ Hermes") - _resp_color = _skin.get_color("response_border", "#CD7F32") - _resp_text = _skin.get_color("banner_text", "#FFF8DC") + _resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32")) + _resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC")) except Exception: label = "⚕ Hermes" _resp_color = "#CD7F32" @@ -11110,12 +11149,12 @@ class HermesCLI: from hermes_cli.skin_engine import get_active_skin _skin = get_active_skin() label = _skin.get_branding("response_label", "⚕ Hermes") - _resp_color = _skin.get_color("response_border", "#CD7F32") - _resp_text = _skin.get_color("banner_text", "#FFF8DC") + _resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32")) + _resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC")) except Exception: label = "⚕ Hermes" - _resp_color = "#CD7F32" - _resp_text = "#FFF8DC" + _resp_color = _maybe_remap_for_light_mode("#CD7F32") + _resp_text = _maybe_remap_for_light_mode("#FFF8DC") is_error_response = result and (result.get("failed") or result.get("partial")) already_streamed = self._stream_started and self._stream_box_opened and not is_error_response