From 4e8f60fd110e54db771af507931fd32e855bd880 Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:19:33 -0600 Subject: [PATCH] fix(cli): use display width for wrapped spinner height --- cli.py | 44 +++++++++++++++++--------------- tests/cli/test_cli_status_bar.py | 7 +++++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/cli.py b/cli.py index 8aa8bb03f..02c1a4f7e 100644 --- a/cli.py +++ b/cli.py @@ -2068,20 +2068,35 @@ class HermesCLI: def _spinner_widget_height(self, width: Optional[int] = None) -> int: """Return the visible height for the spinner/status text line above the status bar.""" - if not getattr(self, "_spinner_text", ""): + spinner_line = self._render_spinner_text() + if not spinner_line: return 0 if self._use_minimal_tui_chrome(width=width): return 0 - # Compute how many lines the spinner text needs when wrapped. - # The rendered text is " {emoji} {label} ({elapsed})" — about - # len(_spinner_text) + 16 chars for indent + timer suffix. width = width or self._get_tui_terminal_width() if width and width > 10: import math - text_len = len(self._spinner_text) + 16 # indent + timer - return max(1, math.ceil(text_len / width)) + text_width = self._status_bar_display_width(spinner_line) + return max(1, math.ceil(text_width / width)) return 1 + def _render_spinner_text(self) -> str: + """Return the live spinner/status text exactly as rendered in the TUI.""" + txt = getattr(self, "_spinner_text", "") + if not txt: + return "" + t0 = getattr(self, "_tool_start_time", 0) or 0 + if t0 > 0: + import time as _time + elapsed = _time.monotonic() - t0 + if elapsed >= 60: + _m, _s = int(elapsed // 60), int(elapsed % 60) + elapsed_str = f"{_m}m {_s}s" + else: + elapsed_str = f"{elapsed:.1f}s" + return f" {txt} ({elapsed_str})" + return f" {txt}" + def _get_voice_status_fragments(self, width: Optional[int] = None): """Return the voice status bar fragments for the interactive TUI.""" width = width or self._get_tui_terminal_width() @@ -9375,21 +9390,10 @@ class HermesCLI: return cli_ref._agent_spacer_height() def get_spinner_text(): - txt = cli_ref._spinner_text - if not txt: + spinner_line = cli_ref._render_spinner_text() + if not spinner_line: return [] - # Append live elapsed timer when a tool is running - t0 = cli_ref._tool_start_time - if t0 > 0: - import time as _time - elapsed = _time.monotonic() - t0 - if elapsed >= 60: - _m, _s = int(elapsed // 60), int(elapsed % 60) - elapsed_str = f"{_m}m {_s}s" - else: - elapsed_str = f"{elapsed:.1f}s" - return [('class:hint', f' {txt} ({elapsed_str})')] - return [('class:hint', f' {txt}')] + return [('class:hint', spinner_line)] def get_spinner_height(): return cli_ref._spinner_widget_height() diff --git a/tests/cli/test_cli_status_bar.py b/tests/cli/test_cli_status_bar.py index eabcd0f96..4a65c6e46 100644 --- a/tests/cli/test_cli_status_bar.py +++ b/tests/cli/test_cli_status_bar.py @@ -237,6 +237,13 @@ class TestCLIStatusBar: cli_obj._spinner_text = "" assert cli_obj._spinner_widget_height(width=90) == 0 + def test_spinner_height_uses_display_width_for_wide_characters(self): + cli_obj = _make_cli() + cli_obj._spinner_text = "你" * 40 + cli_obj._tool_start_time = 0 + + assert cli_obj._spinner_widget_height(width=64) == 2 + def test_voice_status_bar_compacts_on_narrow_terminals(self): cli_obj = _make_cli() cli_obj._voice_mode = True