fix(cli): clamp scrollback box widths + suppress status bar after resize (#25975)

When the terminal shrinks, already-printed box-drawing rules (response,
reasoning, streaming TTS, background-task Panels) reflow into multiple
narrower rows — visible as duplicated horizontal separators / ghost
lines in scrollback. Similarly, prompt_toolkit redraws a fresh status
bar on SIGWINCH on top of one the terminal just reflowed, producing
double-bar artifacts on column shrink.

Two surgical changes:

1. Decorative scrollback boxes now use a new
   `HermesCLI._scrollback_box_width()` helper that clamps to
   `max(32, min(width, 56))`. The live TUI footer is unaffected and still
   uses the full width. Covers: streaming response box (open + close),
   reasoning box (open + close, both streaming and post-stream paths),
   streaming-TTS box close, final-response Rich Panel, and the
   background-task Rich Panel.

2. `_recover_after_resize()` now also sets a new
   `_status_bar_suppressed_after_resize` flag so the dynamic status bar
   and both input separator rules stay hidden until the next user input.
   The flag is cleared in the process loop the moment the user submits
   their next prompt, restoring chrome cleanly.

Tests:
- New `test_input_rules_hide_after_resize_until_next_input` covers the
  flag's effect on rule heights.
- New `test_scrollback_box_width_caps_to_resize_safe_value` covers the
  helper at floor / cap / mid-range / overflow.
- Existing resize-recovery test extended to assert the flag flips.

Refs: #18449 #19280 #22976
Salvage of #24403.

Co-authored-by: Szymonclawd <szymonclawd@mac.home>
This commit is contained in:
Teknium 2026-05-14 15:22:44 -07:00 committed by GitHub
parent f491b07cb2
commit 2844c888f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 98 additions and 8 deletions

View file

@ -79,6 +79,10 @@ class TestForceFullRedraw:
SIGWINCH removes it and ``_replay_output_history`` cannot
reconstruct it. The fix is to only reset the renderer cache and
let ``original_on_resize`` recalculate layout.
Additionally, ``_status_bar_suppressed_after_resize`` must be set
so the input rules and status bar hide until the next user input,
preventing duplicated-bar artifacts on column shrink (#19280).
"""
app = MagicMock()
events = []
@ -86,6 +90,8 @@ class TestForceFullRedraw:
app.invalidate.side_effect = lambda: events.append("invalidate")
original_on_resize = lambda: events.append("original_resize")
# bare_cli skips __init__, so seed the attribute the way __init__ would.
bare_cli._status_bar_suppressed_after_resize = False
bare_cli._recover_after_resize(app, original_on_resize)
assert events == [
@ -97,6 +103,8 @@ class TestForceFullRedraw:
app.renderer.output.erase_screen.assert_not_called()
app.renderer.output.write_raw.assert_not_called()
app.renderer.output.cursor_goto.assert_not_called()
# Status bar / input rules must be suppressed until the next prompt.
assert bare_cli._status_bar_suppressed_after_resize is True
def test_force_redraw_uses_full_screen_clear_without_scrollback_clear(self, bare_cli):
app = MagicMock()

View file

@ -332,6 +332,38 @@ class TestCLIStatusBar:
assert cli_obj._tui_input_rule_height("bottom", width=50) == 0
assert cli_obj._tui_input_rule_height("bottom", width=90) == 1
def test_input_rules_hide_after_resize_until_next_input(self):
"""When _status_bar_suppressed_after_resize is set, both rules hide.
See _recover_after_resize column shrink reflows already-rendered
bars into scrollback, so we hide the separators until the user
submits the next input, at which point the flag is cleared.
"""
cli_obj = _make_cli()
cli_obj._status_bar_suppressed_after_resize = True
assert cli_obj._tui_input_rule_height("top", width=90) == 0
assert cli_obj._tui_input_rule_height("bottom", width=90) == 0
cli_obj._status_bar_suppressed_after_resize = False
assert cli_obj._tui_input_rule_height("top", width=90) == 1
assert cli_obj._tui_input_rule_height("bottom", width=90) == 1
def test_scrollback_box_width_caps_to_resize_safe_value(self):
"""Decorative scrollback boxes clamp to a width small enough that
moderate terminal shrinks don't cause reflow into scrollback."""
from cli import HermesCLI
# Floor at 32 — narrow terminals still get something usable.
assert HermesCLI._scrollback_box_width(20) == 32
assert HermesCLI._scrollback_box_width(32) == 32
# Cap at 56 — wide terminals don't get full-width boxes.
assert HermesCLI._scrollback_box_width(80) == 56
assert HermesCLI._scrollback_box_width(120) == 56
assert HermesCLI._scrollback_box_width(200) == 56
# Mid-range passes through up to the cap.
assert HermesCLI._scrollback_box_width(48) == 48
def test_agent_spacer_reclaimed_on_narrow_terminals(self):
cli_obj = _make_cli()
cli_obj._agent_running = True