From 8eefbef91cd715cfe410bba8c13cfab4eb3040df Mon Sep 17 00:00:00 2001 From: teknium1 Date: Tue, 10 Mar 2026 07:04:02 -0700 Subject: [PATCH] fix: replace ANSI response box with Rich Panel + reduce widget flashing Major UX improvements: 1. Response box now uses a Rich Panel rendered through ChatConsole instead of hand-rolled ANSI box-drawing borders. Rich Panels adapt to terminal width at render time, wrap content inside the borders properly, and use skin colors natively. 2. ChatConsole now reads terminal width at render time via shutil.get_terminal_size() instead of defaulting to 80 cols. All Rich output adapts to the current terminal size. 3. User-input separator reduced to fixed 40-char width so it never wraps regardless of terminal resize. 4. Approval and clarify countdown repaints throttled to every 5s (was 1s), dramatically reducing flicker in Kitty/ghostty. Selection changes still trigger instant repaints via key bindings. 5. Sudo widget now uses dynamic _panel_box_width() instead of hardcoded border strings. Tests: 2860 passed. --- cli.py | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/cli.py b/cli.py index aa09c425e..338d2f723 100755 --- a/cli.py +++ b/cli.py @@ -714,6 +714,8 @@ class ChatConsole: def print(self, *args, **kwargs): self._buffer.seek(0) self._buffer.truncate() + # Read terminal width at render time so panels adapt to current size + self._inner.width = shutil.get_terminal_size((80, 24)).columns self._inner.print(*args, **kwargs) output = self._buffer.getvalue() for line in output.rstrip("\n").split("\n"): @@ -3078,6 +3080,10 @@ class HermesCLI: # Trigger prompt_toolkit repaint from this (non-main) thread self._invalidate() + # Poll for the user's response. The countdown in the hint line + # updates on each invalidate — but frequent repaints cause visible + # flicker in some terminals (Kitty, ghostty). We only refresh the + # countdown every 5 s; selection changes (↑/↓) trigger instant # Poll for the user's response. The countdown in the hint line # updates on each invalidate — but frequent repaints cause visible # flicker in some terminals (Kitty, ghostty). We only refresh the @@ -3098,6 +3104,9 @@ class HermesCLI: if now - _last_countdown_refresh >= 5.0: _last_countdown_refresh = now self._invalidate() + if now - _last_countdown_refresh >= 5.0: + _last_countdown_refresh = now + self._invalidate() # Timed out — tear down the UI and let the agent decide self._clarify_state = None @@ -3239,8 +3248,7 @@ class HermesCLI: # Add user message to history self.conversation_history.append({"role": "user", "content": message}) - w = min(shutil.get_terminal_size().columns, 120) - _cprint(f"{_GOLD}{'─' * w}{_RST}") + _cprint(f"{_GOLD}{'─' * 40}{_RST}") print(flush=True) try: @@ -3315,28 +3323,25 @@ class HermesCLI: response = response + "\n\n---\n_[Interrupted - processing new message]_" if response: - # Cap at 120 so borders don't wrap when shrinking from fullscreen - w = min(shutil.get_terminal_size().columns, 120) - # Use skin branding for response box label + # Use a Rich Panel for the response box — adapts to terminal + # width at render time instead of hard-coding border length. try: from hermes_cli.skin_engine import get_active_skin _skin = get_active_skin() - label = _skin.get_branding("response_label", " ⚕ Hermes ") - _resp_color = _skin.get_color("response_border", "") - if _resp_color: - _resp_start = f"\033[38;2;{int(_resp_color[1:3], 16)};{int(_resp_color[3:5], 16)};{int(_resp_color[5:7], 16)}m" - else: - _resp_start = _GOLD + label = _skin.get_branding("response_label", "⚕ Hermes") + _resp_color = _skin.get_color("response_border", "#CD7F32") except Exception: - label = " ⚕ Hermes " - _resp_start = _GOLD - fill = w - 2 - len(label) # 2 for ╭ and ╮ - top = f"{_resp_start}╭─{label}{'─' * max(fill - 1, 0)}╮{_RST}" - bot = f"{_resp_start}╰{'─' * (w - 2)}╯{_RST}" + label = "⚕ Hermes" + _resp_color = "#CD7F32" - # Render box + response as a single _cprint call so - # nothing can interleave between the box borders. - _cprint(f"\n{top}\n{response}\n\n{bot}") + _chat_console = ChatConsole() + _chat_console.print(Panel( + response, + title=f"[bold]{label}[/bold]", + title_align="left", + border_style=_resp_color, + padding=(1, 2), + )) # Play terminal bell when agent finishes (if enabled). # Works over SSH — the bell propagates to the user's terminal.