# NousResearch/hermes-agent · TUI status bar duplication and rendering bugs

## Open now

- Nothing open on GitHub.

Multiple issues and fixes addressing status bar duplication, ghost rows, and in-place update failures during CLI/TUI sessions. Also includes a related fix to skip a spinner component when the TUI handles tool progress itself.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #3291. Verify it addresses your case.
- **Recommended:** read PR #3291 (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: `resolved` · deciding PR(s): `pr-3291,pr-16527`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.34`: 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

- #3130: TUI: Status bar duplicates and degrades during long sessions
- #12641: bug(cli): Status bar duplicated as new lines when idle (not updating in-place)

## Prior & competing PRs

- **#3291: fix(tui): status bar duplicates and degrades during long sessions**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `tests/test_cli_status_bar.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -1329,7 +1329,12 @@ def _get_status_bar_snapshot(self) -> Dict[str, Any]:
         def _build_status_bar_text(self, width: Optional[int] = None) -> str:
             try:
                 snapshot = self._get_status_bar_snapshot()
    -            width = width or shutil.get_terminal_size((80, 24)).columns
    +            if width is None:
    +                try:
    +                    from prompt_toolkit.application import get_app
    +                    width = get_app().output.get_size().columns
    +                except Exception:
    +                    width = shutil.get_terminal_size((80, 24)).columns
                 percent = snapshot["context_percent"]
                 percent_label = f"{percent}%" if percent is not None else "--"
                 duration_label = snapshot["duration"]
    ```

    ```diff
    @@ -1359,7 +1364,16 @@ def _get_status_bar_fragments(self):
                 return []
             try:
                 snapshot = self._get_status_bar_snapshot()
    -            width = shutil.get_terminal_size((80, 24)).columns
    +            # Use prompt_toolkit's own terminal width when running inside the
    +            # TUI — shutil.get_terminal_size() can return stale or fallback
    +            # values (especially on SSH) that differ from what prompt_toolkit
    +            # actually renders, causing the fragments to overflow to a second
    +            # line and produce duplicated status bar rows over long sessions.
    +            try:
    +                from prompt_toolkit.application import get_app
    +                width = get_app().output.get_size().columns
    +            except Exception:
    +                width = shutil.get_terminal_size((80, 24)).columns
                 duration_label = snapshot["duration"]
     
                 if width < 52:
    ```

    ```diff
    @@ -6894,6 +6908,15 @@ def _get_voice_status():
                 Window(
                     content=FormattedTextControl(lambda: cli_ref._get_status_bar_fragments()),
                     height=1,
    +                # Prevent fragments that overflow the terminal width from
    +                # wrapping onto a second line, which causes the status bar to
    +                # appear duplicated (one full + one partial row) during long
    +                # sessions, especially on SSH where shutil.get_terminal_size
    +                # may return stale values.  _get_status_bar_fragments now reads
    +                # width from prompt_toolkit's own output object, so fragments
    +                # will always fit; wrap_lines=False is the belt-and-suspenders
    +                # guard against any future width mismatch.
    +                wrap_lines=False,
                 ),
                 filter=Condition(lambda: cli_ref._status_bar_visible),
             )
    ```
  - `tests/test_cli_status_bar.py`

    ```diff
    @@ -182,3 +182,94 @@ def test_zero_priced_provider_models_stay_unknown(self, capsys):
             assert "Total cost:" in output
             assert "n/a" in output
             assert "Pricing unknown for glm-5" in output
    +
    +
    +class TestStatusBarWidthSource:
    +    """Ensure status bar fragments don't overflow the terminal width."""
    +
    +    def _make_wide_cli(self):
    +        from datetime import datetime, timedelta
    +        cli_obj = _attach_agent(
    +            _make_cli(),
    +            prompt_tokens=[redacted:secret-assignment]
    +            completion_tokens=[redacted:secret-assignment]
    +            total_tokens=[redacted:secret-assignment]
    +            api_calls=20,
    +            context_tokens=[redacted:secret-assignment]
    +            context_length=200_000,
    +        )
    +        cli_obj._status_bar_visible = True
    +        return cli_obj
    +
    +    def test_fragments_fit_within_announced_width(self):
    +        """Total fragment text length must not exceed the width used to build them."""
    +        from unittest.mock import MagicMock, patch
    +        cli_obj = self._make_wide_cli()
    +
    +        for width in (40, 52, 76, 80, 120, 200):
    +            mock_app = MagicMock()
    +            mock_app.output.get_size.return_value = MagicMock(columns=width)
    +
    +            with patch("prompt_toolkit.application.get_app", return_value=mock_app):
    +                frags = cli_obj._get_status_bar_fragments()
    +
    +            total_text = "".join(text for _, text in frags)
    +            assert len(total_text) <= width + 4, (  # +4 for minor padding chars
    +                f"At width={width}, fragment total {len(total_text)} chars overflows "
    +                f"({total_text!r})"
    +            )
    +
    +    def test_fragments_use_pt_width_over_shutil(self):
    +        """When prompt_toolkit reports a width, shutil.get_terminal_size must not be used."""
    +        from unittest.mock import MagicMock, patch
    +        cli_obj = self._make_wide_cli()
    +
    +        mock_app = MagicMock()
    +        mock_app.output.get_size.return_value = MagicMock(columns=120)
    +
    +        with patch("prompt_toolkit.application.get_app", return_value=mock_app) as mock_get_app, \
    +             patch("shutil.get_terminal_size") as mock_shutil:
    +            cli_obj._get_status_bar_fragments()
    +
    +        mock_shutil.assert_not_called()
    +
    +    def test_fragments_fall_back_to_shutil_when_no_app(self):
    +        """Outside a TUI context (no running app), shutil must be used as fallback."""
    +        from unittest.mock import MagicMock, patch
    +        cli_obj = self._make_wide_cli()
    +
    +        with patch("prompt_toolkit.application.get_app", side_effect=Exception("no app")), \
    +             patch("shutil.get_terminal_size", return_value=MagicMock(columns=100)) as mock_shutil:
    +            frags = cli_obj._get_status_bar_fragments()
    +
    +        mock_shutil.assert_called()
    +        assert len(frags) > 0
    +
    +    def test_build_status_bar_text_uses_pt_width(self):
    +        """_build_status_bar_text() must also prefer prompt_toolkit width."""
    +        from unittest.mock import MagicMock, patch
    +        cli_obj = self._make_wide_cli()
    +
    +        mock_app = MagicMock()
    +        mock_app.output.get_size.return_value = MagicMock(columns=80)
    +
    +        with patch("prompt_toolkit.application.get_app", return_value=mock_app), \
    +             patch("shutil.get_terminal_size") as mock_shutil:
    +            text = cli_obj._build_status_bar_text()  # no explicit width
    +
    +        mock_shutil.assert_not_called()
    +        assert isinstance(text, str)
    +        assert len(text) > 0
    +
    +    def test_explicit_width_skips_pt_lookup(self):
    +        """An explicit width= argument must bypass both PT and shutil lookups."""
    +        from unittest.mock import patch
    +        cli_obj = self._make_wide_cli()
    +
    +        with patch("prompt_toolkit.application.get_app") as mock_get_app, \
    +             patch("shutil.get_terminal_size") as mock_shutil:
    +            text = cli_obj._build_status_bar_text(width=100)
    +
    +        mock_get_app.assert_not_called()
    +        mock_shutil.assert_not_called()
    +        assert len(text) > 0
    ```
- **#16527: fix(cli): eliminate ghost status-bar + DSR input leaks from terminal drift**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `hermes_cli/commands.py`, `scripts/release.py`, `tests/cli/test_cli_force_redraw.py`, `tests/cli/test_cli_terminal_response_sanitizer.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -1574,6 +1574,34 @@ def _strip_leaked_bracketed_paste_wrappers(text: str) -> str:
         return text
     
     
    +# Cursor Position Report (CPR / DSR) response, format ``ESC[<row>;<col>R``.
    +# prompt_toolkit's _on_resize() + renderer send ``ESC[6n`` queries to the
    +# terminal; under resize storms or tab switches the terminal's reply can
    +# race past the input parser and end up in the input buffer as literal
    +# text (see issue #14692). Also matches the visible-form ``^[[<row>;<col>R``
    +# that appears when the ESC byte was stripped by a prior filter.
    +_DSR_CPR_ESC_RE = re.compile(r"\x1b\[\d+;\d+R")
    +_DSR_CPR_VISIBLE_RE = re.compile(r"\^\[\[\d+;\d+R")
    +
    +
    +def _strip_leaked_terminal_responses(text: str) -> str:
    +    """Strip leaked terminal control-response sequences from user input.
    +
    +    Covers Cursor Position Report (CPR / DSR) responses — ``ESC[<row>;<col>R``
    +    and the visible ``^[[<row>;<col>R`` form. These are replies the terminal
    +    sends back to queries prompt_toolkit makes during ``_on_resize`` /
    +    ``_request_absolute_cursor_position``. When the input parser drops one
    +    (resize storms, multiplexer focus changes, slow PTYs) the response
    +    lands in the input buffer as literal text and corrupts what the user
    +    typed.
    +    """
    +    if not text:
    +        return text
    +    text = _DSR_CPR_ESC_RE.sub("", text)
    +    text = _DSR_CPR_VISIBLE_RE.sub("", text)
    +    return text
    +
    +
     def _collect_query_images(query: str | None, image_arg: str | None = None) -> tuple[str, list[Path]]:
         """Collect local image attachments for single-query CLI flows."""
         message = query or ""
    ```

    ```diff
    @@ -2182,6 +2210,42 @@ def _invalidate(self, min_interval: float = 0.25) -> None:
                 self._last_invalidate = now
                 self._app.invalidate()
     
    +    def _force_full_redraw(self) -> None:
    +        """Force a clean full-screen repaint of the prompt_toolkit UI.
    +
    +        Used to recover from terminal buffer drift caused by external
    +        redraws we can't detect — e.g. macOS cmux / tmux tab switches,
    +        ``clear`` issued from a subshell, or SSH window restores. These
    +        wipe or repaint the terminal without firing SIGWINCH, so
    +        prompt_toolkit's tracked ``_cursor_pos`` no longer matches reality
    +        and the next incremental redraw stacks on top of stale content
    +        (ghost status bars, duplicated prompts).
    +
    +        Bound to Ctrl+L and exposed as the ``/redraw`` slash command,
    +        matching the standard terminal-UX convention (bash, zsh, fish,
    +        vim, htop).
    +        """
    +        app = getattr(self, "_app", None)
    +        if not app:
    +            return
    +        try:
    +            renderer = app.renderer
    +            out = renderer.output
    +            out.reset_attributes()
    +            out.erase_screen()
    +            out.cursor_goto(0, 0)
    +            out.flush()
    +            # Drop prompt_toolkit's cached screen + cursor state so the
    +            # next _redraw() starts from a known (0, 0) origin and
    +            # re-renders every cell rather than diffing against stale.
    +            renderer.reset(leave_alternate_screen=False)
    +        except Exception:
    +            pass
    +        try:
    +            app.invalidate()
    +        except Exception:
    +            pass
    +
         def _status_bar_context_style(self, percent_used: Optional[int]) -> str:
             if percent_used is None:
                 return "class:status-bar-dim"
    ```

    ```diff
    @@ -5998,6 +6062,12 @@ def process_command(self, command: str) -> bool:
                 self.show_toolsets()
             elif canonical == "config":
                 self.show_config()
    +        elif canonical == "redraw":
    +            # Manual recovery for terminal buffer drift from multiplexer
    +            # tab switches, subshell ``clear``, SSH window restores, etc.
    +            # See issue #8688 (cmux). Ctrl+L is bound to the same helper.
    +            self._force_full_redraw()
    +            _cprint(f"  {_DIM}✓ UI redrawn{_RST}")
             elif canonical == "clear":
                 self.new_session(silent=True)
                 # Clear terminal screen.  Inside the TUI, Rich's console.clear()
    ```

    ```diff
    @@ -9563,6 +9633,17 @@ def history_down(event):
                 """Down arrow: browse history when on last line, else move cursor down."""
                 event.app.current_buffer.auto_down(count=event.arg)
     
    +        @kb.add('c-l')
    +        def handle_ctrl_l(event):
    +            """Ctrl+L: force a clean full-screen repaint.
    +
    +            Recovers the UI after external terminal buffer drift — tmux /
    +            cmux tab switches, ``clear`` from a subshell, SSH window
    +            restores, etc. — that prompt_toolkit can't detect on its own.
    +            Matches the universal bash/zsh/fish/vim/htop convention.
    +            """
    +            self._force_full_redraw()
    +
             @kb.add('c-c')
             def handle_ctrl_c(event):
                 """Handle Ctrl+C - cancel interactive prompts, interrupt agent, or exit.
    ```

    ```diff
    @@ -9795,6 +9876,7 @@ def handle_paste(event):
                 # so the 5-line collapse threshold and display are consistent.
                 pasted_text = pasted_text.replace('\r\n', '\n').replace('\r', '\n')
                 pasted_text = _strip_leaked_bracketed_paste_wrappers(pasted_text)
    +            pasted_text = _strip_leaked_terminal_responses(pasted_text)
                 if _should_auto_attach_clipboard_image_on_paste(pasted_text) and self._try_attach_clipboard_image():
                     event.app.invalidate()
                 if pasted_text:
    ```

    ```diff
    @@ -9937,6 +10019,7 @@ def _on_text_changed(buf):
                    event so it never triggers this.
                 """
                 text = _strip_leaked_bracketed_paste_wrappers(buf.text)
    +            text = _strip_leaked_terminal_responses(text)
                 if text != buf.text:
                     cursor = min(buf.cursor_position, len(text))
                     _paste_just_collapsed[0] = True
    ```

    ```diff
    @@ -10601,36 +10684,30 @@ def _get_voice_status():
             # only cursor_up()s by the stored layout height, missing the extra
             # rows created by reflow — leaving ghost duplicates visible.
             #
    -        # Fix: before the standard erase, inflate _cursor_pos.y so the
    -        # cursor moves up far enough to cover the reflowed ghost content.
    +        # It's not just column-shrink: widening, row-shrinking, and
    +        # multiplexer-driven SIGWINCH-less redraws (cmux / tmux tab switch)
    +        # all produce the same class of drift, where the renderer's tracked
    +        # _cursor_pos.y no longer matches terminal reality. The only reliable
    +        # recovery is a full screen-clear (\x1b[2J\x1b[H) before the next
    +        # redraw, so we force one on every resize rather than trying to
    +        # compute the exact drift.
             _original_on_resize = app._on_resize
     
             def _resize_clear_ghosts():
    -            from prompt_toolkit.data_structures import Point as _Pt
                 renderer = app.renderer
                 try:
    -                old_size = renderer._last_size
    -                new_size = renderer.output.get_size()
    -                if (
    -                    old_size
    -                    and new_size.columns < old_size.columns
    -                    and new_size.columns > 0
    -                ):
    -                    reflow_factor = (
    -                        (old_size.columns + new_size.columns - 1)
    -                        // new_size.columns
    -                    )
    -                    last_h = (
    -                        renderer._last_screen.height
    -                        if renderer._last_screen
    -                        else 0
    -                    )
    -                    extra = last_h * (reflow_factor - 1)
    -                    if extra > 0:
    -                        renderer._cursor_pos = _Pt(
    -                            x=renderer._cursor_pos.x,
    -                            y=renderer._cursor_pos.y + extra,
    -                        )
    +                out = renderer.output
    +                # Reset attributes, erase the entire screen, and home the
    +                # cursor. This overwrites any reflowed status-bar rows or
    +                # stale content the terminal kept from the prior layout.
    +                out.reset_attributes()
    +                out.erase_screen()
    +                out.cursor_goto(0, 0)
    +                out.flush()
    +                # Tell the renderer its tracked position is fresh so its
    +                # own erase() inside _on_resize doesn't cursor_up() past
    +                # the top of the screen.
    +                renderer.reset(leave_alternate_screen=False)
                 except Exception:
                     pass  # never break resize handling
                 _original_on_resize()
    ```

    ```diff
    @@ -10638,7 +10715,6 @@ def _resize_clear_ghosts():
             app._on_resize = _resize_clear_ghosts
     
             def spinner_loop():
    -            last_idle_refresh = 0.0
                 while not self._should_exit:
                     if not self._app:
                         time.sleep(0.1)
    ```

    ```diff
    @@ -10647,10 +10723,11 @@ def spinner_loop():
                         self._invalidate(min_interval=0.1)
                         time.sleep(0.1)
                     else:
    -                    now = time.monotonic()
    -                    if now - last_idle_refresh >= 1.0:
    -                        last_idle_refresh = now
    -                        self._invalidate(min_interval=1.0)
    +                    # Do not repaint the idle prompt every second. In non-full-screen
    +                    # prompt_toolkit mode, background redraws can fight tmux/Ghostty/cmux
    +                    # viewport restoration after focus changes and visually move the
    +                    # command input area. Keep idle stable; input/agent events still
    +                    # invalidate explicitly when the UI actually changes.
                         time.sleep(0.2)
     
             spinner_thread = threading.Thread(target=spinner_loop, daemon=True)
    ```

    ```diff
    @@ -10695,6 +10772,7 @@ def process_loop():
     
                         if isinstance(user_input, str):
                             user_input = _strip_leaked_bracketed_paste_wrappers(user_input)
    +                        user_input = _strip_leaked_terminal_responses(user_input)
                         
                         # Check for commands — but detect dragged/pasted file paths first.
                         # See _detect_file_drop() for details.
    ```
  - `hermes_cli/commands.py`

    ```diff
    @@ -62,6 +62,8 @@ class CommandDef:
                    aliases=("reset",)),
         CommandDef("clear", "Clear screen and start a new session", "Session",
                    cli_only=True),
    +    CommandDef("redraw", "Force a full UI repaint (recovers from terminal drift)", "Session",
    +               cli_only=True),
         CommandDef("history", "Show conversation history", "Session",
                    cli_only=True),
         CommandDef("save", "Save the current conversation", "Session",
    ```
  - `scripts/release.py`

    ```diff
    @@ -61,6 +61,7 @@
         "[redacted:email]": "BadTechBandit",
         "[redacted:email]": "Yoimex",
         "[redacted:email]": "romanornr",
    +    "[redacted:email]": "foxion37",
         # contributors (from noreply pattern)
         "[redacted:email]": "davidvv",
         "[redacted:email]": "xiaoqiang243",
    ```
  - `tests/cli/test_cli_force_redraw.py`

    ```diff
    @@ -0,0 +1,73 @@
    +"""Tests for CLI redraw helpers used to recover from terminal buffer drift.
    +
    +Covers:
    +  - _force_full_redraw (#8688 cmux tab switch, /redraw, Ctrl+L)
    +  - the resize handler we install over prompt_toolkit's _on_resize (#5474)
    +
    +Both behaviors are exercised against fake prompt_toolkit renderer/output
    +objects — we're asserting the escape sequences the CLI sends, not that
    +the terminal physically repainted.
    +"""
    +
    +from unittest.mock import MagicMock
    +
    +import pytest
    +
    +from cli import HermesCLI
    +
    +
    [redacted:email]
    +def bare_cli():
    +    """A HermesCLI with no __init__ — we only exercise the redraw helper."""
    +    cli = object.__new__(HermesCLI)
    +    return cli
    +
    +
    +class TestForceFullRedraw:
    +    def test_no_app_is_safe(self, bare_cli):
    +        # _force_full_redraw must be a no-op when the TUI isn't running.
    +        bare_cli._app = None
    +        bare_cli._force_full_redraw()  # must not raise
    +
    +    def test_missing_app_attr_is_safe(self, bare_cli):
    +        # Simulate HermesCLI before the TUI has ever been constructed.
    +        bare_cli._force_full_redraw()  # must not raise
    +
    +    def test_sends_full_clear_and_invalidates(self, bare_cli):
    +        app = MagicMock()
    +        out = app.renderer.output
    +        bare_cli._app = app
    +
    +        bare_cli._force_full_redraw()
    +
    +        # Must erase screen, home cursor, and flush — in that order.
    +        out.reset_attributes.assert_called_once()
    +        out.erase_screen.assert_called_once()
    +        out.cursor_goto.assert_called_once_with(0, 0)
    +        out.flush.assert_called_once()
    +
    +        # Must reset prompt_toolkit's tracked screen/cursor state so the
    +        # next incremental redraw starts from a clean (0, 0) origin.
    +        app.renderer.reset.assert_called_once_with(leave_alternate_screen=False)
    +
    +        # Must schedule a repaint.
    +        app.invalidate.assert_called_once()
    +
    +    def test_swallows_renderer_exceptions(self, bare_cli):
    +        # If the renderer blows up for any reason, the helper must not
    +        # propagate — otherwise a stray Ctrl+L would crash the CLI.
    +        app = MagicMock()
    +        app.renderer.output.erase_screen.side_effect = RuntimeError("boom")
    +        bare_cli._app = app
    +
    +        bare_cli._force_full_redraw()  # must not raise
    +
    +        # invalidate() is still attempted after a renderer failure.
    +        app.invalidate.assert_called_once()
    +
    +    def test_swallows_invalidate_exceptions(self, bare_cli):
    +        app = MagicMock()
    +        app.invalidate.side_effect = RuntimeError("boom")
    +        bare_cli._app = app
    +
    +        bare_cli._force_full_redraw()  # must not raise
    ```
  - `tests/cli/test_cli_terminal_response_sanitizer.py`

    ```diff
    @@ -0,0 +1,57 @@
    +"""Tests for defensive terminal control-response stripping in the CLI.
    +
    +Covers Cursor Position Report (CPR / DSR) responses that occasionally
    +leak into the input buffer after terminal resize storms or multiplexer
    +tab switches — see issue #14692.
    +"""
    +
    +from cli import _strip_leaked_terminal_responses
    +
    +
    +class TestStripLeakedTerminalResponses:
    +    def test_plain_text_unchanged(self):
    +        text = "hello world"
    +        assert _strip_leaked_terminal_responses(text) == text
    +
    +    def test_empty_text(self):
    +        assert _strip_leaked_terminal_responses("") == ""
    +
    +    def test_strips_canonical_dsr_response(self):
    +        # Reports from issue #14692
    +        text = "\x1b[53;1R"
    +        assert _strip_leaked_terminal_responses(text) == ""
    +
    +    def test_strips_dsr_response_in_middle_of_text(self):
    +        text = "hello\x1b[53;1Rworld"
    +        assert _strip_leaked_terminal_responses(text) == "helloworld"
    +
    +    def test_strips_multiple_dsr_responses(self):
    +        text = "a\x1b[53;1Rb\x1b[51;1Rc\x1b[50;9Rd"
    +        assert _strip_leaked_terminal_responses(text) == "abcd"
    +
    +    def test_strips_visible_form_dsr(self):
    +        # When an upstream filter has already stripped the ESC byte and
    +        # left the caret-escape representation in place.
    +        text = "^[[53;1R"
    +        assert _strip_leaked_terminal_responses(text) == ""
    +
    +    def test_strips_visible_form_dsr_in_middle_of_text(self):
    +        text = "typed^[[53;1Rmore"
    +        assert _strip_leaked_terminal_responses(text) == "typedmore"
    +
    +    def test_does_not_strip_user_text_with_R(self):
    +        # Don't over-match; user might genuinely type text containing [N;NR patterns.
    +        # Our regex requires the leading ESC or caret-escape, so bare
    +        # "[53;1R" as user text is preserved.
    +        text = "see section [53;1R for details"
    +        assert _strip_leaked_terminal_responses(text) == text
    +
    +    def test_does_not_strip_sgr_sequences(self):
    +        # Sanity: don't wipe legitimate terminal control sequences that
    +        # aren't DSR responses.
    +        text = "\x1b[31mred\x1b[0m"
    +        assert _strip_leaked_terminal_responses(text) == text
    +
    +    def test_preserves_multiline_content(self):
    +        text = "line 1\n\x1b[53;1Rline 2"
    +        assert _strip_leaked_terminal_responses(text) == "line 1\nline 2"
    ```
- **#3181: fix(tui): status bar duplicates and degrades during long sessions**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#3291`

## Fix status

- Merged to main in PR #3291 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-27`.
- Subsystem: `hermes_cli`
