{"repo":"NousResearch/hermes-agent","public_id":"4807","display_label":"Terminal light background readability and theme detection","summary":"These issues and pull requests concern Hermes TUI and CLI text being unreadable on light or cream terminal backgrounds, and efforts to improve auto-detection of light terminals, fix unreliable COLORFGBG inheritance (especially in Zed), and add light-mode skins and color adjustments.","subsystem":"apps/desktop","counts":{"members":32,"open_issues":5,"open_issues_distinct":5,"open_prs":3,"closed_issues":6,"merged_prs":5,"attempt_prs":16},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":9461},"resolution":{"state":"partially_resolved","by":"pr-13103"},"freshness":{"state":"verified","last_verified_at":"2026-05-15T06:23:32+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.06129032258064515},"fix_edges":[{"issue_number":4807,"pr_number":9461,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8526,"pr_number":13103,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8526,"pr_number":17695,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8526,"pr_number":26137,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":11300,"pr_number":13103,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":12330,"pr_number":17113,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":12330,"pr_number":26137,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":60141,"pr_number":17695,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":4807,"title":"[Bug]: CLI is unreadable on light/cream terminal backgrounds — no light mode support","url":null,"state":"closed"},{"kind":"issue","number":8526,"title":"[Bug]:  [color] default color is broken, when launching hermes via terminal, not being readable","url":null,"state":"closed"},{"kind":"issue","number":11300,"title":"[Bug]: TUI hardcodes bright yellow colors, unreadable on white terminal backgrounds","url":null,"state":"closed"},{"kind":"issue","number":12330,"title":"[Bug]: The text of Hermes is hard to read on a white terminal background.","url":null,"state":"closed"},{"kind":"issue","number":19039,"title":"[Bug]: CLI teminal interface hard to read in day/light background.","url":null,"state":"closed"},{"kind":"issue","number":31143,"title":"Remote/Docker deployments: skin selection should auto-set TUI light/dark mode base","url":null,"state":"open"},{"kind":"issue","number":47210,"title":"[Feature]: Hermes CLI does not auto-detect light terminal backgrounds in Ghostty, breaks dark-themed skins","url":null,"state":"open"},{"kind":"issue","number":58038,"title":"[Feature]: QR code color-inversion detection for dark-mode terminals (WhatsApp pairing)","url":null,"state":"open"},{"kind":"issue","number":59751,"title":"[Bug]: Hermes .app TUI agent reply text unreadable on MacBook Air M2 15.3\" Retina","url":null,"state":"open"},{"kind":"issue","number":60141,"title":"[Bug]: Hermes TUI assistant reply text remains low-contrast on macOS Terminal.app light profile","url":null,"state":"closed"},{"kind":"issue","number":61536,"title":"[Bug]: Zed dark themes: `/skin default` is rendered as light-mode colors because Hermes trusts inherited COLORFGBG","url":null,"state":"open"},{"kind":"pr","number":4811,"title":"feat(skin): add built-in light skin for light/cream terminal backgrounds","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#9461","number":9461}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":8567,"title":"fix(cli): improve default skin contrast on light terminals","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#9461","number":9461}],"category":"wontfix_bydesign","implication":"wontfix"},"merged":false},{"kind":"pr","number":9461,"title":"feat(skin): add light-mode skins + skin-aware completion menus","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","docs/skins/example-skin.yaml","hermes_cli/skin_engine.py","tests/hermes_cli/test_skin_engine.py","website/docs/user-guide/features/skins.md"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -988,19 +988,19 @@ def _prune_orphaned_branches(repo_root: str) -> None:\n # ANSI building blocks for conversation display\n _ACCENT_ANSI_DEFAULT = \"\\033[1;38;2;255;215;0m\"  # True-color #FFD700 bold — fallback\n _BOLD = \"\\033[1m\"\n-_DIM = \"\\033[2m\"\n _RST = \"\\033[0m\"\n \n \n-def _hex_to_ansi_bold(hex_color: str) -> str:\n-    \"\"\"Convert a hex color like '#268bd2' to a bold true-color ANSI escape.\"\"\"\n+def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:\n+    \"\"\"Convert a hex color like '#268bd2' to a true-color ANSI escape.\"\"\"\n     try:\n         r = int(hex_color[1:3], 16)\n         g = int(hex_color[3:5], 16)\n         b = int(hex_color[5:7], 16)\n-        return f\"\\033[1;38;2;{r};{g};{b}m\"\n+        prefix = \"1;\" if bold else \"\"\n+        return f\"\\033[{prefix}38;2;{r};{g};{b}m\"\n     except (ValueError, IndexError):\n-        return _ACCENT_ANSI_DEFAULT\n+        return _ACCENT_ANSI_DEFAULT if bold else \"\\033[38;2;184;134;11m\"\n \n \n class _SkinAwareAnsi:","@@ -1010,20 +1010,22 @@ class _SkinAwareAnsi:\n     force re-resolution after a ``/skin`` switch.\n     \"\"\"\n \n-    def __init__(self, skin_key: str, fallback_hex: str = \"#FFD700\"):\n+    def __init__(self, skin_key: str, fallback_hex: str = \"#FFD700\", *, bold: bool = False):\n         self._skin_key = skin_key\n         self._fallback_hex = fallback_hex\n+        self._bold = bold\n         self._cached: str | None = None\n \n     def __str__(self) -> str:\n         if self._cached is None:\n             try:\n                 from hermes_cli.skin_engine import get_active_skin\n-                self._cached = _hex_to_ansi_bold(\n-                    get_active_skin().get_color(self._skin_key, self._fallback_hex)\n+                self._cached = _hex_to_ansi(\n+                    get_active_skin().get_color(self._skin_key, self._fallback_hex),\n+                    bold=self._bold,\n                 )\n             except Exception:\n-                self._cached = _hex_to_ansi_bold(self._fallback_hex)\n+                self._cached = _hex_to_ansi(self._fallback_hex, bold=self._bold)\n         return self._cached\n \n     def __add__(self, other: str) -> str:","@@ -1037,7 +1039,8 @@ def reset(self) -> None:\n         self._cached = None\n \n \n-_ACCENT = _SkinAwareAnsi(\"response_border\", \"#FFD700\")\n+_ACCENT = _SkinAwareAnsi(\"response_border\", \"#FFD700\", bold=True)\n+_DIM = _SkinAwareAnsi(\"banner_dim\", \"#B8860B\")\n \n \n def _accent_hex() -> str:","@@ -6156,6 +6159,7 @@ def _handle_skin_command(self, cmd: str):\n \n         set_active_skin(new_skin)\n         _ACCENT.reset()  # Re-resolve ANSI color for the new skin\n+        _DIM.reset()     # Re-resolve dim/secondary ANSI color for the new skin\n         if save_config_value(\"display.skin\", new_skin):\n             print(f\"  Skin set to: {new_skin} (saved)\")\n         else:"]},{"file":"docs/skins/example-skin.yaml","hunks":["@@ -41,6 +41,14 @@ colors:\n   session_label: \"#DAA520\"        # Session label\n   session_border: \"#8B8682\"       # Session ID dim color\n \n+  # TUI surfaces\n+  status_bar_bg: \"#1a1a2e\"              # Status / usage bar background\n+  voice_status_bg: \"#1a1a2e\"            # Voice-mode badge background\n+  completion_menu_bg: \"#1a1a2e\"         # Completion list background\n+  completion_menu_current_bg: \"#333355\" # Active completion row background\n+  completion_menu_meta_bg: \"#1a1a2e\"    # Completion meta column background\n+  completion_menu_meta_current_bg: \"#333355\"  # Active completion meta background\n+\n # ── Spinner ─────────────────────────────────────────────────────────────────\n # Customize the animated spinner shown during API calls and tool execution.\n spinner:"]},{"file":"hermes_cli/skin_engine.py","hunks":["@@ -32,6 +32,12 @@\n       response_border: \"#FFD700\"         # Response box border (ANSI)\n       session_label: \"#DAA520\"           # Session label color\n       session_border: \"#8B8682\"          # Session ID dim color\n+      status_bar_bg: \"#1a1a2e\"          # TUI status/usage bar background\n+      voice_status_bg: \"#1a1a2e\"        # TUI voice status background\n+      completion_menu_bg: \"#1a1a2e\"      # Completion menu background\n+      completion_menu_current_bg: \"#333355\"  # Active completion row background\n+      completion_menu_meta_bg: \"#1a1a2e\"     # Completion meta column background\n+      completion_menu_meta_current_bg: \"#333355\"  # Active completion meta background\n \n     # Spinner: customize the animated spinner during API calls\n     spinner:","@@ -87,6 +93,8 @@\n - ``ares``    — Crimson/bronze war-god theme with custom spinner wings\n - ``mono``    — Clean grayscale monochrome\n - ``slate``   — Cool blue developer-focused theme\n+- ``daylight`` — Light background theme with dark text and blue accents\n+- ``warm-lightmode`` — Warm brown/gold text for light terminal backgrounds\n \n USER SKINS\n ==========","@@ -304,6 +312,80 @@ def get_branding(self, key: str, fallback: str = \"\") -> str:\n         },\n         \"tool_prefix\": \"┊\",\n     },\n+    \"daylight\": {\n+        \"name\": \"daylight\",\n+        \"description\": \"Light theme for bright terminals with dark text and cool blue accents\",\n+        \"colors\": {\n+            \"banner_border\": \"#2563EB\",\n+            \"banner_title\": \"#0F172A\",\n+            \"banner_accent\": \"#1D4ED8\",\n+            \"banner_dim\": \"#475569\",\n+            \"banner_text\": \"#111827\",\n+            \"ui_accent\": \"#2563EB\",\n+            \"ui_label\": \"#0F766E\",\n+            \"ui_ok\": \"#15803D\",\n+            \"ui_error\": \"#B91C1C\",\n+            \"ui_warn\": \"#B45309\",\n+            \"prompt\": \"#111827\",\n+            \"input_rule\": \"#93C5FD\",\n+            \"response_border\": \"#2563EB\",\n+            \"session_label\": \"#1D4ED8\",\n+            \"session_border\": \"#64748B\",\n+            \"status_bar_bg\": \"#E5EDF8\",\n+            \"voice_status_bg\": \"#E5EDF8\",\n+            \"completion_menu_bg\": \"#F8FAFC\",\n+            \"completion_menu_current_bg\": \"#DBEAFE\",\n+            \"completion_menu_meta_bg\": \"#EEF2FF\",\n+            \"completion_menu_meta_current_bg\": \"#BFDBFE\",\n+        },\n+        \"spinner\": {},\n+        \"branding\": {\n+            \"agent_name\": \"Hermes Agent\",\n+            \"welcome\": \"Welcome to Hermes Agent! Type your message or /help for commands.\",\n+            \"goodbye\": \"Goodbye! ⚕\",\n+            \"response_label\": \" ⚕ Hermes \",\n+            \"prompt_symbol\": \"❯ \",\n+            \"help_header\": \"[?] Available Commands\",\n+        },\n+        \"tool_prefix\": \"│\",\n+    },\n+    \"warm-lightmode\": {\n+        \"name\": \"warm-lightmode\",\n+        \"description\": \"Warm light mode — dark brown/gold text for light terminal backgrounds\",\n+        \"colors\": {\n+            \"banner_border\": \"#8B6914\",\n+            \"banner_title\": \"#5C3D11\",\n+            \"banner_accent\": \"#8B4513\",\n+            \"banner_dim\": \"#8B7355\",\n+            \"banner_text\": \"#2C1810\",\n+            \"ui_accent\": \"#8B4513\",\n+            \"ui_label\": \"#5C3D11\",\n+            \"ui_ok\": \"#2E7D32\",\n+            \"ui_error\": \"#C62828\",\n+            \"ui_warn\": \"#E65100\",\n+            \"prompt\": \"#2C1810\",\n+            \"input_rule\": \"#8B6914\",\n+            \"response_border\": \"#8B6914\",\n+            \"session_label\": \"#5C3D11\",\n+            \"session_border\": \"#A0845C\",\n+            \"status_bar_bg\": \"#F5F0E8\",\n+            \"voice_status_bg\": \"#F5F0E8\",\n+            \"completion_menu_bg\": \"#F5EFE0\",\n+            \"completion_menu_current_bg\": \"#E8DCC8\",\n+            \"completion_menu_meta_bg\": \"#F0E8D8\",\n+            \"completion_menu_meta_current_bg\": \"#DFCFB0\",\n+        },\n+        \"spinner\": {},\n+        \"branding\": {\n+            \"agent_name\": \"Hermes Agent\",\n+            \"welcome\": \"Welcome to Hermes Agent! Type your message or /help for commands.\",\n+            \"goodbye\": \"Goodbye! \\u2695\",\n+            \"response_label\": \" \\u2695 Hermes \",\n+            \"prompt_symbol\": \"\\u276f \",\n+            \"help_header\": \"(^_^)? Available Commands\",\n+        },\n+        \"tool_prefix\": \"\\u250a\",\n+    },\n     \"poseidon\": {\n         \"name\": \"poseidon\",\n         \"description\": \"Ocean-god theme — deep blue and seafoam\",","@@ -685,6 +767,12 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:\n     label = skin.get_color(\"ui_label\", title)\n     warn = skin.get_color(\"ui_warn\", \"#FF8C00\")\n     error = skin.get_color(\"ui_error\", \"#FF6B6B\")\n+    status_bg = skin.get_color(\"status_bar_bg\", \"#1a1a2e\")\n+    voice_bg = skin.get_color(\"voice_status_bg\", status_bg)\n+    menu_bg = skin.get_color(\"completion_menu_bg\", \"#1a1a2e\")\n+    menu_current_bg = skin.get_color(\"completion_menu_current_bg\", \"#333355\")\n+    menu_meta_bg = skin.get_color(\"completion_menu_meta_bg\", menu_bg)\n+    menu_meta_current_bg = skin.get_color(\"completion_menu_meta_current_bg\", menu_current_bg)\n \n     return {\n         \"input-area\": prompt,","@@ -692,13 +780,20 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:\n         \"prompt\": prompt,\n         \"prompt-working\": f\"{dim} italic\",\n         \"hint\": f\"{dim} italic\",\n+        \"status-bar\": f\"bg:{status_bg} {text}\",\n+        \"status-bar-strong\": f\"bg:{status_bg} {title} bold\",\n+        \"status-bar-dim\": f\"bg:{status_bg} {dim}\",\n+        \"status-bar-good\": f\"bg:{status_bg} {skin.get_color('ui_ok', '#8FBC8F')} bold\",\n+        \"status-bar-warn\": f\"bg:{status_bg} {warn} bold\",\n+        \"status-bar-bad\": f\"bg:{status_bg} {skin.get_color('banner_accent', warn)} bold\",\n+        \"status-bar-critical\": f\"bg:{status_bg} {error} bold\",\n         \"input-rule\": input_rule,\n         \"image-badge\": f\"{label} bold\",\n-        \"completion-menu\": f\"bg:#1a1a2e {text}\",\n-        \"completion-menu.completion\": f\"bg:#1a1a2e {text}\",\n-        \"completion-menu.completion.current\": f\"bg:#333355 {title}\",\n-        \"completion-menu.meta.completion\": f\"bg:#1a1a2e {dim}\",\n-        \"completion-menu.meta.completion.current\": f\"bg:#333355 {label}\",\n+        \"completion-menu\": f\"bg:{menu_bg} {text}\",\n+        \"completion-menu.completion\": f\"bg:{menu_bg} {text}\",\n+        \"completion-menu.completion.current\": f\"bg:{menu_current_bg} {title}\",\n+        \"completion-menu.meta.completion\": f\"bg:{menu_meta_bg} {dim}\",\n+        \"completion-menu.meta.completion.current\": f\"bg:{menu_meta_current_bg} {label}\",\n         \"clarify-border\": input_rule,\n         \"clarify-title\": f\"{title} bold\",\n         \"clarify-question\": f\"{text} bold\",","@@ -716,4 +811,6 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:\n         \"approval-cmd\": f\"{dim} italic\",\n         \"approval-choice\": dim,\n         \"approval-selected\": f\"{title} bold\",\n+        \"voice-status\": f\"bg:{voice_bg} {label}\",\n+        \"voice-status-recording\": f\"bg:{voice_bg} {error} bold\",\n     }"]},{"file":"tests/hermes_cli/test_skin_engine.py","hunks":["@@ -78,6 +78,28 @@ def test_slate_skin_loads(self):\n         assert skin.name == \"slate\"\n         assert skin.get_color(\"banner_title\") == \"#7eb8f6\"\n \n+    def test_daylight_skin_loads(self):\n+        from hermes_cli.skin_engine import load_skin\n+\n+        skin = load_skin(\"daylight\")\n+        assert skin.name == \"daylight\"\n+        assert skin.tool_prefix == \"│\"\n+        assert skin.get_color(\"banner_title\") == \"#0F172A\"\n+        assert skin.get_color(\"status_bar_bg\") == \"#E5EDF8\"\n+        assert skin.get_color(\"voice_status_bg\") == \"#E5EDF8\"\n+        assert skin.get_color(\"completion_menu_bg\") == \"#F8FAFC\"\n+        assert skin.get_color(\"completion_menu_current_bg\") == \"#DBEAFE\"\n+        assert skin.get_color(\"completion_menu_meta_bg\") == \"#EEF2FF\"\n+        assert skin.get_color(\"completion_menu_meta_current_bg\") == \"#BFDBFE\"\n+\n+    def test_warm_lightmode_skin_loads(self):\n+        from hermes_cli.skin_engine import load_skin\n+\n+        skin = load_skin(\"warm-lightmode\")\n+        assert skin.name == \"warm-lightmode\"\n+        assert skin.get_color(\"banner_text\") == \"#2C1810\"\n+        assert skin.get_color(\"completion_menu_bg\") == \"#F5EFE0\"\n+\n     def test_unknown_skin_falls_back_to_default(self):\n         from hermes_cli.skin_engine import load_skin\n         skin = load_skin(\"nonexistent_skin_xyz\")","@@ -114,6 +136,8 @@ def test_list_skins_includes_builtins(self):\n         assert \"ares\" in names\n         assert \"mono\" in names\n         assert \"slate\" in names\n+        assert \"daylight\" in names\n+        assert \"warm-lightmode\" in names\n         for s in skins:\n             assert \"source\" in s\n             assert s[\"source\"] == \"builtin\"","@@ -242,6 +266,15 @@ def test_prompt_toolkit_style_overrides_cover_tui_classes(self):\n             \"completion-menu.completion.current\",\n             \"completion-menu.meta.completion\",\n             \"completion-menu.meta.completion.current\",\n+            \"status-bar\",\n+            \"status-bar-strong\",\n+            \"status-bar-dim\",\n+            \"status-bar-good\",\n+            \"status-bar-warn\",\n+            \"status-bar-bad\",\n+            \"status-bar-critical\",\n+            \"voice-status\",\n+            \"voice-status-recording\",\n             \"clarify-border\",\n             \"clarify-title\",\n             \"clarify-question\",","@@ -277,3 +310,9 @@ def test_prompt_toolkit_style_overrides_use_skin_colors(self):\n         assert overrides[\"clarify-title\"] == f\"{skin.get_color('banner_title')} bold\"\n         assert overrides[\"sudo-prompt\"] == f\"{skin.get_color('ui_error')} bold\"\n         assert overrides[\"approval-title\"] == f\"{skin.get_color('ui_warn')} bold\"\n+\n+        set_active_skin(\"daylight\")\n+        skin = get_active_skin()\n+        overrides = get_prompt_toolkit_style_overrides()\n+        assert overrides[\"status-bar\"] == f\"bg:{skin.get_color('status_bar_bg')} {skin.get_color('banner_text')}\"\n+        assert overrides[\"voice-status\"] == f\"bg:{skin.get_color('voice_status_bg')} {skin.get_color('ui_label')}\""]},{"file":"website/docs/user-guide/features/skins.md","hunks":["@@ -36,6 +36,8 @@ display:\n | `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. |\n | `mono` | Monochrome — clean grayscale | `Hermes Agent` | All grays — no color. Borders are `#555555`, text is `#c9d1d9`. Ideal for minimal terminal setups or screen recordings. |\n | `slate` | Cool blue — developer-focused | `Hermes Agent` | Royal blue borders (`#4169e1`), soft blue text. Calm and professional. No custom spinner — uses default faces. |\n+| `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. |\n+| `warm-lightmode` | Warm brown/gold text for light terminal backgrounds | `Hermes Agent` | Warm parchment tones for light terminals. Dark brown text with saddle-brown accents, cream-colored status surfaces. An earthy alternative to the cooler daylight theme. |\n | `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. |\n | `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. |\n | `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 +65,12 @@ Controls all color values throughout the CLI. Values are hex color strings.\n | `response_border` | Border around the agent's response box (ANSI escape) | `#FFD700` |\n | `session_label` | Session label color | `#DAA520` |\n | `session_border` | Session ID dim border color | `#8B8682` |\n+| `status_bar_bg` | Background color for the TUI status / usage bar | `#1a1a2e` |\n+| `voice_status_bg` | Background color for the voice-mode status badge | `#1a1a2e` |\n+| `completion_menu_bg` | Background color for the completion menu list | `#1a1a2e` |\n+| `completion_menu_current_bg` | Background color for the active completion row | `#333355` |\n+| `completion_menu_meta_bg` | Background color for the completion meta column | `#1a1a2e` |\n+| `completion_menu_meta_current_bg` | Background color for the active completion meta column | `#333355` |\n \n ### Spinner (`spinner:`)\n ","@@ -129,6 +137,12 @@ colors:\n   response_border: \"#FFD700\"\n   session_label: \"#DAA520\"\n   session_border: \"#8B8682\"\n+  status_bar_bg: \"#1a1a2e\"\n+  voice_status_bg: \"#1a1a2e\"\n+  completion_menu_bg: \"#1a1a2e\"\n+  completion_menu_current_bg: \"#333355\"\n+  completion_menu_meta_bg: \"#1a1a2e\"\n+  completion_menu_meta_current_bg: \"#333355\"\n \n spinner:\n   waiting_faces:\n"]}]},{"kind":"pr","number":9808,"title":"fix(colors): readable defaults on light terminals via HERMES_THEME + COLORFGBG","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17113","number":17113},{"kind":"pr","value":"#13103","number":13103},{"kind":"pr","value":"#17113","number":17113},{"kind":"pr","value":"#17695","number":17695},{"kind":"pr","value":"#26137","number":26137},{"kind":"pr","value":"#17113","number":17113}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":11311,"title":"fix(tui): replace hardcoded yellow colors unreadable on light backgrounds","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11335","number":11335}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":11335,"title":"fix(tui): replace bright yellow with dark_goldenrod for light bg contrast","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":11536,"title":"fix(cli): auto-select daylight skin on light terminal backgrounds","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11553","number":11553},{"kind":"pr","value":"#12337","number":12337},{"kind":"pr","value":"#11300","number":11300},{"kind":"pr","value":"#4807","number":4807}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":11553,"title":"fix: add light terminal background detection","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11300","number":11300}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":12337,"title":"fix: adaptive color scheme for light terminal backgrounds (#12330)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17113","number":17113},{"kind":"pr","value":"#17113","number":17113},{"kind":"pr","value":"#26137","number":26137}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":12487,"title":"fix(tui): replace unreadable yellow colors with dark goldenrod (#11300)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":13103,"title":"fix(tui): theme-driven update-behind banner + auto-detect light terminals (#11300)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["ui-tui/src/__tests__/theme.test.ts","ui-tui/src/components/branding.tsx","ui-tui/src/theme.ts"],"fix_hunks":[{"file":"ui-tui/src/__tests__/theme.test.ts","hunks":["@@ -1,6 +1,6 @@\n import { describe, expect, it } from 'vitest'\n \n-import { DARK_THEME, DEFAULT_THEME, fromSkin, LIGHT_THEME } from '../theme.js'\n+import { DARK_THEME, DEFAULT_THEME, detectLightMode, fromSkin, LIGHT_THEME } from '../theme.js'\n \n describe('DEFAULT_THEME', () => {\n   it('has brand defaults', () => {","@@ -30,11 +30,37 @@ describe('LIGHT_THEME', () => {\n })\n \n describe('DEFAULT_THEME aliasing', () => {\n-  it('defaults to DARK_THEME when HERMES_TUI_LIGHT is unset', () => {\n+  it('defaults to DARK_THEME when nothing signals light', () => {\n     expect(DEFAULT_THEME).toBe(DARK_THEME)\n   })\n })\n \n+describe('detectLightMode', () => {\n+  it('returns false on empty env', () => {\n+    expect(detectLightMode({})).toBe(false)\n+  })\n+\n+  it('honors HERMES_TUI_LIGHT on/off', () => {\n+    expect(detectLightMode({ HERMES_TUI_LIGHT: '1' })).toBe(true)\n+    expect(detectLightMode({ HERMES_TUI_LIGHT: 'true' })).toBe(true)\n+    expect(detectLightMode({ HERMES_TUI_LIGHT: 'on' })).toBe(true)\n+    expect(detectLightMode({ HERMES_TUI_LIGHT: '0' })).toBe(false)\n+    expect(detectLightMode({ HERMES_TUI_LIGHT: 'off' })).toBe(false)\n+  })\n+\n+  it('sniffs COLORFGBG bg slots 7 and 15 as light (#11300)', () => {\n+    expect(detectLightMode({ COLORFGBG: '0;15' })).toBe(true)\n+    expect(detectLightMode({ COLORFGBG: '0;default;15' })).toBe(true)\n+    expect(detectLightMode({ COLORFGBG: '0;7' })).toBe(true)\n+    expect(detectLightMode({ COLORFGBG: '15;0' })).toBe(false)\n+    expect(detectLightMode({ COLORFGBG: '7;default;0' })).toBe(false)\n+  })\n+\n+  it('lets HERMES_TUI_LIGHT=0 override a light COLORFGBG', () => {\n+    expect(detectLightMode({ COLORFGBG: '0;15', HERMES_TUI_LIGHT: '0' })).toBe(false)\n+  })\n+})\n+\n describe('fromSkin', () => {\n   it('overrides banner colors', () => {\n     expect(fromSkin({ banner_title: '#FF0000' }, {}).color.gold).toBe('#FF0000')"]},{"file":"ui-tui/src/components/branding.tsx","hunks":["@@ -161,16 +161,16 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {\n         </Text>\n \n         {typeof info.update_behind === 'number' && info.update_behind > 0 && (\n-          <Text bold color=\"yellow\">\n+          <Text bold color={t.color.warn}>\n             ! {info.update_behind} {info.update_behind === 1 ? 'commit' : 'commits'} behind\n-            <Text bold={false} color=\"yellow\" dimColor>\n+            <Text bold={false} color={t.color.warn} dimColor>\n               {' '}\n               - run{' '}\n             </Text>\n-            <Text bold color=\"yellow\">\n+            <Text bold color={t.color.warn}>\n               {info.update_command || 'hermes update'}\n             </Text>\n-            <Text bold={false} color=\"yellow\" dimColor>\n+            <Text bold={false} color={t.color.warn} dimColor>\n               {' '}\n               to update\n             </Text>"]},{"file":"ui-tui/src/theme.ts","hunks":["@@ -171,9 +171,26 @@ export const LIGHT_THEME: Theme = {\n   bannerHero: ''\n }\n \n-const LIGHT_MODE = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_LIGHT ?? '').trim())\n+// Pick light vs dark. Explicit `HERMES_TUI_LIGHT` wins; otherwise sniff\n+// `COLORFGBG` (set by XFCE Terminal, rxvt, Terminal.app, etc.) — last field is the\n+// background ANSI index; 7/15 are the \"white\" slots most light themes emit (#11300).\n+export function detectLightMode(env: NodeJS.ProcessEnv = process.env): boolean {\n+  const explicit = (env.HERMES_TUI_LIGHT ?? '').trim().toLowerCase()\n+\n+  if (/^(?:1|true|yes|on)$/.test(explicit)) {\n+    return true\n+  }\n+\n+  if (/^(?:0|false|no|off)$/.test(explicit)) {\n+    return false\n+  }\n+\n+  const bg = Number((env.COLORFGBG ?? '').trim().split(';').at(-1))\n+\n+  return bg === 7 || bg === 15\n+}\n \n-export const DEFAULT_THEME: Theme = LIGHT_MODE ? LIGHT_THEME : DARK_THEME\n+export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME\n \n // ── Skin → Theme ─────────────────────────────────────────────────────\n \n"]}]},{"kind":"pr","number":17113,"title":"feat(tui): expand light-terminal auto-detection (HERMES_TUI_THEME, background hex)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["ui-tui/src/__tests__/theme.test.ts","ui-tui/src/theme.ts"],"fix_hunks":[{"file":"ui-tui/src/__tests__/theme.test.ts","hunks":["@@ -1,46 +1,90 @@\n-import { describe, expect, it } from 'vitest'\n-\n-import { DARK_THEME, DEFAULT_THEME, detectLightMode, fromSkin, LIGHT_THEME } from '../theme.js'\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+\n+// `theme.js` reads `process.env` at module-load to compute DEFAULT_THEME,\n+// and `fromSkin` closes over DEFAULT_THEME.  A developer shell with\n+// HERMES_TUI_THEME=light (or HERMES_TUI_BACKGROUND set to something\n+// bright) would flip the base and turn these assertions into a local-\n+// only failure.  We sterilize the relevant env vars + dynamically\n+// import the module fresh so EVERY symbol that closes over the env\n+// (DEFAULT_THEME, DARK_THEME, LIGHT_THEME, fromSkin) is loaded against\n+// a known-empty environment.\n+//\n+// `detectLightMode` takes env as an explicit arg, so it's safe to import\n+// statically — but we stay consistent and dynamic-import it too.\n+const RELEVANT_ENV = [\n+  'HERMES_TUI_LIGHT',\n+  'HERMES_TUI_THEME',\n+  'HERMES_TUI_BACKGROUND',\n+  'COLORFGBG',\n+  'TERM_PROGRAM',\n+] as const\n+\n+async function importThemeWithCleanEnv() {\n+  for (const key of RELEVANT_ENV) {\n+    vi.stubEnv(key, '')\n+  }\n+  vi.resetModules()\n+  return import('../theme.js')\n+}\n+\n+afterEach(() => {\n+  vi.unstubAllEnvs()\n+  vi.resetModules()\n+})\n \n describe('DEFAULT_THEME', () => {\n-  it('has brand defaults', () => {\n+  it('has brand defaults', async () => {\n+    const { DEFAULT_THEME } = await importThemeWithCleanEnv()\n+\n     expect(DEFAULT_THEME.brand.name).toBe('Hermes Agent')\n     expect(DEFAULT_THEME.brand.prompt).toBe('❯')\n     expect(DEFAULT_THEME.brand.tool).toBe('┊')\n   })\n \n-  it('has color palette', () => {\n+  it('has color palette', async () => {\n+    const { DEFAULT_THEME } = await importThemeWithCleanEnv()\n+\n     expect(DEFAULT_THEME.color.gold).toBe('#FFD700')\n     expect(DEFAULT_THEME.color.error).toBe('#ef5350')\n   })\n })\n \n describe('LIGHT_THEME', () => {\n-  it('avoids bright-yellow accents unreadable on white backgrounds (#11300)', () => {\n+  it('avoids bright-yellow accents unreadable on white backgrounds (#11300)', async () => {\n+    const { LIGHT_THEME } = await importThemeWithCleanEnv()\n+\n     expect(LIGHT_THEME.color.gold).not.toBe('#FFD700')\n     expect(LIGHT_THEME.color.amber).not.toBe('#FFBF00')\n     expect(LIGHT_THEME.color.dim).not.toBe('#B8860B')\n     expect(LIGHT_THEME.color.statusWarn).not.toBe('#FFD700')\n   })\n \n-  it('keeps the same shape as DARK_THEME', () => {\n+  it('keeps the same shape as DARK_THEME', async () => {\n+    const { DARK_THEME, LIGHT_THEME } = await importThemeWithCleanEnv()\n+\n     expect(Object.keys(LIGHT_THEME.color).sort()).toEqual(Object.keys(DARK_THEME.color).sort())\n     expect(LIGHT_THEME.brand).toEqual(DARK_THEME.brand)\n   })\n })\n \n describe('DEFAULT_THEME aliasing', () => {\n-  it('defaults to DARK_THEME when nothing signals light', () => {\n-    expect(DEFAULT_THEME).toBe(DARK_THEME)\n+  it('defaults to DARK_THEME when nothing signals light', async () => {\n+    const { DEFAULT_THEME, DARK_THEME: DARK } = await importThemeWithCleanEnv()\n+\n+    expect(DEFAULT_THEME).toBe(DARK)\n   })\n })\n \n describe('detectLightMode', () => {\n-  it('returns false on empty env', () => {\n+  it('returns false on empty env', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n     expect(detectLightMode({})).toBe(false)\n   })\n \n-  it('honors HERMES_TUI_LIGHT on/off', () => {\n+  it('honors HERMES_TUI_LIGHT on/off', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n     expect(detectLightMode({ HERMES_TUI_LIGHT: '1' })).toBe(true)\n     expect(detectLightMode({ HERMES_TUI_LIGHT: 'true' })).toBe(true)\n     expect(detectLightMode({ HERMES_TUI_LIGHT: 'on' })).toBe(true)","@@ -48,7 +92,9 @@ describe('detectLightMode', () => {\n     expect(detectLightMode({ HERMES_TUI_LIGHT: 'off' })).toBe(false)\n   })\n \n-  it('sniffs COLORFGBG bg slots 7 and 15 as light (#11300)', () => {\n+  it('sniffs COLORFGBG bg slots 7 and 15 as light (#11300)', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n     expect(detectLightMode({ COLORFGBG: '0;15' })).toBe(true)\n     expect(detectLightMode({ COLORFGBG: '0;default;15' })).toBe(true)\n     expect(detectLightMode({ COLORFGBG: '0;7' })).toBe(true)","@@ -56,38 +102,119 @@ describe('detectLightMode', () => {\n     expect(detectLightMode({ COLORFGBG: '7;default;0' })).toBe(false)\n   })\n \n-  it('lets HERMES_TUI_LIGHT=0 override a light COLORFGBG', () => {\n+  it('falls through on malformed COLORFGBG with empty/non-numeric trailing field', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+    // `Number('')` is 0, so `'15;'` would have been read as bg=0\n+    // (authoritative dark) and incorrectly blocked TERM_PROGRAM.\n+    // The strict /^\\d+$/ guard makes these fall through instead.\n+    const allowList = new Set(['Apple_Terminal'])\n+\n+    expect(detectLightMode({ COLORFGBG: '15;', TERM_PROGRAM: 'Apple_Terminal' }, allowList)).toBe(true)\n+    expect(detectLightMode({ COLORFGBG: 'default;default', TERM_PROGRAM: 'Apple_Terminal' }, allowList)).toBe(true)\n+    // Without an allow-list match, fall-through still defaults to dark.\n+    expect(detectLightMode({ COLORFGBG: '15;' })).toBe(false)\n+  })\n+\n+  it('lets HERMES_TUI_LIGHT=0 override a light COLORFGBG', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n     expect(detectLightMode({ COLORFGBG: '0;15', HERMES_TUI_LIGHT: '0' })).toBe(false)\n   })\n+\n+  it('honors HERMES_TUI_THEME=light/dark as a symmetric explicit override', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n+    expect(detectLightMode({ HERMES_TUI_THEME: 'light' })).toBe(true)\n+    expect(detectLightMode({ HERMES_TUI_THEME: 'dark' })).toBe(false)\n+    expect(detectLightMode({ COLORFGBG: '0;15', HERMES_TUI_THEME: 'dark' })).toBe(false)\n+    expect(detectLightMode({ COLORFGBG: '15;0', HERMES_TUI_THEME: 'light' })).toBe(true)\n+  })\n+\n+  it('uses HERMES_TUI_BACKGROUND luminance when COLORFGBG is missing', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#ffffff' })).toBe(true)\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#000000' })).toBe(false)\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#1e1e1e' })).toBe(false)\n+    // Three-char hex normalises like CSS.\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fff' })).toBe(true)\n+    // Garbage falls through to the default-dark path.\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: 'not-a-colour' })).toBe(false)\n+  })\n+\n+  it('rejects partially-invalid hex instead of silently truncating', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+    // `parseInt('fffgff'.slice(2,4), 16)` would return 15 — the strict\n+    // regex must reject these inputs so they fall through to default-\n+    // dark instead of producing a false-positive light reading.\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fffgff' })).toBe(false)\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: 'ffggff' })).toBe(false)\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#xyz' })).toBe(false)\n+    // Wrong length also rejected (no implicit padding/truncation).\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fffff' })).toBe(false)\n+    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fffffff' })).toBe(false)\n+  })\n+\n+  it('treats COLORFGBG as authoritative when present so it dominates the TERM_PROGRAM allow-list', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+    // Inject a light-default allow-list so the precedence test is\n+    // meaningful even though the production allow-list is empty.\n+    const allowList = new Set(['Apple_Terminal'])\n+\n+    // Sanity: the allow-list alone WOULD turn this terminal light.\n+    expect(detectLightMode({ TERM_PROGRAM: 'Apple_Terminal' }, allowList)).toBe(true)\n+\n+    // Dark COLORFGBG must beat the allow-list.\n+    expect(\n+      detectLightMode({ COLORFGBG: '15;0', TERM_PROGRAM: 'Apple_Terminal' }, allowList),\n+    ).toBe(false)\n+  })\n })\n \n describe('fromSkin', () => {\n-  it('overrides banner colors', () => {\n+  // `fromSkin` closes over DEFAULT_THEME (which is env-derived), so we\n+  // must dynamic-import it after sterilizing env — otherwise an ambient\n+  // HERMES_TUI_THEME=light would flip the base palette and make these\n+  // assertions order-dependent on the developer's shell.\n+\n+  it('overrides banner colors', async () => {\n+    const { fromSkin } = await importThemeWithCleanEnv()\n+\n     expect(fromSkin({ banner_title: '#FF0000' }, {}).color.gold).toBe('#FF0000')\n   })\n \n-  it('preserves unset colors', () => {\n+  it('preserves unset colors', async () => {\n+    const { DEFAULT_THEME, fromSkin } = await importThemeWithCleanEnv()\n+\n     expect(fromSkin({ banner_title: '#FF0000' }, {}).color.amber).toBe(DEFAULT_THEME.color.amber)\n   })\n \n-  it('overrides branding', () => {\n+  it('overrides branding', async () => {\n+    const { fromSkin } = await importThemeWithCleanEnv()\n     const { brand } = fromSkin({}, { agent_name: 'TestBot', prompt_symbol: '$' })\n+\n     expect(brand.name).toBe('TestBot')\n     expect(brand.prompt).toBe('$')\n   })\n \n-  it('defaults for empty skin', () => {\n+  it('defaults for empty skin', async () => {\n+    const { DEFAULT_THEME, fromSkin } = await importThemeWithCleanEnv()\n+\n     expect(fromSkin({}, {}).color).toEqual(DEFAULT_THEME.color)\n     expect(fromSkin({}, {}).brand.icon).toBe(DEFAULT_THEME.brand.icon)\n   })\n \n-  it('passes banner logo/hero', () => {\n+  it('passes banner logo/hero', async () => {\n+    const { fromSkin } = await importThemeWithCleanEnv()\n+\n     expect(fromSkin({}, {}, 'LOGO', 'HERO').bannerLogo).toBe('LOGO')\n     expect(fromSkin({}, {}, 'LOGO', 'HERO').bannerHero).toBe('HERO')\n   })\n \n-  it('maps ui_ color keys + cascades to status', () => {\n+  it('maps ui_ color keys + cascades to status', async () => {\n+    const { fromSkin } = await importThemeWithCleanEnv()\n     const { color } = fromSkin({ ui_ok: '#008000' }, {})\n+\n     expect(color.ok).toBe('#008000')\n     expect(color.statusGood).toBe('#008000')\n   })"]},{"file":"ui-tui/src/theme.ts","hunks":["@@ -179,23 +179,129 @@ export const LIGHT_THEME: Theme = {\n   bannerHero: ''\n }\n \n-// Pick light vs dark. Explicit `HERMES_TUI_LIGHT` wins; otherwise sniff\n-// `COLORFGBG` (set by XFCE Terminal, rxvt, Terminal.app, etc.) — last field is the\n-// background ANSI index; 7/15 are the \"white\" slots most light themes emit (#11300).\n-export function detectLightMode(env: NodeJS.ProcessEnv = process.env): boolean {\n-  const explicit = (env.HERMES_TUI_LIGHT ?? '').trim().toLowerCase()\n+const TRUE_RE = /^(?:1|true|yes|on)$/\n+const FALSE_RE = /^(?:0|false|no|off)$/\n+\n+// Reserved for future TERM_PROGRAM-based heuristics.  Empty by default:\n+// most modern terminals (Ghostty, Warp, iTerm2, Apple_Terminal) ship a\n+// dark profile out of the box, so guessing wrong here is more annoying\n+// than missing a light user — light users can always set\n+// `HERMES_TUI_LIGHT=1` or `HERMES_TUI_THEME=light`.\n+const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>()\n+\n+// Best-effort RGB → luminance check.  Currently only accepts a 3- or\n+// 6-digit hex value (with or without a leading `#`); the env var name\n+// `HERMES_TUI_BACKGROUND` is intentionally generic so a future OSC11\n+// query helper can cache its answer there too, but additional formats\n+// (rgb()/hsl()/named colours) would need explicit parsing here first.\n+const LUMA_LIGHT_THRESHOLD = 0.6\n+\n+// Strict allow-list: parseInt(..., 16) silently truncates at the first\n+// non-hex character (e.g. `fffgff` would parse as `fff` and yield a\n+// false-positive \"white\" reading), so reject anything that doesn't match\n+// the canonical 3- or 6-digit shape up front.\n+const HEX_3_RE = /^[0-9a-f]{3}$/\n+const HEX_6_RE = /^[0-9a-f]{6}$/\n+\n+function backgroundLuminance(raw: string): null | number {\n+  const v = raw.trim().toLowerCase()\n+\n+  if (!v) {\n+    return null\n+  }\n+\n+  const hex = v.startsWith('#') ? v.slice(1) : v\n+  const rgb = HEX_6_RE.test(hex)\n+    ? [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]\n+    : HEX_3_RE.test(hex)\n+      ? [parseInt(hex[0]! + hex[0]!, 16), parseInt(hex[1]! + hex[1]!, 16), parseInt(hex[2]! + hex[2]!, 16)]\n+      : null\n+\n+  if (!rgb) {\n+    return null\n+  }\n+\n+  // Rec. 709 luma — close enough for \"is this background bright\".\n+  return (0.2126 * rgb[0]! + 0.7152 * rgb[1]! + 0.0722 * rgb[2]!) / 255\n+}\n+\n+// Pick light vs dark with ordered, explainable signals (#11300):\n+//\n+//   1. `HERMES_TUI_LIGHT` boolean — `1`/`true`/`yes`/`on` → light;\n+//      `0`/`false`/`no`/`off` → dark.  Either explicit value wins\n+//      regardless of any later signal.\n+//   2. `HERMES_TUI_THEME` named override — `light` / `dark` win over\n+//      every signal below.\n+//   3. `HERMES_TUI_BACKGROUND` hex hint (3- or 6-digit) — luminance\n+//      ≥ LUMA_LIGHT_THRESHOLD → light.\n+//   4. `COLORFGBG` last field — XFCE / rxvt / Terminal.app emit\n+//      slot 7 or 15 on light profiles; 0–15 ranges are otherwise\n+//      treated as authoritatively dark so the TERM_PROGRAM\n+//      allow-list below cannot override an explicit dark profile.\n+//   5. `TERM_PROGRAM` light-default allow-list (currently empty).\n+//\n+// Anything we can't decide stays dark — the default Hermes palette\n+// is the dark one.\n+export function detectLightMode(\n+  env: NodeJS.ProcessEnv = process.env,\n+  // Injectable so tests can prove the COLORFGBG-over-TERM_PROGRAM\n+  // precedence rule even though the production allow-list is empty.\n+  lightDefaultTermPrograms: ReadonlySet<string> = LIGHT_DEFAULT_TERM_PROGRAMS,\n+): boolean {\n+  const lightFlag = (env.HERMES_TUI_LIGHT ?? '').trim().toLowerCase()\n+\n+  if (TRUE_RE.test(lightFlag)) {\n+    return true\n+  }\n+\n+  if (FALSE_RE.test(lightFlag)) {\n+    return false\n+  }\n+\n+  const themeFlag = (env.HERMES_TUI_THEME ?? '').trim().toLowerCase()\n \n-  if (/^(?:1|true|yes|on)$/.test(explicit)) {\n+  if (themeFlag === 'light') {\n     return true\n   }\n \n-  if (/^(?:0|false|no|off)$/.test(explicit)) {\n+  if (themeFlag === 'dark') {\n     return false\n   }\n \n-  const bg = Number((env.COLORFGBG ?? '').trim().split(';').at(-1))\n+  const bgHint = backgroundLuminance(env.HERMES_TUI_BACKGROUND ?? '')\n+\n+  if (bgHint !== null) {\n+    return bgHint >= LUMA_LIGHT_THRESHOLD\n+  }\n+\n+  const colorfgbg = (env.COLORFGBG ?? '').trim()\n+\n+  if (colorfgbg) {\n+    // Validate as a decimal integer before coercing — `Number('')` is 0,\n+    // so a malformed `COLORFGBG='15;'` would otherwise look like an\n+    // authoritative dark slot and incorrectly block the TERM_PROGRAM\n+    // allow-list.  Anything that isn't pure digits falls through.\n+    const lastField = colorfgbg.split(';').at(-1) ?? ''\n+\n+    if (/^\\d+$/.test(lastField)) {\n+      const bg = Number(lastField)\n+\n+      if (bg === 7 || bg === 15) {\n+        return true\n+      }\n+\n+      // Slots 0–6 and 8–14 are the dark half of the 0–15 ANSI range.\n+      // When COLORFGBG is set we trust it as authoritative — a non-light\n+      // value here shouldn't get overridden by the TERM_PROGRAM allow-list.\n+      if (bg >= 0 && bg < 16) {\n+        return false\n+      }\n+    }\n+  }\n+\n+  const termProgram = (env.TERM_PROGRAM ?? '').trim()\n \n-  return bg === 7 || bg === 15\n+  return lightDefaultTermPrograms.has(termProgram)\n }\n \n export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME\n"]}]},{"kind":"pr","number":17695,"title":"fix(tui): normalize legacy Terminal.app colors","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["ui-tui/packages/hermes-ink/src/ink/colorize.test.ts","ui-tui/packages/hermes-ink/src/ink/colorize.ts","ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts","ui-tui/packages/hermes-ink/src/ink/components/Text.tsx","ui-tui/src/__tests__/forceTruecolor.test.ts","ui-tui/src/__tests__/theme.test.ts","ui-tui/src/entry.tsx","ui-tui/src/lib/forceTruecolor.ts","ui-tui/src/theme.ts"],"fix_hunks":[{"file":"ui-tui/packages/hermes-ink/src/ink/colorize.test.ts","hunks":["@@ -0,0 +1,60 @@\n+import { describe, expect, it } from 'vitest'\n+\n+import {\n+  CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE,\n+  richEightBitColorNumber,\n+  shouldUseRichEightBitDowngradeForLegacyAppleTerminal\n+} from './colorize.js'\n+\n+describe('shouldUseRichEightBitDowngradeForLegacyAppleTerminal', () => {\n+  it('memoizes the current process decision for render hot paths', () => {\n+    expect(typeof CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE).toBe('boolean')\n+  })\n+\n+  it('uses Rich-compatible 256-color downgrade on legacy Apple Terminal', () => {\n+    expect(\n+      shouldUseRichEightBitDowngradeForLegacyAppleTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv, 2)\n+    ).toBe(true)\n+  })\n+\n+  it('normalizes Apple Terminal names before matching', () => {\n+    expect(\n+      shouldUseRichEightBitDowngradeForLegacyAppleTerminal({ TERM_PROGRAM: ' Apple_Terminal ' } as NodeJS.ProcessEnv, 2)\n+    ).toBe(true)\n+  })\n+\n+  it('does not rewrite when Apple Terminal advertises truecolor', () => {\n+    expect(\n+      shouldUseRichEightBitDowngradeForLegacyAppleTerminal(\n+        { COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv,\n+        3\n+      )\n+    ).toBe(false)\n+  })\n+\n+  it('does not override explicit color environment choices', () => {\n+    expect(\n+      shouldUseRichEightBitDowngradeForLegacyAppleTerminal(\n+        { FORCE_COLOR: '2', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv,\n+        2\n+      )\n+    ).toBe(false)\n+    expect(\n+      shouldUseRichEightBitDowngradeForLegacyAppleTerminal(\n+        { HERMES_TUI_TRUECOLOR: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv,\n+        3\n+      )\n+    ).toBe(false)\n+  })\n+})\n+\n+describe('richEightBitColorNumber', () => {\n+  it('matches Rich downgrade output for default Hermes skin colors', () => {\n+    expect(richEightBitColorNumber(0xff, 0xd7, 0x00)).toBe(220)\n+    expect(richEightBitColorNumber(0xff, 0xbf, 0x00)).toBe(214)\n+    expect(richEightBitColorNumber(0xcd, 0x7f, 0x32)).toBe(173)\n+    expect(richEightBitColorNumber(0xb8, 0x86, 0x0b)).toBe(136)\n+    expect(richEightBitColorNumber(0xff, 0xf8, 0xdc)).toBe(230)\n+  })\n+})\n+"]},{"file":"ui-tui/packages/hermes-ink/src/ink/colorize.ts","hunks":["@@ -28,6 +28,39 @@ function boostChalkLevelForXtermJs(): boolean {\n   return false\n }\n \n+export function shouldUseRichEightBitDowngradeForLegacyAppleTerminal(\n+  env: NodeJS.ProcessEnv = process.env,\n+  level = chalk.level\n+): boolean {\n+  const termProgram = (env.TERM_PROGRAM ?? '').trim()\n+  const truecolorOverride = /^(?:1|true|yes|on)$/i.test((env.HERMES_TUI_TRUECOLOR ?? '').trim())\n+  const advertisesTruecolor = /^(?:truecolor|24bit)$/i.test((env.COLORTERM ?? '').trim())\n+\n+  return termProgram === 'Apple_Terminal' && !truecolorOverride && !advertisesTruecolor && !('FORCE_COLOR' in env) && level === 2\n+}\n+\n+export function richEightBitColorNumber(red: number, green: number, blue: number): number {\n+  const rn = red / 255\n+  const gn = green / 255\n+  const bn = blue / 255\n+  const max = Math.max(rn, gn, bn)\n+  const min = Math.min(rn, gn, bn)\n+  const lightness = (max + min) / 2\n+  const saturation = max === min ? 0 : lightness > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min)\n+\n+  if (saturation < 0.15) {\n+    const gray = Math.round(lightness * 25)\n+\n+    return gray === 0 ? 16 : gray === 25 ? 231 : 231 + gray\n+  }\n+\n+  const sixRed = red < 95 ? red / 95 : 1 + (red - 95) / 40\n+  const sixGreen = green < 95 ? green / 95 : 1 + (green - 95) / 40\n+  const sixBlue = blue < 95 ? blue / 95 : 1 + (blue - 95) / 40\n+\n+  return 16 + 36 * Math.round(sixRed) + 6 * Math.round(sixGreen) + Math.round(sixBlue)\n+}\n+\n /**\n  * tmux parses truecolor SGR (\\e[48;2;r;g;bm) into its cell buffer correctly,\n  * but its client-side emitter only re-emits truecolor to the outer terminal if","@@ -58,15 +91,17 @@ function clampChalkLevelForTmux(): boolean {\n }\n \n // Computed once at module load — terminal/tmux environment doesn't change mid-session.\n-// Order matters: boost first so the tmux clamp can re-clamp if tmux is running\n-// inside a VS Code terminal. Exported for debugging — tree-shaken if unused.\n+// Order matters: boost first; then tmux can still clamp RGB to 256.\n+// Exported for debugging — tree-shaken if unused.\n export const CHALK_BOOSTED_FOR_XTERMJS = boostChalkLevelForXtermJs()\n export const CHALK_CLAMPED_FOR_TMUX = clampChalkLevelForTmux()\n+export const CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE = shouldUseRichEightBitDowngradeForLegacyAppleTerminal()\n \n export type ColorType = 'foreground' | 'background'\n \n const RGB_REGEX = /^rgb\\(\\s?(\\d+),\\s?(\\d+),\\s?(\\d+)\\s?\\)$/\n const ANSI_REGEX = /^ansi256\\(\\s?(\\d+)\\s?\\)$/\n+const HEX_REGEX = /^#[0-9a-fA-F]{6}$/\n \n export const colorize = (str: string, color: string | undefined, type: ColorType): string => {\n   if (!color) {","@@ -128,6 +163,16 @@ export const colorize = (str: string, color: string | undefined, type: ColorType\n   }\n \n   if (color.startsWith('#')) {\n+    if (HEX_REGEX.test(color) && CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE) {\n+      const value = Number.parseInt(color.slice(1), 16)\n+      const red = (value >> 16) & 0xff\n+      const green = (value >> 8) & 0xff\n+      const blue = value & 0xff\n+      const ansi = richEightBitColorNumber(red, green, blue)\n+\n+      return type === 'foreground' ? chalk.ansi256(ansi)(str) : chalk.bgAnsi256(ansi)(str)\n+    }\n+\n     return type === 'foreground' ? chalk.hex(color)(str) : chalk.bgHex(color)(str)\n   }\n ","@@ -154,6 +199,12 @@ export const colorize = (str: string, color: string | undefined, type: ColorType\n     const secondValue = Number(matches[2])\n     const thirdValue = Number(matches[3])\n \n+    if (CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE) {\n+      const ansi = richEightBitColorNumber(firstValue, secondValue, thirdValue)\n+\n+      return type === 'foreground' ? chalk.ansi256(ansi)(str) : chalk.bgAnsi256(ansi)(str)\n+    }\n+\n     return type === 'foreground'\n       ? chalk.rgb(firstValue, secondValue, thirdValue)(str)\n       : chalk.bgRgb(firstValue, secondValue, thirdValue)(str)"]},{"file":"ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts","hunks":["@@ -1,18 +1,38 @@\n import { describe, expect, it } from 'vitest'\n \n-import { shouldUseAnsiDim } from './Text.js'\n+import { dimColorFallback, shouldUseAnsiDim } from './Text.js'\n \n describe('shouldUseAnsiDim', () => {\n   it('disables ANSI dim on VTE terminals by default', () => {\n     expect(shouldUseAnsiDim({ VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(false)\n   })\n \n+  it('disables ANSI dim on Apple Terminal by default', () => {\n+    expect(shouldUseAnsiDim({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)\n+  })\n+\n   it('keeps ANSI dim enabled elsewhere by default', () => {\n     expect(shouldUseAnsiDim({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true)\n   })\n \n   it('honors explicit env override', () => {\n     expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(true)\n+    expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(true)\n     expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '0' } as NodeJS.ProcessEnv)).toBe(false)\n   })\n })\n+\n+describe('dimColorFallback', () => {\n+  it('renders Apple Terminal dim as muted gray by default', () => {\n+    expect(dimColorFallback({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe('#6B7280')\n+  })\n+\n+  it('normalizes Apple Terminal names before matching', () => {\n+    expect(dimColorFallback({ TERM_PROGRAM: ' Apple_Terminal ' } as NodeJS.ProcessEnv)).toBe('#6B7280')\n+  })\n+\n+  it('does not apply when dim is explicitly configured', () => {\n+    expect(dimColorFallback({ HERMES_TUI_DIM: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBeUndefined()\n+    expect(dimColorFallback({ HERMES_TUI_DIM: '0', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBeUndefined()\n+  })\n+})"]},{"file":"ui-tui/packages/hermes-ink/src/ink/components/Text.tsx","hunks":["@@ -6,6 +6,7 @@ import type { Color, Styles } from '../styles.js'\n \n const ENV_ON_RE = /^(?:1|true|yes|on)$/i\n const ENV_OFF_RE = /^(?:0|false|no|off)$/i\n+const LEGACY_APPLE_DIM_COLOR: Color = '#6B7280'\n type BaseProps = {\n   /**\n    * Change text color. Accepts a raw color value (rgb, hex, ansi).","@@ -76,9 +77,23 @@ export function shouldUseAnsiDim(env: NodeJS.ProcessEnv = process.env): boolean\n     return false\n   }\n \n+  if ((env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal') {\n+    return false\n+  }\n+\n   return !env.VTE_VERSION\n }\n \n+export function dimColorFallback(env: NodeJS.ProcessEnv = process.env): Color | undefined {\n+  const override = (env.HERMES_TUI_DIM ?? '').trim()\n+\n+  if (ENV_ON_RE.test(override) || ENV_OFF_RE.test(override)) {\n+    return undefined\n+  }\n+\n+  return (env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal' ? LEGACY_APPLE_DIM_COLOR : undefined\n+}\n+\n const memoizedStylesForWrap: Record<NonNullable<Styles['textWrap']>, Styles> = {\n   wrap: {\n     flexGrow: 0,","@@ -161,6 +176,7 @@ export default function Text(t0: Props) {\n   const inverse = t4 === undefined ? false : t4\n   const wrap = t5 === undefined ? 'wrap' : t5\n   const effectiveDim = dim && shouldUseAnsiDim()\n+  const effectiveColor = dim && !effectiveDim ? (color ?? dimColorFallback()) : color\n \n   if (children === undefined || children === null) {\n     return null","@@ -168,11 +184,11 @@ export default function Text(t0: Props) {\n \n   let t6\n \n-  if ($[0] !== color) {\n-    t6 = color && {\n-      color\n+  if ($[0] !== effectiveColor) {\n+    t6 = effectiveColor && {\n+      color: effectiveColor\n     }\n-    $[0] = color\n+    $[0] = effectiveColor\n     $[1] = t6\n   } else {\n     t6 = $[1]"]},{"file":"ui-tui/src/__tests__/forceTruecolor.test.ts","hunks":["@@ -1,6 +1,7 @@\n import { describe, expect, it } from 'vitest'\n \n-const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR'] as const\n+const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR', 'TERM', 'TERM_PROGRAM'] as const\n+let importId = 0\n \n async function withCleanEnv(setup: () => void, body: () => Promise<void>) {\n   const saved: Record<string, string | undefined> = {}","@@ -25,11 +26,39 @@ async function withCleanEnv(setup: () => void, body: () => Promise<void>) {\n }\n \n describe('forceTruecolor', () => {\n-  it('sets COLORTERM=truecolor and FORCE_COLOR=3 when unset', async () => {\n+  it('does not force truecolor by default', async () => {\n     await withCleanEnv(\n       () => {},\n       async () => {\n-        await import('../lib/forceTruecolor.js?t=' + Date.now())\n+        await import('../lib/forceTruecolor.js?t=default-' + importId++)\n+        expect(process.env.COLORTERM).toBeUndefined()\n+        expect(process.env.FORCE_COLOR).toBeUndefined()\n+      }\n+    )\n+  })\n+\n+  it('does not infer truecolor from Apple Terminal on pre-Tahoe macOS', async () => {\n+    await withCleanEnv(\n+      () => {\n+        process.env.TERM_PROGRAM = 'Apple_Terminal'\n+        process.env.TERM = 'xterm-256color'\n+      },\n+      async () => {\n+        const mod = await import('../lib/forceTruecolor.js?t=apple-' + importId++)\n+        expect(mod.shouldForceTruecolor({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false)\n+        expect(process.env.COLORTERM).toBeUndefined()\n+        expect(process.env.FORCE_COLOR).toBeUndefined()\n+      }\n+    )\n+  })\n+\n+  it('sets COLORTERM=truecolor and FORCE_COLOR=3 when explicitly enabled', async () => {\n+    await withCleanEnv(\n+      () => {\n+        process.env.HERMES_TUI_TRUECOLOR = '1'\n+      },\n+      async () => {\n+        await import('../lib/forceTruecolor.js?t=enabled-' + importId++)\n         expect(process.env.COLORTERM).toBe('truecolor')\n         expect(process.env.FORCE_COLOR).toBe('3')\n       }","@@ -40,9 +69,10 @@ describe('forceTruecolor', () => {\n     await withCleanEnv(\n       () => {\n         process.env.HERMES_TUI_TRUECOLOR = '0'\n+        process.env.TERM_PROGRAM = 'Apple_Terminal'\n       },\n       async () => {\n-        await import('../lib/forceTruecolor.js?t=optout-' + Date.now())\n+        await import('../lib/forceTruecolor.js?t=optout-' + importId++)\n         expect(process.env.COLORTERM).toBeUndefined()\n         expect(process.env.FORCE_COLOR).toBeUndefined()\n       }","@@ -53,12 +83,41 @@ describe('forceTruecolor', () => {\n     await withCleanEnv(\n       () => {\n         process.env.NO_COLOR = '1'\n+        process.env.HERMES_TUI_TRUECOLOR = '1'\n       },\n       async () => {\n-        await import('../lib/forceTruecolor.js?t=no-color-' + Date.now())\n+        await import('../lib/forceTruecolor.js?t=no-color-' + importId++)\n         expect(process.env.COLORTERM).toBeUndefined()\n         expect(process.env.FORCE_COLOR).toBeUndefined()\n       }\n     )\n   })\n+\n+  it('respects existing FORCE_COLOR unless Hermes truecolor is explicit', async () => {\n+    await withCleanEnv(\n+      () => {\n+        process.env.FORCE_COLOR = ''\n+      },\n+      async () => {\n+        const mod = await import('../lib/forceTruecolor.js?t=force-color-' + importId++)\n+        expect(mod.shouldForceTruecolor(process.env)).toBe(false)\n+        expect(process.env.COLORTERM).toBeUndefined()\n+        expect(process.env.FORCE_COLOR).toBe('')\n+      }\n+    )\n+  })\n+\n+  it('lets explicit Hermes truecolor override existing FORCE_COLOR', async () => {\n+    await withCleanEnv(\n+      () => {\n+        process.env.FORCE_COLOR = '0'\n+        process.env.HERMES_TUI_TRUECOLOR = '1'\n+      },\n+      async () => {\n+        await import('../lib/forceTruecolor.js?t=explicit-force-' + importId++)\n+        expect(process.env.COLORTERM).toBe('truecolor')\n+        expect(process.env.FORCE_COLOR).toBe('3')\n+      }\n+    )\n+  })\n })"]},{"file":"ui-tui/src/__tests__/theme.test.ts","hunks":["@@ -16,12 +16,13 @@ const RELEVANT_ENV = [\n   'HERMES_TUI_THEME',\n   'HERMES_TUI_BACKGROUND',\n   'COLORFGBG',\n+  'COLORTERM',\n   'TERM_PROGRAM'\n ] as const\n \n-async function importThemeWithCleanEnv() {\n+async function importThemeWithEnv(env: Partial<Record<(typeof RELEVANT_ENV)[number], string>> = {}) {\n   for (const key of RELEVANT_ENV) {\n-    vi.stubEnv(key, '')\n+    vi.stubEnv(key, env[key] ?? '')\n   }\n \n   vi.resetModules()","@@ -29,6 +30,10 @@ async function importThemeWithCleanEnv() {\n   return import('../theme.js')\n }\n \n+async function importThemeWithCleanEnv() {\n+  return importThemeWithEnv()\n+}\n+\n afterEach(() => {\n   vi.unstubAllEnvs()\n   vi.resetModules()","@@ -84,6 +89,12 @@ describe('detectLightMode', () => {\n     expect(detectLightMode({})).toBe(false)\n   })\n \n+  it('defaults Apple Terminal to light when no stronger signal is present', async () => {\n+    const { detectLightMode } = await importThemeWithCleanEnv()\n+\n+    expect(detectLightMode({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(true)\n+  })\n+\n   it('honors HERMES_TUI_LIGHT on/off', async () => {\n     const { detectLightMode } = await importThemeWithCleanEnv()\n ","@@ -159,8 +170,8 @@ describe('detectLightMode', () => {\n \n   it('treats COLORFGBG as authoritative when present so it dominates the TERM_PROGRAM allow-list', async () => {\n     const { detectLightMode } = await importThemeWithCleanEnv()\n-    // Inject a light-default allow-list so the precedence test is\n-    // meaningful even though the production allow-list is empty.\n+    // Injecting the allow-list keeps this precedence rule explicit even if\n+    // production defaults change.\n     const allowList = new Set(['Apple_Terminal'])\n \n     // Sanity: the allow-list alone WOULD turn this terminal light.","@@ -221,6 +232,40 @@ describe('fromSkin', () => {\n     expect(fromSkin({}, {}).brand.icon).toBe(DEFAULT_THEME.brand.icon)\n   })\n \n+  it('normalizes non-banner foregrounds on light Apple Terminal', async () => {\n+    const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: 'Apple_Terminal' })\n+\n+    const theme = fromSkin({\n+      banner_accent: '#FFBF00',\n+      banner_border: '#CD7F32',\n+      banner_dim: '#B8860B',\n+      banner_text: '#FFF8DC',\n+      banner_title: '#FFD700',\n+      prompt: '#FFF8DC'\n+    }, {})\n+\n+    expect(theme.color.primary).toBe('#FFD700')\n+    expect(theme.color.accent).toBe('#FFBF00')\n+    expect(theme.color.border).toBe('#CD7F32')\n+    expect(theme.color.muted).toBe('ansi256(245)')\n+    expect(theme.color.text).toBe('ansi256(136)')\n+    expect(theme.color.prompt).toBe('ansi256(136)')\n+  })\n+\n+  it('does not normalize light Apple Terminal when truecolor is advertised', async () => {\n+    const { fromSkin } = await importThemeWithEnv({ COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' })\n+    const theme = fromSkin({ banner_text: '#FFF8DC' }, {})\n+\n+    expect(theme.color.text).toBe('#FFF8DC')\n+  })\n+\n+  it('normalizes Apple Terminal names before matching', async () => {\n+    const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: ' Apple_Terminal ' })\n+    const theme = fromSkin({ banner_text: '#FFF8DC' }, {})\n+\n+    expect(theme.color.text).toBe('ansi256(136)')\n+  })\n+\n   it('passes banner logo/hero', async () => {\n     const { fromSkin } = await importThemeWithCleanEnv()\n "]},{"file":"ui-tui/src/entry.tsx","hunks":["@@ -1,8 +1,6 @@\n #!/usr/bin/env -S node --max-old-space-size=8192 --expose-gc\n-// Must be first import — mutates process.env.FORCE_COLOR / COLORTERM before\n-// any chalk / supports-color import so the banner gradient renders in\n-// truecolor instead of being downsampled to 256-color (which collapses\n-// gold #FFD700 and amber #FFBF00 to the same slot).\n+// Must be first import. If the user explicitly opts into truecolor, this\n+// nudges chalk / supports-color before either package is initialized.\n import './lib/forceTruecolor.js'\n \n import type { FrameEvent } from '@hermes/ink'"]},{"file":"ui-tui/src/lib/forceTruecolor.ts","hunks":["@@ -1,27 +1,25 @@\n /**\n- * Force 24-bit truecolor output before any chalk / supports-color import.\n+ * Targeted 24-bit truecolor override before chalk / supports-color imports.\n  *\n- * Why this exists:\n- *   The base CLI (Python/Rich) emits banner colors as truecolor ANSI\n- *   (`\\033[38;2;R;G;Bm`). The TUI renders through Ink → chalk, whose\n- *   supports-color auto-detection defaults to 256-color on macOS Terminal.app\n- *   and any terminal that does NOT set `COLORTERM=truecolor`. In 256-color\n- *   mode, chalk downsamples `#FFD700` (gold) and `#FFBF00` (amber) to the\n- *   *same* xterm-256 palette slot (220) — collapsing the banner gradient\n- *   into a single flat yellow band. The bronze and dim rows also lose\n- *   contrast against each other.\n- *\n- *   Terminal.app (macOS 12+), iTerm2, kitty, Alacritty, VS Code, Cursor,\n- *   and WezTerm all render truecolor correctly. The few that don't\n- *   (ancient xterm, some CI environments) can set `HERMES_TUI_TRUECOLOR=0`\n- *   to opt out.\n- *\n- * This MUST run before any `chalk` or `supports-color` import. supports-color\n- * caches its level on first load, so nudging env vars after that point has\n- * no effect.\n+ * macOS Terminal.app before Tahoe 26 does not support RGB SGR, so do not\n+ * infer truecolor from TERM_PROGRAM=Apple_Terminal. Users can still opt in\n+ * explicitly on terminals that support RGB but do not advertise COLORTERM.\n  */\n \n-if (process.env.HERMES_TUI_TRUECOLOR !== '0' && !process.env.NO_COLOR && !process.env.FORCE_COLOR) {\n+const TRUE_RE = /^(?:1|true|yes|on)$/i\n+const FALSE_RE = /^(?:0|false|no|off)$/i\n+\n+export function shouldForceTruecolor(env: NodeJS.ProcessEnv = process.env): boolean {\n+  const override = (env.HERMES_TUI_TRUECOLOR ?? '').trim()\n+\n+  if (FALSE_RE.test(override) || 'NO_COLOR' in env) {\n+    return false\n+  }\n+\n+  return TRUE_RE.test(override)\n+}\n+\n+if (shouldForceTruecolor()) {\n   if (!process.env.COLORTERM) {\n     process.env.COLORTERM = 'truecolor'\n   }"]},{"file":"ui-tui/src/theme.ts","hunks":["@@ -76,6 +76,162 @@ function mix(a: string, b: string, t: number) {\n   return '#' + ((1 << 24) | (lerp(0) << 16) | (lerp(1) << 8) | lerp(2)).toString(16).slice(1)\n }\n \n+const XTERM_6_LEVELS = [0, 95, 135, 175, 215, 255] as const\n+const ANSI_LIGHT_MAX_LUMINANCE = 0.72\n+const ANSI_LIGHT_TARGET_LUMINANCE = 0.34\n+const ANSI_LIGHT_MIN_SATURATION = 0.22\n+const ANSI_MUTED_BUCKET = 245\n+\n+const ANSI_NORMALIZED_FOREGROUNDS: readonly (keyof ThemeColors)[] = [\n+  'text',\n+  'label',\n+  'ok',\n+  'error',\n+  'warn',\n+  'prompt',\n+  'statusFg',\n+  'statusGood',\n+  'statusWarn',\n+  'statusBad',\n+  'statusCritical',\n+  'shellDollar'\n+]\n+\n+const ANSI_MUTED_FOREGROUNDS: readonly (keyof ThemeColors)[] = ['muted', 'sessionLabel', 'sessionBorder']\n+\n+function xtermEightBitRgb(colorNumber: number): [number, number, number] {\n+  if (colorNumber >= 232) {\n+    const value = 8 + (colorNumber - 232) * 10\n+\n+    return [value, value, value]\n+  }\n+\n+  if (colorNumber >= 16) {\n+    const offset = colorNumber - 16\n+\n+    return [\n+      XTERM_6_LEVELS[Math.floor(offset / 36) % 6]!,\n+      XTERM_6_LEVELS[Math.floor(offset / 6) % 6]!,\n+      XTERM_6_LEVELS[offset % 6]!\n+    ]\n+  }\n+\n+  return [0, 0, 0]\n+}\n+\n+function channelLuminance(value: number): number {\n+  const normalized = value / 255\n+\n+  return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4\n+}\n+\n+function relativeLuminance(red: number, green: number, blue: number): number {\n+  return 0.2126 * channelLuminance(red) + 0.7152 * channelLuminance(green) + 0.0722 * channelLuminance(blue)\n+}\n+\n+function rgbToHsl(red: number, green: number, blue: number): [number, number, number] {\n+  const rn = red / 255\n+  const gn = green / 255\n+  const bn = blue / 255\n+  const max = Math.max(rn, gn, bn)\n+  const min = Math.min(rn, gn, bn)\n+  const lightness = (max + min) / 2\n+\n+  if (max === min) {\n+    return [0, 0, lightness]\n+  }\n+\n+  const delta = max - min\n+  const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min)\n+\n+  const hue =\n+    max === rn\n+      ? (gn - bn) / delta + (gn < bn ? 6 : 0)\n+      : max === gn\n+        ? (bn - rn) / delta + 2\n+        : (rn - gn) / delta + 4\n+\n+  return [hue / 6, saturation, lightness]\n+}\n+\n+function circularDistance(a: number, b: number): number {\n+  const distance = Math.abs(a - b)\n+\n+  return Math.min(distance, 1 - distance)\n+}\n+\n+// Mirrors @hermes/ink's colorize.ts. Keep local: app code compiles from\n+// ui-tui/src, while @hermes/ink is bundled separately from packages/.\n+function richEightBitColorNumber(red: number, green: number, blue: number): number {\n+  const [, saturation, lightness] = rgbToHsl(red, green, blue)\n+\n+  if (saturation < 0.15) {\n+    const gray = Math.round(lightness * 25)\n+\n+    return gray === 0 ? 16 : gray === 25 ? 231 : 231 + gray\n+  }\n+\n+  const sixRed = red < 95 ? red / 95 : 1 + (red - 95) / 40\n+  const sixGreen = green < 95 ? green / 95 : 1 + (green - 95) / 40\n+  const sixBlue = blue < 95 ? blue / 95 : 1 + (blue - 95) / 40\n+\n+  return 16 + 36 * Math.round(sixRed) + 6 * Math.round(sixGreen) + Math.round(sixBlue)\n+}\n+\n+function bestReadableAnsiColor(red: number, green: number, blue: number): number {\n+  const [hue, saturation, lightness] = rgbToHsl(red, green, blue)\n+  let bestColor = richEightBitColorNumber(red, green, blue)\n+  let bestScore = Number.POSITIVE_INFINITY\n+\n+  for (let colorNumber = 16; colorNumber <= 255; colorNumber += 1) {\n+    const [candidateRed, candidateGreen, candidateBlue] = xtermEightBitRgb(colorNumber)\n+    const candidateLuminance = relativeLuminance(candidateRed, candidateGreen, candidateBlue)\n+\n+    if (candidateLuminance > ANSI_LIGHT_MAX_LUMINANCE) {\n+      continue\n+    }\n+\n+    const [candidateHue, candidateSaturation, candidateLightness] = rgbToHsl(\n+      candidateRed,\n+      candidateGreen,\n+      candidateBlue\n+    )\n+\n+    const saturationFloorPenalty =\n+      candidateSaturation < ANSI_LIGHT_MIN_SATURATION ? (ANSI_LIGHT_MIN_SATURATION - candidateSaturation) * 3 : 0\n+\n+    const score =\n+      circularDistance(candidateHue, hue) * 4 +\n+      Math.abs(candidateSaturation - Math.max(ANSI_LIGHT_MIN_SATURATION, saturation)) * 0.8 +\n+      Math.abs(candidateLightness - Math.min(lightness, ANSI_LIGHT_TARGET_LUMINANCE)) * 2 +\n+      saturationFloorPenalty\n+\n+    if (score < bestScore) {\n+      bestColor = colorNumber\n+      bestScore = score\n+    }\n+  }\n+\n+  return bestColor\n+}\n+\n+function normalizeAnsiForeground(color: string): string {\n+  const rgb = parseHex(color)\n+\n+  if (!rgb) {\n+    return color\n+  }\n+\n+  const richAnsi = richEightBitColorNumber(rgb[0], rgb[1], rgb[2])\n+  const richRgb = xtermEightBitRgb(richAnsi)\n+\n+  const ansi = relativeLuminance(richRgb[0], richRgb[1], richRgb[2]) > ANSI_LIGHT_MAX_LUMINANCE\n+    ? bestReadableAnsiColor(rgb[0], rgb[1], rgb[2])\n+    : richAnsi\n+\n+  return `ansi256(${ansi})`\n+}\n+\n // ── Defaults ─────────────────────────────────────────────────────────\n \n const BRAND: ThemeBrand = {","@@ -190,12 +346,11 @@ export const LIGHT_THEME: Theme = {\n const TRUE_RE = /^(?:1|true|yes|on)$/\n const FALSE_RE = /^(?:0|false|no|off)$/\n \n-// Reserved for future TERM_PROGRAM-based heuristics.  Empty by default:\n-// most modern terminals (Ghostty, Warp, iTerm2, Apple_Terminal) ship a\n-// dark profile out of the box, so guessing wrong here is more annoying\n-// than missing a light user — light users can always set\n-// `HERMES_TUI_LIGHT=1` or `HERMES_TUI_THEME=light`.\n-const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>()\n+// TERM_PROGRAM fallback allow-list for terminals whose default profile is\n+// light and which may not expose COLORFGBG. This currently includes Apple\n+// Terminal. Explicit HERMES_TUI_THEME / COLORFGBG signals above still win,\n+// so dark Apple Terminal profiles that advertise a dark background stay dark.\n+const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>(['Apple_Terminal'])\n \n // Best-effort RGB → luminance check.  Currently only accepts a 3- or\n // 6-digit hex value (with or without a leading `#`); the env var name","@@ -247,7 +402,7 @@ function backgroundLuminance(raw: string): null | number {\n //      slot 7 or 15 on light profiles; 0–15 ranges are otherwise\n //      treated as authoritatively dark so the TERM_PROGRAM\n //      allow-list below cannot override an explicit dark profile.\n-//   5. `TERM_PROGRAM` light-default allow-list (currently empty).\n+//   5. `TERM_PROGRAM` light-default allow-list.\n //\n // Anything we can't decide stays dark — the default Hermes palette\n // is the dark one.","@@ -313,7 +468,42 @@ export function detectLightMode(\n   return lightDefaultTermPrograms.has(termProgram)\n }\n \n-export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME\n+function shouldNormalizeAnsiLightTheme(env: NodeJS.ProcessEnv = process.env, isLight = detectLightMode(env)): boolean {\n+  const colorTerm = (env.COLORTERM ?? '').trim().toLowerCase()\n+  const termProgram = (env.TERM_PROGRAM ?? '').trim()\n+\n+  return termProgram === 'Apple_Terminal' && colorTerm !== 'truecolor' && colorTerm !== '24bit' && isLight\n+}\n+\n+export function normalizeThemeForAnsiLightTerminal(\n+  theme: Theme,\n+  env: NodeJS.ProcessEnv = process.env,\n+  isLight = detectLightMode(env)\n+): Theme {\n+  if (!shouldNormalizeAnsiLightTheme(env, isLight)) {\n+    return theme\n+  }\n+\n+  const color = { ...theme.color }\n+\n+  for (const key of ANSI_NORMALIZED_FOREGROUNDS) {\n+    color[key] = normalizeAnsiForeground(color[key])\n+  }\n+\n+  for (const key of ANSI_MUTED_FOREGROUNDS) {\n+    color[key] = `ansi256(${ANSI_MUTED_BUCKET})`\n+  }\n+\n+  return { ...theme, color }\n+}\n+\n+const DEFAULT_LIGHT_MODE = detectLightMode()\n+\n+export const DEFAULT_THEME: Theme = normalizeThemeForAnsiLightTerminal(\n+  DEFAULT_LIGHT_MODE ? LIGHT_THEME : DARK_THEME,\n+  process.env,\n+  DEFAULT_LIGHT_MODE\n+)\n \n // ── Skin → Theme ─────────────────────────────────────────────────────\n ","@@ -333,7 +523,7 @@ export function fromSkin(\n   const muted = c('banner_dim') ?? d.color.muted\n   const completionBg = c('completion_menu_bg') ?? d.color.completionBg\n \n-  return {\n+  return normalizeThemeForAnsiLightTerminal({\n     color: {\n       primary: c('ui_primary') ?? c('banner_title') ?? d.color.primary,\n       accent,","@@ -379,5 +569,5 @@ export function fromSkin(\n \n     bannerLogo,\n     bannerHero\n-  }\n+  }, process.env, DEFAULT_LIGHT_MODE)\n }\n"]}]},{"kind":"pr","number":26137,"title":"fix(cli): kill resize scrollback duplication + light-mode visibility","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_cli/skin_engine.py","tests/cli/test_cli_light_mode.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -1242,7 +1242,13 @@ def _prune_orphaned_branches(repo_root: str) -> None:\n \n \n def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:\n-    \"\"\"Convert a hex color like '#268bd2' to a true-color ANSI escape.\"\"\"\n+    \"\"\"Convert a hex color like '#268bd2' to a true-color ANSI escape.\n+\n+    Auto-remaps known dark-mode-tuned colors to readable light-mode\n+    equivalents when running on a light terminal (see\n+    _maybe_remap_for_light_mode + _LIGHT_MODE_REMAP).\n+    \"\"\"\n+    hex_color = _maybe_remap_for_light_mode(hex_color)\n     try:\n         r = int(hex_color[1:3], 16)\n         g = int(hex_color[3:5], 16)","@@ -1253,6 +1259,250 @@ def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:\n         return _ACCENT_ANSI_DEFAULT if bold else \"\\033[38;2;184;134;11m\"\n \n \n+# ────────────────────────────────────────────────────────────────────────\n+# Light/dark terminal mode detection.\n+#\n+# Mirrors ui-tui/src/theme.ts detectLightMode().  Used to decide whether\n+# to remap \"near-white\" skin colors (e.g. #FFF8DC banner_text, #B8860B\n+# banner_dim) to darker equivalents that are readable on a light\n+# Terminal.app / iTerm2 background.\n+#\n+# Detection priority:\n+#   1. HERMES_LIGHT / HERMES_TUI_LIGHT env (true/false) — explicit override\n+#   2. HERMES_TUI_THEME=light|dark — explicit theme\n+#   3. HERMES_TUI_BACKGROUND=#RRGGBB — explicit bg hint\n+#   4. COLORFGBG env (set by xterm/Konsole/urxvt) — bg slot 7/15 = light\n+#   5. OSC 11 query (\\x1b]11;?\\x1b\\\\) — ask the terminal directly\n+#   6. Default: assume dark (matches the legacy Hermes assumption)\n+#\n+# Cached after first call so we don't query the terminal repeatedly.\n+_LIGHT_MODE_CACHE: bool | None = None\n+_TRUE_RE = re.compile(r\"^(1|true|on|yes|y)$\")\n+_FALSE_RE = re.compile(r\"^(0|false|off|no|n)$\")\n+_LIGHT_DEFAULT_TERM_PROGRAMS = frozenset()  # Apple_Terminal doesn't reliably indicate; require explicit\n+\n+\n+def _luminance_from_hex(hex_str: str) -> float | None:\n+    s = (hex_str or \"\").strip().lstrip(\"#\")\n+    if len(s) == 3:\n+        s = \"\".join(c * 2 for c in s)\n+    if len(s) != 6 or not all(c in \"0123456789abcdefABCDEF\" for c in s):\n+        return None\n+    try:\n+        r, g, b = int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)\n+    except ValueError:\n+        return None\n+    # Rec.709 luma\n+    return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0\n+\n+\n+def _query_osc11_background() -> str | None:\n+    \"\"\"Ask the terminal for its background color via OSC 11.\n+\n+    Most modern terminals reply with \\x1b]11;rgb:RRRR/GGGG/BBBB\\x1b\\\\\n+    within a few ms.  We wait up to 100ms total before giving up.\n+    Returns \"#RRGGBB\" or None on timeout / non-tty.\n+    \"\"\"\n+    if not sys.stdin.isatty() or not sys.stdout.isatty():\n+        return None\n+    try:\n+        import termios\n+        import tty\n+        fd = sys.stdin.fileno()\n+        old = termios.tcgetattr(fd)\n+    except Exception:\n+        return None\n+    try:\n+        try:\n+            tty.setcbreak(fd)\n+        except Exception:\n+            return None\n+        try:\n+            sys.stdout.write(\"\\x1b]11;?\\x1b\\\\\")\n+            sys.stdout.flush()\n+        except Exception:\n+            return None\n+        # Read up to ~50ms for the response\n+        import select\n+        deadline = time.monotonic() + 0.1\n+        buf = b\"\"\n+        while time.monotonic() < deadline:\n+            r, _, _ = select.select([fd], [], [], deadline - time.monotonic())\n+            if not r:\n+                continue\n+            try:\n+                chunk = os.read(fd, 64)\n+            except OSError:\n+                break\n+            if not chunk:\n+                break\n+            buf += chunk\n+            if b\"\\x1b\\\\\" in buf or b\"\\x07\" in buf:\n+                break\n+        # Parse: \\x1b]11;rgb:RRRR/GGGG/BBBB\\x1b\\\\\n+        m = re.search(rb\"rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)\", buf)\n+        if not m:\n+            return None\n+        # Each component is 1-4 hex digits — normalize to 8-bit\n+        def norm(h: bytes) -> int:\n+            v = int(h, 16)\n+            # Scale to 0-255 based on hex length\n+            bits = len(h) * 4\n+            return (v * 255) // ((1 << bits) - 1) if bits else 0\n+        r, g, b = norm(m.group(1)), norm(m.group(2)), norm(m.group(3))\n+        return f\"#{r:02X}{g:02X}{b:02X}\"\n+    finally:\n+        try:\n+            termios.tcsetattr(fd, termios.TCSANOW, old)\n+        except Exception:\n+            pass\n+\n+\n+def _detect_light_mode() -> bool:\n+    global _LIGHT_MODE_CACHE\n+    if _LIGHT_MODE_CACHE is not None:\n+        return _LIGHT_MODE_CACHE\n+    result = False\n+    try:\n+        # 1. Explicit env override\n+        for var in (\"HERMES_LIGHT\", \"HERMES_TUI_LIGHT\"):\n+            v = (os.environ.get(var) or \"\").strip().lower()\n+            if _TRUE_RE.match(v):\n+                result = True\n+                _LIGHT_MODE_CACHE = result\n+                return result\n+            if _FALSE_RE.match(v):\n+                _LIGHT_MODE_CACHE = result\n+                return result\n+        # 2. Theme hint\n+        theme = (os.environ.get(\"HERMES_TUI_THEME\") or \"\").strip().lower()\n+        if theme == \"light\":\n+            result = True\n+            _LIGHT_MODE_CACHE = result\n+            return result\n+        if theme == \"dark\":\n+            _LIGHT_MODE_CACHE = result\n+            return result\n+        # 3. Explicit bg hex\n+        bg_hint = os.environ.get(\"HERMES_TUI_BACKGROUND\") or \"\"\n+        bg_lum = _luminance_from_hex(bg_hint)\n+        if bg_lum is not None:\n+            result = bg_lum >= 0.5\n+            _LIGHT_MODE_CACHE = result\n+            return result\n+        # 4. COLORFGBG (xterm/Konsole/urxvt)\n+        cfgbg = (os.environ.get(\"COLORFGBG\") or \"\").strip()\n+        if cfgbg:\n+            last = cfgbg.split(\";\")[-1] if \";\" in cfgbg else cfgbg\n+            if last.isdigit():\n+                bg = int(last)\n+                if bg in (7, 15):\n+                    result = True\n+                    _LIGHT_MODE_CACHE = result\n+                    return result\n+                if 0 <= bg < 16:\n+                    _LIGHT_MODE_CACHE = result\n+                    return result\n+        # 5. OSC 11 query (best-effort, only when stdin/stdout are TTY)\n+        bg_color = _query_osc11_background()\n+        if bg_color:\n+            lum = _luminance_from_hex(bg_color)\n+            if lum is not None:\n+                result = lum >= 0.5\n+                _LIGHT_MODE_CACHE = result\n+                return result\n+        # 6. TERM_PROGRAM allow-list (currently empty)\n+        tp = (os.environ.get(\"TERM_PROGRAM\") or \"\").strip()\n+        if tp in _LIGHT_DEFAULT_TERM_PROGRAMS:\n+            result = True\n+    except Exception:\n+        result = False\n+    _LIGHT_MODE_CACHE = result\n+    return result\n+\n+\n+# Light-mode equivalents of skin colors that are unreadable on cream\n+# Terminal.app backgrounds.  Used by _SkinAwareAnsi to remap colors\n+# at resolution time when light mode is detected.\n+#\n+# IMPORTANT: only remap colors that are used as STANDALONE foregrounds\n+# on the terminal's background.  Don't remap colors that are paired\n+# with a dark bg (e.g. status bar text on bg:#1a1a2e) — those would\n+# become invisible the OTHER direction (dark gray on dark navy).\n+_LIGHT_MODE_REMAP: dict[str, str] = {\n+    # Original (dark-mode) -> Light-mode replacement (darker, readable)\n+    \"#FFF8DC\": \"#1A1A1A\",   # cornsilk -> near-black\n+    \"#FFD700\": \"#9A6B00\",   # gold -> dark goldenrod (readable on cream)\n+    \"#FFBF00\": \"#8A5A00\",   # amber -> dark amber\n+    \"#B8860B\": \"#5C4500\",   # dark goldenrod -> deeper brown (more contrast)\n+    \"#DAA520\": \"#6B4F00\",   # goldenrod -> dark olive\n+    \"#F1E6CF\": \"#1A1A1A\",   # cream -> near-black\n+    \"#c9d1d9\": \"#24292F\",   # github-light fg\n+    \"#EAF7FF\": \"#0F1B26\",   # ice\n+    \"#F5F5F5\": \"#1A1A1A\",\n+    \"#FFF0D4\": \"#1A1A1A\",\n+    \"#CD7F32\": \"#8A4F1A\",   # bronze -> darker bronze\n+    \"#FFEFB5\": \"#3A2A00\",\n+    # NOTE: skipping #C0C0C0/#888888/#555555/#8B8682 — those are\n+    # status-bar foregrounds paired with dark navy bg, where dark\n+    # remap values would become invisible.\n+}\n+\n+\n+def _maybe_remap_for_light_mode(hex_color: str) -> str:\n+    \"\"\"If we're in light mode, remap a dark-mode-tuned color to a\n+    higher-contrast equivalent.  No-op in dark mode.\"\"\"\n+    if not _detect_light_mode():\n+        return hex_color\n+    if not hex_color or not hex_color.startswith(\"#\"):\n+        return hex_color\n+    # Case-insensitive lookup\n+    upper = hex_color.upper()\n+    if upper in _LIGHT_MODE_REMAP_UPPER:\n+        return _LIGHT_MODE_REMAP_UPPER[upper]\n+    return hex_color\n+\n+\n+# Pre-uppercased lookup table for case-insensitive remapping\n+_LIGHT_MODE_REMAP_UPPER = {k.upper(): v for k, v in _LIGHT_MODE_REMAP.items()}\n+\n+\n+def _install_skin_light_mode_hook() -> None:\n+    \"\"\"Wrap SkinConfig.get_color at import time so EVERY skin color read goes\n+    through the light-mode remap.  Idempotent.\"\"\"\n+    try:\n+        from hermes_cli.skin_engine import SkinConfig  # type: ignore[import]\n+    except Exception:\n+        return\n+    if getattr(SkinConfig, \"_hermes_light_mode_hook_installed\", False):\n+        return\n+    _orig_get_color = SkinConfig.get_color\n+\n+    def _wrapped_get_color(self, key, fallback=\"\"):\n+        value = _orig_get_color(self, key, fallback)\n+        try:\n+            return _maybe_remap_for_light_mode(value)\n+        except Exception:\n+            return value\n+\n+    SkinConfig.get_color = _wrapped_get_color  # type: ignore[method-assign]\n+    SkinConfig._hermes_light_mode_hook_installed = True  # type: ignore[attr-defined]\n+\n+\n+_install_skin_light_mode_hook()\n+\n+\n+# Prime the light-mode detection cache early (at module load) when\n+# we're running interactively so OSC 11 happens before pt grabs the\n+# tty.  Skip for non-tty contexts (subagents, gateway, tests).\n+try:\n+    if sys.stdin.isatty() and sys.stdout.isatty():\n+        _detect_light_mode()\n+except Exception:\n+    pass\n+\n+\n+\n class _SkinAwareAnsi:\n     \"\"\"Lazy ANSI escape that resolves from the skin engine on first use.\n ","@@ -1290,7 +1540,12 @@ def reset(self) -> None:\n \n \n _ACCENT = _SkinAwareAnsi(\"response_border\", \"#FFD700\", bold=True)\n-_DIM = _SkinAwareAnsi(\"banner_dim\", \"#B8860B\")\n+# Use ANSI dim+italic attributes (\\x1b[2;3m) instead of a hardcoded\n+# hex color so dim/thinking text inherits the terminal's default\n+# foreground color and stays readable in both light and dark\n+# Terminal.app modes.  Hardcoded skin colors like #B8860B\n+# (dark goldenrod) become invisible against light cream backgrounds.\n+_DIM = \"\\x1b[2;3m\"\n \n \n def _accent_hex() -> str:","@@ -7947,8 +8202,8 @@ def _bg_thinking(text: str) -> None:\n                         from hermes_cli.skin_engine import get_active_skin\n                         _skin = get_active_skin()\n                         label = _skin.get_branding(\"response_label\", \"⚕ Hermes\")\n-                        _resp_color = _skin.get_color(\"response_border\", \"#CD7F32\")\n-                        _resp_text = _skin.get_color(\"banner_text\", \"#FFF8DC\")\n+                        _resp_color = _maybe_remap_for_light_mode(_skin.get_color(\"response_border\", \"#CD7F32\"))\n+                        _resp_text = _maybe_remap_for_light_mode(_skin.get_color(\"banner_text\", \"#FFF8DC\"))\n                     except Exception:\n                         label = \"⚕ Hermes\"\n                         _resp_color = \"#CD7F32\"","@@ -8549,7 +8804,8 @@ def _handle_skin_command(self, cmd: str):\n \n         set_active_skin(new_skin)\n         _ACCENT.reset()  # Re-resolve ANSI color for the new skin\n-        _DIM.reset()     # Re-resolve dim/secondary ANSI color for the new skin\n+        # _DIM is now a fixed dim+italic ANSI escape (terminal-default fg)\n+        # so it doesn't need re-resolving on skin switch.\n         if save_config_value(\"display.skin\", new_skin):\n             print(f\"  Skin set to: {new_skin} (saved)\")\n         else:","@@ -10928,12 +11184,12 @@ def run_agent():\n                     from hermes_cli.skin_engine import get_active_skin\n                     _skin = get_active_skin()\n                     label = _skin.get_branding(\"response_label\", \"⚕ Hermes\")\n-                    _resp_color = _skin.get_color(\"response_border\", \"#CD7F32\")\n-                    _resp_text = _skin.get_color(\"banner_text\", \"#FFF8DC\")\n+                    _resp_color = _maybe_remap_for_light_mode(_skin.get_color(\"response_border\", \"#CD7F32\"))\n+                    _resp_text = _maybe_remap_for_light_mode(_skin.get_color(\"banner_text\", \"#FFF8DC\"))\n                 except Exception:\n                     label = \"⚕ Hermes\"\n-                    _resp_color = \"#CD7F32\"\n-                    _resp_text = \"#FFF8DC\"\n+                    _resp_color = _maybe_remap_for_light_mode(\"#CD7F32\")\n+                    _resp_text = _maybe_remap_for_light_mode(\"#FFF8DC\")\n \n                 is_error_response = result and (result.get(\"failed\") or result.get(\"partial\"))\n                 already_streamed = self._stream_started and self._stream_box_opened and not is_error_response","@@ -11172,13 +11428,48 @@ def _get_tui_prompt_text(self) -> str:\n         return \"\".join(text for _, text in self._get_tui_prompt_fragments())\n \n     def _build_tui_style_dict(self) -> dict[str, str]:\n-        \"\"\"Layer the active skin's prompt_toolkit colors over the base TUI style.\"\"\"\n+        \"\"\"Layer the active skin's prompt_toolkit colors over the base TUI style.\n+\n+        Also rewrites any hex-color tokens in the resulting style strings\n+        to their light-mode equivalents (via _LIGHT_MODE_REMAP) when the\n+        terminal is detected as light.  This makes the chrome readable\n+        on cream Terminal.app backgrounds without per-skin overrides.\n+        \"\"\"\n         style_dict = dict(getattr(self, \"_tui_style_base\", {}) or {})\n         try:\n             from hermes_cli.skin_engine import get_prompt_toolkit_style_overrides\n             style_dict.update(get_prompt_toolkit_style_overrides())\n         except Exception:\n             pass\n+        # Light-mode remap on the style strings.  Each value is a pt\n+        # style string like \"bg:#1a1a2e #C0C0C0 bold\" — split on space,\n+        # rewrite any \"#XXX\" tokens (including \"bg:#XXX\") through the\n+        # light-mode remap, rejoin.\n+        #\n+        # CRITICAL: skip the remap entirely when a style string already\n+        # specifies its own bg (e.g. status-bar / completion-menu styles\n+        # with `bg:#1a1a2e ...`).  Those colors were tuned for that\n+        # specific dark bg and remapping the FG to a dark equivalent\n+        # would produce dark-on-dark (invisible).  The terminal's BG\n+        # mode is irrelevant — what matters is the bg the style itself\n+        # paints.\n+        try:\n+            if _detect_light_mode():\n+                def _remap_value(v: str) -> str:\n+                    if not v:\n+                        return v\n+                    tokens = [redacted:secret-assignment]\n+                    has_explicit_bg = any(t.startswith(\"bg:\") for t in tokens)\n+                    if has_explicit_bg:\n+                        # The style paints its own bg — leave its fg alone.\n+                        return v\n+                    return \" \".join(\n+                        _maybe_remap_for_light_mode(t) if t.startswith(\"#\") else t\n+                        for t in tokens\n+                    )\n+                style_dict = {k: _remap_value(v or \"\") for k, v in style_dict.items()}\n+        except Exception:\n+            pass\n         return style_dict\n \n     def _apply_tui_skin_style(self) -> bool:","@@ -11264,6 +11555,13 @@ def _build_tui_layout_children(\n \n     def run(self):\n         \"\"\"Run the interactive CLI loop with persistent input at bottom.\"\"\"\n+        # Detect light/dark terminal mode now (before pt grabs the tty).\n+        # Caches the result so subsequent _hex_to_ansi / style calls\n+        # don't risk re-querying mid-render.\n+        try:\n+            _detect_light_mode()\n+        except Exception:\n+            pass\n         # Push the entire TUI to the bottom of the terminal so the banner,\n         # responses, and prompt all appear pinned to the bottom — empty\n         # space stays above, not below.  This prints enough blank lines to","@@ -13027,11 +13325,16 @@ def _get_voice_status():\n         \n         # Style for the application\n         self._tui_style_base = {\n-            'input-area': '#FFF8DC',\n-            'placeholder': '#555555 italic',\n-            'prompt': '#FFF8DC',\n+            # Input area / prompt: empty style strings inherit the\n+            # terminal's default foreground/background, so the typed\n+            # text is readable in both light and dark Terminal.app\n+            # color schemes.  (Hardcoding a near-white #FFF8DC made\n+            # input invisible on light backgrounds.)\n+            'input-area': '',\n+            'placeholder': '#888888 italic',\n+            'prompt': '',\n             'prompt-working': '#888888 italic',\n-            'hint': '#555555 italic',\n+            'hint': '#888888 italic',\n             'status-bar': 'bg:#1a1a2e #C0C0C0',\n             'status-bar-strong': 'bg:#1a1a2e #FFD700 bold',\n             'status-bar-dim': 'bg:#1a1a2e #8B8682',","@@ -13090,19 +13393,70 @@ def _get_voice_status():\n         self._app = app  # Store reference for clarify_callback\n \n         # ── Fix ghost status-bar lines on terminal resize ──────────────\n-        # When the terminal shrinks (e.g. un-maximize), the emulator reflows\n-        # the previously-rendered full-width rows (status bar, input rules)\n-        # into multiple narrower rows.  prompt_toolkit's _on_resize handler\n-        # only cursor_up()s by the stored layout height, missing the extra\n-        # rows created by reflow — leaving ghost duplicates visible.\n+        # Resize handling: monkey-patch prompt_toolkit's _output_screen_diff\n+        # to suppress the deliberate \"reserve vertical space\" scroll-up.\n         #\n-        # It's not just column-shrink: widening, row-shrinking, and\n-        # multiplexer-driven SIGWINCH-less redraws (cmux / tmux tab switch)\n-        # all produce the same class of drift, where the renderer's tracked\n-        # _cursor_pos.y no longer matches terminal reality. The only reliable\n-        # recovery is a full screen-clear (\\x1b[2J\\x1b[H) before the next\n-        # redraw, so we force one on every resize rather than trying to\n-        # compute the exact drift.\n+        # Background: prompt_toolkit's renderer (renderer.py L232-242)\n+        # explicitly moves the cursor to the bottom of the canvas after\n+        # painting \"to make sure the terminal scrolls up, even when the\n+        # lower lines of the canvas just contain whitespace\".  In\n+        # non-fullscreen mode this scrolls chrome content (status bar,\n+        # input rules) into terminal scrollback on every render.  When\n+        # the terminal column-shrinks, the emulator reflows the previously\n+        # rendered full-width rows into multiple narrower rows that get\n+        # pushed up — leaving ghost duplicates AND polluting scrollback.\n+        # Same issue as pt #29 (open since 2014), #1675, #1933.\n+        #\n+        # Surgical fix: wrap _output_screen_diff so that when its internal\n+        # `if current_height > previous_screen.height` branch fires (the\n+        # one that does the bottom-cursor-move), we make it fall through\n+        # by inflating previous_screen.height first.\n+        try:\n+            import prompt_toolkit.renderer as _pt_renderer\n+            from prompt_toolkit.renderer import _output_screen_diff as _orig_osd\n+\n+            if not getattr(_pt_renderer, \"_hermes_osd_patched\", False):\n+                def _patched_output_screen_diff(\n+                    app, output, screen, current_pos, color_depth,\n+                    previous_screen, last_style, is_done, full_screen,\n+                    attrs_for_style_string, style_string_has_style,\n+                    size, previous_width,\n+                ):\n+                    \"\"\"Wraps pt's _output_screen_diff to suppress the\n+                    reserve-vertical-space scroll (renderer.py L232-242).\n+\n+                    Strategy: ONLY when previous_screen is non-None and\n+                    its current height is genuinely smaller than the new\n+                    screen's height, inflate it to match.  This prevents\n+                    the bottom-cursor-move at L242 without changing any\n+                    other code path's behavior.\n+\n+                    Critical: do NOT replace a None previous_screen with\n+                    a fresh Screen() — that would skip the proper\n+                    reset_attributes()+erase_down() at L178-185 which\n+                    fires when previous_screen is None (first-paint /\n+                    width-change).  Without that reset, ANSI styles\n+                    leak between renders.\n+                    \"\"\"\n+                    try:\n+                        if previous_screen is not None and hasattr(previous_screen, \"height\"):\n+                            if previous_screen.height < screen.height:\n+                                previous_screen.height = screen.height\n+                    except Exception:\n+                        pass\n+\n+                    return _orig_osd(\n+                        app, output, screen, current_pos, color_depth,\n+                        previous_screen, last_style, is_done, full_screen,\n+                        attrs_for_style_string, style_string_has_style,\n+                        size, previous_width,\n+                    )\n+\n+                _pt_renderer._output_screen_diff = _patched_output_screen_diff\n+                _pt_renderer._hermes_osd_patched = True\n+        except Exception:\n+            pass\n+\n         _original_on_resize = app._on_resize\n \n         def _resize_clear_ghosts():"]},{"file":"hermes_cli/skin_engine.py","hunks":["@@ -849,10 +849,14 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:\n     except Exception:\n         return {}\n \n-    prompt = skin.get_color(\"prompt\", \"#FFF8DC\")\n+    # Input/prompt: leave unset by default so the typed text inherits\n+    # the terminal's foreground color (readable in both light and dark\n+    # color schemes).  Skins can opt into a colored prompt by setting\n+    # `prompt` explicitly in their YAML.\n+    prompt = skin.get_color(\"prompt\", \"\")\n     input_rule = skin.get_color(\"input_rule\", \"#CD7F32\")\n     title = skin.get_color(\"banner_title\", \"#FFD700\")\n-    text = skin.get_color(\"banner_text\", prompt)\n+    text = skin.get_color(\"banner_text\", \"#FFF8DC\")\n     dim = skin.get_color(\"banner_dim\", \"#555555\")\n     label = skin.get_color(\"ui_label\", title)\n     warn = skin.get_color(\"ui_warn\", \"#FF8C00\")","@@ -872,7 +876,11 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:\n     menu_meta_current_bg = skin.get_color(\"completion_menu_meta_current_bg\", menu_current_bg)\n \n     return {\n-        \"input-area\": prompt,\n+        # Typed input always uses terminal default fg/bg so it's\n+        # readable in both light and dark Terminal.app modes.  The\n+        # skin's `prompt` color (if any) only styles the prompt symbol,\n+        # NOT the user's typed text.\n+        \"input-area\": \"\",\n         \"placeholder\": f\"{dim} italic\",\n         \"prompt\": prompt,\n         \"prompt-working\": f\"{dim} italic\","]},{"file":"tests/cli/test_cli_light_mode.py","hunks":["@@ -0,0 +1,154 @@\n+\"\"\"Tests for the light-mode terminal detection + color remap in cli.py.\n+\n+Covers the env-override path and the SkinConfig.get_color() wrapper that\n+the resize / light-mode salvage installs at module import time.  We don't\n+try to fake an OSC 11 reply — the env-override branch short-circuits\n+before the terminal query, which is the path most users hit.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import importlib\n+\n+import pytest\n+\n+\n[redacted:email]\n+def cli_mod(monkeypatch):\n+    \"\"\"Import cli with the light-mode cache cleared each test.\"\"\"\n+    import cli as _cli\n+\n+    # The module-level _install_skin_light_mode_hook() and import-time\n+    # _detect_light_mode() prime ran once at first import.  We just reset\n+    # the detection cache so the per-test env override takes effect.\n+    monkeypatch.setattr(_cli, \"_LIGHT_MODE_CACHE\", None)\n+    return _cli\n+\n+\n+class TestLightModeDetection:\n+    def test_hermes_light_env_true_forces_light(self, cli_mod, monkeypatch):\n+        monkeypatch.setenv(\"HERMES_LIGHT\", \"1\")\n+        assert cli_mod._detect_light_mode() is True\n+\n+    def test_hermes_light_env_false_forces_dark(self, cli_mod, monkeypatch):\n+        monkeypatch.setenv(\"HERMES_LIGHT\", \"0\")\n+        # Also blank out other signals so nothing else flips it light.\n+        monkeypatch.delenv(\"HERMES_TUI_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_THEME\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_BACKGROUND\", raising=False)\n+        monkeypatch.delenv(\"COLORFGBG\", raising=False)\n+        assert cli_mod._detect_light_mode() is False\n+\n+    def test_theme_hint_light(self, cli_mod, monkeypatch):\n+        monkeypatch.delenv(\"HERMES_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_LIGHT\", raising=False)\n+        monkeypatch.setenv(\"HERMES_TUI_THEME\", \"light\")\n+        assert cli_mod._detect_light_mode() is True\n+\n+    def test_background_hex_hint_light(self, cli_mod, monkeypatch):\n+        monkeypatch.delenv(\"HERMES_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_THEME\", raising=False)\n+        monkeypatch.setenv(\"HERMES_TUI_BACKGROUND\", \"#FFFFFF\")\n+        assert cli_mod._detect_light_mode() is True\n+\n+    def test_background_hex_hint_dark(self, cli_mod, monkeypatch):\n+        monkeypatch.delenv(\"HERMES_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_THEME\", raising=False)\n+        monkeypatch.setenv(\"HERMES_TUI_BACKGROUND\", \"#1a1a2e\")\n+        monkeypatch.delenv(\"COLORFGBG\", raising=False)\n+        assert cli_mod._detect_light_mode() is False\n+\n+    def test_colorfgbg_light_bg_slot(self, cli_mod, monkeypatch):\n+        monkeypatch.delenv(\"HERMES_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_LIGHT\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_THEME\", raising=False)\n+        monkeypatch.delenv(\"HERMES_TUI_BACKGROUND\", raising=False)\n+        monkeypatch.setenv(\"COLORFGBG\", \"0;15\")  # bg slot 15 = light\n+        assert cli_mod._detect_light_mode() is True\n+\n+    def test_cache_is_sticky(self, cli_mod, monkeypatch):\n+        monkeypatch.setenv(\"HERMES_LIGHT\", \"1\")\n+        assert cli_mod._detect_light_mode() is True\n+        # Even if the env flips, the cached result wins until reset.\n+        monkeypatch.setenv(\"HERMES_LIGHT\", \"0\")\n+        assert cli_mod._detect_light_mode() is True\n+\n+\n+class TestLightModeRemap:\n+    def test_remap_no_op_in_dark_mode(self, cli_mod, monkeypatch):\n+        monkeypatch.setenv(\"HERMES_LIGHT\", \"0\")\n+        # Cache is None from the fixture; first call sticks at False.\n+        assert cli_mod._maybe_remap_for_light_mode(\"#FFF8DC\") == \"#FFF8DC\"\n+\n+    def test_remap_known_dark_color(self, cli_mod, monkeypatch):\n+        monkeypatch.setenv(\"HERMES_LIGHT\", \"1\")\n+        # Force the detect cache to True for this test.\n+        cli_mod._LIGHT_MODE_CACHE = True\n+        assert cli_mod._maybe_remap_for_light_mode(\"#FFF8DC\") == \"#1A1A1A\"\n+        assert cli_mod._maybe_remap_for_light_mode(\"#FFD700\") == \"#9A6B00\"\n+\n+    def test_remap_case_insensitive(self, cli_mod, monkeypatch):\n+        cli_mod._LIGHT_MODE_CACHE = True\n+        # Lowercase input should still remap.\n+        assert cli_mod._maybe_remap_for_light_mode(\"#fff8dc\") == \"#1A1A1A\"\n+\n+    def test_remap_unknown_color_passthrough(self, cli_mod, monkeypatch):\n+        cli_mod._LIGHT_MODE_CACHE = True\n+        # A color not in the remap table is returned unchanged.\n+        assert cli_mod._maybe_remap_for_light_mode(\"#ABCDEF\") == \"#ABCDEF\"\n+\n+    def test_remap_skips_statusbar_paired_colors(self, cli_mod, monkeypatch):\n+        \"\"\"Colors that live on a dark bg (status bar fg) MUST NOT be\n+        remapped — otherwise they go dark-on-dark and disappear.\n+\n+        Regression guard for the patch-11 fix (intentional table omission).\n+        \"\"\"\n+        cli_mod._LIGHT_MODE_CACHE = True\n+        for fg in (\"#C0C0C0\", \"#888888\", \"#555555\", \"#8B8682\"):\n+            assert cli_mod._maybe_remap_for_light_mode(fg) == fg, (\n+                f\"{fg} is a status-bar fg paired with dark bg; remapping it \"\n+                \"would produce dark-on-dark\"\n+            )\n+\n+\n+class TestSkinConfigHook:\n+    \"\"\"The salvage wraps SkinConfig.get_color at module import time so\n+    every skin color read goes through the light-mode remap.  Verify\n+    the hook installed and functions correctly.\n+    \"\"\"\n+\n+    def test_hook_installed(self, cli_mod):\n+        from hermes_cli.skin_engine import SkinConfig\n+\n+        assert getattr(SkinConfig, \"_hermes_light_mode_hook_installed\", False) is True\n+\n+    def test_hook_is_idempotent(self, cli_mod):\n+        # Calling the installer twice must not double-wrap (the marker\n+        # attribute is the guard).\n+        from hermes_cli.skin_engine import SkinConfig\n+\n+        before = SkinConfig.get_color\n+        cli_mod._install_skin_light_mode_hook()\n+        after = SkinConfig.get_color\n+        assert before is after\n+\n+    def test_skin_color_remaps_through_wrapper_in_light_mode(self, cli_mod, monkeypatch):\n+        from hermes_cli.skin_engine import SkinConfig\n+\n+        cli_mod._LIGHT_MODE_CACHE = True\n+        skin = SkinConfig(\n+            name=\"test\",\n+            colors={\"banner_text\": \"#FFF8DC\", \"response_border\": \"#FFD700\"},\n+        )\n+        # The wrapper kicks in at get_color, not at construction time.\n+        assert skin.get_color(\"banner_text\") == \"#1A1A1A\"\n+        assert skin.get_color(\"response_border\") == \"#9A6B00\"\n+\n+    def test_skin_color_passthrough_in_dark_mode(self, cli_mod, monkeypatch):\n+        from hermes_cli.skin_engine import SkinConfig\n+\n+        cli_mod._LIGHT_MODE_CACHE = False\n+        skin = SkinConfig(name=\"test\", colors={\"banner_text\": \"#FFF8DC\"})\n+        assert skin.get_color(\"banner_text\") == \"#FFF8DC\"\n"]}]},{"kind":"pr","number":58545,"title":"fix(qr): use invert=False for QR code terminal rendering (preserve quiet zone)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#58038","number":58038},{"kind":"pr","value":"#58038","number":58038}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":60171,"title":"fix(tui): improve light theme text contrast","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4cc6da84a155b3c1d4dd81ec6d24285c8d5b7b5e","number":0},{"kind":"pr","value":"#17695","number":17695},{"kind":"pr","value":"#60141","number":60141}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":60274,"title":"fix(desktop): use default font-smoothing on Retina to fix faint text (#59751)","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":60452,"title":"fix(desktop): use default font-smoothing on Retina to fix faint text (#59751)","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":60972,"title":"fix(cli): improve light terminal body contrast","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#60141","number":60141}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":61553,"title":"fix(cli,tui): skip unreliable COLORFGBG in Zed terminal (#61536)","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":61600,"title":"fix(theme): ignore inherited COLORFGBG in Zed","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#61553","number":61553},{"kind":"pr","value":"#61536","number":61536},{"kind":"pr","value":"#61553","number":61553}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}