# NousResearch/hermes-agent · CLI terminal resize corrupts startup banner display

## Open now

- 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.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 2 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #25227. Verify it addresses your case.
- **Recommended:** read PR #25227 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #22999: Classic CLI resize clears startup banner/tool summary after terminal maximize

## Prior & competing PRs

- **#25227: fix(cli): preserve startup banner on terminal resize**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `tests/cli/test_cli_force_redraw.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `scripts/release.py`, `tests/cli/test_cprint_bg_thread.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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):
    ```

    ```diff
    @@ -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)
     
    ```
- **#23048: fix(cli): preserve startup banner on terminal resize**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `e2b2d4861`, `#25227`
- **#23294: fix(cli): preserve startup scrollback on resize**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `e2b2d4861`, `#25227`, `#25974`

## Fix status

- Merged to main in PR #25227 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-14`.
