CLI terminal resize corrupts startup banner display
NousResearch/hermes-agent · cluster 22999
Data as of
Nothing open on GitHub. Fix merged in #25227. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #25227. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and pull requests address problems where resizing the terminal window corrupts, clears, or duplicates the startup banner, tool summary, and related display elements in the CLI. Fixes involve managing ANSI styling, viewport clearing, and scrollback preservation during resize events.
Open issues0
Open PRs0
Fixes on record
-
#25227 fix(cli): preserve startup banner on terminal resize
files:
cli.py,tests/cli/test_cli_force_redraw.pyShow the diff
-
cli.py@@ -2703,9 +2703,27 @@ def _clear_prompt_toolkit_screen(self, app, *, rebuild_scrollback: bool = False) pass def _recover_after_resize(self, app, original_on_resize) -> None: - """Recover a resized classic CLI without desynchronizing cursor state.""" - self._clear_prompt_toolkit_screen(app, rebuild_scrollback=True) - _replay_output_history() + """Recover a resized classic CLI without desynchronizing cursor state. + + Unlike _force_full_redraw, we do NOT clear the physical screen or + scrollback here. The startup banner and tool summary are printed + before prompt_toolkit owns the live chrome, so they live in normal + terminal scrollback. Erasing the screen on SIGWINCH removes that + startup UI and ``_replay_output_history`` cannot reconstruct it + (the banner was never added to ``_OUTPUT_HISTORY``). + + Instead we just reset prompt_toolkit's renderer cache so the next + incremental redraw starts from a clean slate, then let + ``original_on_resize`` recalculate layout for the new size. + """ + try: + app.renderer.reset(leave_alternate_screen=False) + except Exception: + pass + try: + app.invalidate() + except Exception: + pass original_on_resize() def _schedule_resize_recovery(self, app, original_on_resize, delay: float = 0.12) -> None: -
tests/cli/test_cli_force_redraw.py@@ -71,32 +71,32 @@ def test_sends_full_clear_replays_then_invalidates(self, bare_cli, monkeypatch): "invalidate", ] - def test_resize_rebuilds_scrollback_before_prompt_toolkit_redraw(self, bare_cli, monkeypatch): + def test_resize_preserves_scrollback_and_resets_renderer(self, bare_cli, monkeypatch): + """Resize recovery must NOT erase screen or scrollback. + + The startup banner lives in normal terminal scrollback (printed + before prompt_toolkit owns the chrome). Clearing scrollback on + 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. + """ app = MagicMock() - out = app.renderer.output events = [] - out.reset_attributes.side_effect = lambda: events.append("reset_attrs") - out.erase_screen.side_effect = lambda: events.append("erase") - out.write_raw.side_effect = lambda text: events.append(("raw", text)) - out.cursor_goto.side_effect = lambda *_: events.append("home") - out.flush.side_effect = lambda: events.append("flush") app.renderer.reset.side_effect = lambda **_: events.append("renderer_reset") - monkeypatch.setattr(cli_mod, "_replay_output_history", lambda: events.append("replay")) + app.invalidate.side_effect = lambda: events.append("invalidate") original_on_resize = lambda: events.append("original_resize") bare_cli._recover_after_resize(app, original_on_resize) assert events == [ - "reset_attrs", - "erase", - ("raw", "\x1b[3J"), - "home", - "flush", "renderer_reset", - "replay", + "invalidate", "original_resize", ] - app.invalidate.assert_not_called() + # Must NOT clear the screen or scrollback — those destroy the banner. + app.renderer.output.erase_screen.assert_not_called() + app.renderer.output.write_raw.assert_not_called() + app.renderer.output.cursor_goto.assert_not_called() def test_force_redraw_uses_full_screen_clear_without_scrollback_clear(self, bare_cli): app = MagicMock()
-
-
#25974 fix(cli): preserve ANSI styling in resize replay history (salvage #24292)
files:
cli.py,scripts/release.py,tests/cli/test_cprint_bg_thread.pyShow the diff
-
cli.py@@ -1415,9 +1415,6 @@ def _render_final_assistant_content(text: str, mode: str = "render"): _OUTPUT_HISTORY_SUPPRESSED = False _OUTPUT_HISTORY_MAX_LINES = 200 _OUTPUT_HISTORY = deque(maxlen=_OUTPUT_HISTORY_MAX_LINES) -_ANSI_CONTROL_RE = re.compile( - r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))" -) def _coerce_output_history_limit(value) -> int: @@ -1459,10 +1456,10 @@ def _record_output_history_entry(entry) -> None: def _record_output_history(text: str) -> None: if not _OUTPUT_HISTORY_ENABLED or _OUTPUT_HISTORY_REPLAYING or _OUTPUT_HISTORY_SUPPRESSED: return - clean = _ANSI_CONTROL_RE.sub("", str(text)).replace("\r", "").rstrip("\n") - if not clean: + normalized = str(text).replace("\r", "").rstrip("\n") + if not normalized: return - for line in clean.splitlines(): + for line in normalized.splitlines(): _record_output_history_entry(line) -
scripts/release.py@@ -146,6 +146,7 @@ "[redacted:email]": "SandroHub013", "[redacted:email]": "maciekczech", "[redacted:email]": "LeonSGP43", + "[redacted:email]": "LeonSGP43", "[redacted:email]": "zeejaytan", "[redacted:email]": "Aslaaen", "[redacted:email]": "Tkander1715", -
tests/cli/test_cprint_bg_thread.py@@ -215,13 +215,15 @@ def find_spec(self, name, path=None, target=None): assert direct_prints == ["fallback2"] -def test_output_history_strips_ansi_and_keeps_recent_lines(): +def test_output_history_preserves_ansi_and_keeps_recent_lines(): cli._configure_output_history(True, 10) for idx in range(12): cli._record_output_history(f"\x1b[31mline-{idx}\x1b[0m") - assert list(cli._OUTPUT_HISTORY) == [f"line-{idx}" for idx in range(2, 12)] + assert list(cli._OUTPUT_HISTORY) == [ + f"\x1b[31mline-{idx}\x1b[0m" for idx in range(2, 12) + ] def test_replay_output_history_does_not_record_replayed_lines(monkeypatch): @@ -277,6 +279,16 @@ def test_replay_output_history_batches_rendered_lines_into_one_print(monkeypatch assert printed == ["first line\nsecond line\nthird line\nfourth line"] +def test_chat_console_records_rich_ansi_for_resize_replay(monkeypatch): + cli._configure_output_history(True, 10) + monkeypatch.setattr(cli, "_pt_print", lambda *_args, **_kwargs: None) + + cli.ChatConsole().print("[bold red]Hello[/]") + + assert cli._OUTPUT_HISTORY + assert any("\x1b[" in line for line in cli._OUTPUT_HISTORY) + + def test_suspend_output_history_blocks_recording(): cli._configure_output_history(True, 10)
-
2 PRs closed without merging
From the close comments: done elsewhere 1 · merged via another change 1
Closed issues · 1
- Closed#22999Classic CLI resize clears startup banner/tool summary after terminal maximize
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab