{"repo":"NousResearch/hermes-agent","public_id":"3130","display_label":"TUI status bar duplication and rendering bugs","summary":"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.","subsystem":"hermes_cli","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":2,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":3291},"resolution":{"state":"resolved","by":"pr-3291,pr-16527"},"freshness":{"state":"verified","last_verified_at":"2026-04-27T12:31:47+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":3130,"pr_number":3291,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":12641,"pr_number":16527,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":3130,"title":"TUI: Status bar duplicates and degrades during long sessions","url":null,"state":"closed"},{"kind":"issue","number":12641,"title":"bug(cli): Status bar duplicated as new lines when idle (not updating in-place)","url":null,"state":"closed"},{"kind":"pr","number":3181,"title":"fix(tui): status bar duplicates and degrades during long sessions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#3291","number":3291}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":3291,"title":"fix(tui): status bar duplicates and degrades during long sessions","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","tests/test_cli_status_bar.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -1329,7 +1329,12 @@ def _get_status_bar_snapshot(self) -> Dict[str, Any]:\n     def _build_status_bar_text(self, width: Optional[int] = None) -> str:\n         try:\n             snapshot = self._get_status_bar_snapshot()\n-            width = width or shutil.get_terminal_size((80, 24)).columns\n+            if width is None:\n+                try:\n+                    from prompt_toolkit.application import get_app\n+                    width = get_app().output.get_size().columns\n+                except Exception:\n+                    width = shutil.get_terminal_size((80, 24)).columns\n             percent = snapshot[\"context_percent\"]\n             percent_label = f\"{percent}%\" if percent is not None else \"--\"\n             duration_label = snapshot[\"duration\"]","@@ -1359,7 +1364,16 @@ def _get_status_bar_fragments(self):\n             return []\n         try:\n             snapshot = self._get_status_bar_snapshot()\n-            width = shutil.get_terminal_size((80, 24)).columns\n+            # Use prompt_toolkit's own terminal width when running inside the\n+            # TUI — shutil.get_terminal_size() can return stale or fallback\n+            # values (especially on SSH) that differ from what prompt_toolkit\n+            # actually renders, causing the fragments to overflow to a second\n+            # line and produce duplicated status bar rows over long sessions.\n+            try:\n+                from prompt_toolkit.application import get_app\n+                width = get_app().output.get_size().columns\n+            except Exception:\n+                width = shutil.get_terminal_size((80, 24)).columns\n             duration_label = snapshot[\"duration\"]\n \n             if width < 52:","@@ -6894,6 +6908,15 @@ def _get_voice_status():\n             Window(\n                 content=FormattedTextControl(lambda: cli_ref._get_status_bar_fragments()),\n                 height=1,\n+                # Prevent fragments that overflow the terminal width from\n+                # wrapping onto a second line, which causes the status bar to\n+                # appear duplicated (one full + one partial row) during long\n+                # sessions, especially on SSH where shutil.get_terminal_size\n+                # may return stale values.  _get_status_bar_fragments now reads\n+                # width from prompt_toolkit's own output object, so fragments\n+                # will always fit; wrap_lines=False is the belt-and-suspenders\n+                # guard against any future width mismatch.\n+                wrap_lines=False,\n             ),\n             filter=Condition(lambda: cli_ref._status_bar_visible),\n         )"]},{"file":"tests/test_cli_status_bar.py","hunks":["@@ -182,3 +182,94 @@ def test_zero_priced_provider_models_stay_unknown(self, capsys):\n         assert \"Total cost:\" in output\n         assert \"n/a\" in output\n         assert \"Pricing unknown for glm-5\" in output\n+\n+\n+class TestStatusBarWidthSource:\n+    \"\"\"Ensure status bar fragments don't overflow the terminal width.\"\"\"\n+\n+    def _make_wide_cli(self):\n+        from datetime import datetime, timedelta\n+        cli_obj = _attach_agent(\n+            _make_cli(),\n+            prompt_tokens=[redacted:secret-assignment]\n+            completion_tokens=[redacted:secret-assignment]\n+            total_tokens=[redacted:secret-assignment]\n+            api_calls=20,\n+            context_tokens=[redacted:secret-assignment]\n+            context_length=200_000,\n+        )\n+        cli_obj._status_bar_visible = True\n+        return cli_obj\n+\n+    def test_fragments_fit_within_announced_width(self):\n+        \"\"\"Total fragment text length must not exceed the width used to build them.\"\"\"\n+        from unittest.mock import MagicMock, patch\n+        cli_obj = self._make_wide_cli()\n+\n+        for width in (40, 52, 76, 80, 120, 200):\n+            mock_app = MagicMock()\n+            mock_app.output.get_size.return_value = MagicMock(columns=width)\n+\n+            with patch(\"prompt_toolkit.application.get_app\", return_value=mock_app):\n+                frags = cli_obj._get_status_bar_fragments()\n+\n+            total_text = \"\".join(text for _, text in frags)\n+            assert len(total_text) <= width + 4, (  # +4 for minor padding chars\n+                f\"At width={width}, fragment total {len(total_text)} chars overflows \"\n+                f\"({total_text!r})\"\n+            )\n+\n+    def test_fragments_use_pt_width_over_shutil(self):\n+        \"\"\"When prompt_toolkit reports a width, shutil.get_terminal_size must not be used.\"\"\"\n+        from unittest.mock import MagicMock, patch\n+        cli_obj = self._make_wide_cli()\n+\n+        mock_app = MagicMock()\n+        mock_app.output.get_size.return_value = MagicMock(columns=120)\n+\n+        with patch(\"prompt_toolkit.application.get_app\", return_value=mock_app) as mock_get_app, \\\n+             patch(\"shutil.get_terminal_size\") as mock_shutil:\n+            cli_obj._get_status_bar_fragments()\n+\n+        mock_shutil.assert_not_called()\n+\n+    def test_fragments_fall_back_to_shutil_when_no_app(self):\n+        \"\"\"Outside a TUI context (no running app), shutil must be used as fallback.\"\"\"\n+        from unittest.mock import MagicMock, patch\n+        cli_obj = self._make_wide_cli()\n+\n+        with patch(\"prompt_toolkit.application.get_app\", side_effect=Exception(\"no app\")), \\\n+             patch(\"shutil.get_terminal_size\", return_value=MagicMock(columns=100)) as mock_shutil:\n+            frags = cli_obj._get_status_bar_fragments()\n+\n+        mock_shutil.assert_called()\n+        assert len(frags) > 0\n+\n+    def test_build_status_bar_text_uses_pt_width(self):\n+        \"\"\"_build_status_bar_text() must also prefer prompt_toolkit width.\"\"\"\n+        from unittest.mock import MagicMock, patch\n+        cli_obj = self._make_wide_cli()\n+\n+        mock_app = MagicMock()\n+        mock_app.output.get_size.return_value = MagicMock(columns=80)\n+\n+        with patch(\"prompt_toolkit.application.get_app\", return_value=mock_app), \\\n+             patch(\"shutil.get_terminal_size\") as mock_shutil:\n+            text = cli_obj._build_status_bar_text()  # no explicit width\n+\n+        mock_shutil.assert_not_called()\n+        assert isinstance(text, str)\n+        assert len(text) > 0\n+\n+    def test_explicit_width_skips_pt_lookup(self):\n+        \"\"\"An explicit width= argument must bypass both PT and shutil lookups.\"\"\"\n+        from unittest.mock import patch\n+        cli_obj = self._make_wide_cli()\n+\n+        with patch(\"prompt_toolkit.application.get_app\") as mock_get_app, \\\n+             patch(\"shutil.get_terminal_size\") as mock_shutil:\n+            text = cli_obj._build_status_bar_text(width=100)\n+\n+        mock_get_app.assert_not_called()\n+        mock_shutil.assert_not_called()\n+        assert len(text) > 0\n"]}]},{"kind":"pr","number":16527,"title":"fix(cli): eliminate ghost status-bar + DSR input leaks from terminal drift","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"cli.py","hunks":["@@ -1574,6 +1574,34 @@ def _strip_leaked_bracketed_paste_wrappers(text: str) -> str:\n     return text\n \n \n+# Cursor Position Report (CPR / DSR) response, format ``ESC[<row>;<col>R``.\n+# prompt_toolkit's _on_resize() + renderer send ``ESC[6n`` queries to the\n+# terminal; under resize storms or tab switches the terminal's reply can\n+# race past the input parser and end up in the input buffer as literal\n+# text (see issue #14692). Also matches the visible-form ``^[[<row>;<col>R``\n+# that appears when the ESC byte was stripped by a prior filter.\n+_DSR_CPR_ESC_RE = re.compile(r\"\\x1b\\[\\d+;\\d+R\")\n+_DSR_CPR_VISIBLE_RE = re.compile(r\"\\^\\[\\[\\d+;\\d+R\")\n+\n+\n+def _strip_leaked_terminal_responses(text: str) -> str:\n+    \"\"\"Strip leaked terminal control-response sequences from user input.\n+\n+    Covers Cursor Position Report (CPR / DSR) responses — ``ESC[<row>;<col>R``\n+    and the visible ``^[[<row>;<col>R`` form. These are replies the terminal\n+    sends back to queries prompt_toolkit makes during ``_on_resize`` /\n+    ``_request_absolute_cursor_position``. When the input parser drops one\n+    (resize storms, multiplexer focus changes, slow PTYs) the response\n+    lands in the input buffer as literal text and corrupts what the user\n+    typed.\n+    \"\"\"\n+    if not text:\n+        return text\n+    text = _DSR_CPR_ESC_RE.sub(\"\", text)\n+    text = _DSR_CPR_VISIBLE_RE.sub(\"\", text)\n+    return text\n+\n+\n def _collect_query_images(query: str | None, image_arg: str | None = None) -> tuple[str, list[Path]]:\n     \"\"\"Collect local image attachments for single-query CLI flows.\"\"\"\n     message = query or \"\"","@@ -2182,6 +2210,42 @@ def _invalidate(self, min_interval: float = 0.25) -> None:\n             self._last_invalidate = now\n             self._app.invalidate()\n \n+    def _force_full_redraw(self) -> None:\n+        \"\"\"Force a clean full-screen repaint of the prompt_toolkit UI.\n+\n+        Used to recover from terminal buffer drift caused by external\n+        redraws we can't detect — e.g. macOS cmux / tmux tab switches,\n+        ``clear`` issued from a subshell, or SSH window restores. These\n+        wipe or repaint the terminal without firing SIGWINCH, so\n+        prompt_toolkit's tracked ``_cursor_pos`` no longer matches reality\n+        and the next incremental redraw stacks on top of stale content\n+        (ghost status bars, duplicated prompts).\n+\n+        Bound to Ctrl+L and exposed as the ``/redraw`` slash command,\n+        matching the standard terminal-UX convention (bash, zsh, fish,\n+        vim, htop).\n+        \"\"\"\n+        app = getattr(self, \"_app\", None)\n+        if not app:\n+            return\n+        try:\n+            renderer = app.renderer\n+            out = renderer.output\n+            out.reset_attributes()\n+            out.erase_screen()\n+            out.cursor_goto(0, 0)\n+            out.flush()\n+            # Drop prompt_toolkit's cached screen + cursor state so the\n+            # next _redraw() starts from a known (0, 0) origin and\n+            # re-renders every cell rather than diffing against stale.\n+            renderer.reset(leave_alternate_screen=False)\n+        except Exception:\n+            pass\n+        try:\n+            app.invalidate()\n+        except Exception:\n+            pass\n+\n     def _status_bar_context_style(self, percent_used: Optional[int]) -> str:\n         if percent_used is None:\n             return \"class:status-bar-dim\"","@@ -5998,6 +6062,12 @@ def process_command(self, command: str) -> bool:\n             self.show_toolsets()\n         elif canonical == \"config\":\n             self.show_config()\n+        elif canonical == \"redraw\":\n+            # Manual recovery for terminal buffer drift from multiplexer\n+            # tab switches, subshell ``clear``, SSH window restores, etc.\n+            # See issue #8688 (cmux). Ctrl+L is bound to the same helper.\n+            self._force_full_redraw()\n+            _cprint(f\"  {_DIM}✓ UI redrawn{_RST}\")\n         elif canonical == \"clear\":\n             self.new_session(silent=True)\n             # Clear terminal screen.  Inside the TUI, Rich's console.clear()","@@ -9563,6 +9633,17 @@ def history_down(event):\n             \"\"\"Down arrow: browse history when on last line, else move cursor down.\"\"\"\n             event.app.current_buffer.auto_down(count=event.arg)\n \n+        @kb.add('c-l')\n+        def handle_ctrl_l(event):\n+            \"\"\"Ctrl+L: force a clean full-screen repaint.\n+\n+            Recovers the UI after external terminal buffer drift — tmux /\n+            cmux tab switches, ``clear`` from a subshell, SSH window\n+            restores, etc. — that prompt_toolkit can't detect on its own.\n+            Matches the universal bash/zsh/fish/vim/htop convention.\n+            \"\"\"\n+            self._force_full_redraw()\n+\n         @kb.add('c-c')\n         def handle_ctrl_c(event):\n             \"\"\"Handle Ctrl+C - cancel interactive prompts, interrupt agent, or exit.","@@ -9795,6 +9876,7 @@ def handle_paste(event):\n             # so the 5-line collapse threshold and display are consistent.\n             pasted_text = pasted_text.replace('\\r\\n', '\\n').replace('\\r', '\\n')\n             pasted_text = _strip_leaked_bracketed_paste_wrappers(pasted_text)\n+            pasted_text = _strip_leaked_terminal_responses(pasted_text)\n             if _should_auto_attach_clipboard_image_on_paste(pasted_text) and self._try_attach_clipboard_image():\n                 event.app.invalidate()\n             if pasted_text:","@@ -9937,6 +10019,7 @@ def _on_text_changed(buf):\n                event so it never triggers this.\n             \"\"\"\n             text = _strip_leaked_bracketed_paste_wrappers(buf.text)\n+            text = _strip_leaked_terminal_responses(text)\n             if text != buf.text:\n                 cursor = min(buf.cursor_position, len(text))\n                 _paste_just_collapsed[0] = True","@@ -10601,36 +10684,30 @@ def _get_voice_status():\n         # only cursor_up()s by the stored layout height, missing the extra\n         # rows created by reflow — leaving ghost duplicates visible.\n         #\n-        # Fix: before the standard erase, inflate _cursor_pos.y so the\n-        # cursor moves up far enough to cover the reflowed ghost content.\n+        # It's not just column-shrink: widening, row-shrinking, and\n+        # multiplexer-driven SIGWINCH-less redraws (cmux / tmux tab switch)\n+        # all produce the same class of drift, where the renderer's tracked\n+        # _cursor_pos.y no longer matches terminal reality. The only reliable\n+        # recovery is a full screen-clear (\\x1b[2J\\x1b[H) before the next\n+        # redraw, so we force one on every resize rather than trying to\n+        # compute the exact drift.\n         _original_on_resize = app._on_resize\n \n         def _resize_clear_ghosts():\n-            from prompt_toolkit.data_structures import Point as _Pt\n             renderer = app.renderer\n             try:\n-                old_size = renderer._last_size\n-                new_size = renderer.output.get_size()\n-                if (\n-                    old_size\n-                    and new_size.columns < old_size.columns\n-                    and new_size.columns > 0\n-                ):\n-                    reflow_factor = (\n-                        (old_size.columns + new_size.columns - 1)\n-                        // new_size.columns\n-                    )\n-                    last_h = (\n-                        renderer._last_screen.height\n-                        if renderer._last_screen\n-                        else 0\n-                    )\n-                    extra = last_h * (reflow_factor - 1)\n-                    if extra > 0:\n-                        renderer._cursor_pos = _Pt(\n-                            x=renderer._cursor_pos.x,\n-                            y=renderer._cursor_pos.y + extra,\n-                        )\n+                out = renderer.output\n+                # Reset attributes, erase the entire screen, and home the\n+                # cursor. This overwrites any reflowed status-bar rows or\n+                # stale content the terminal kept from the prior layout.\n+                out.reset_attributes()\n+                out.erase_screen()\n+                out.cursor_goto(0, 0)\n+                out.flush()\n+                # Tell the renderer its tracked position is fresh so its\n+                # own erase() inside _on_resize doesn't cursor_up() past\n+                # the top of the screen.\n+                renderer.reset(leave_alternate_screen=False)\n             except Exception:\n                 pass  # never break resize handling\n             _original_on_resize()","@@ -10638,7 +10715,6 @@ def _resize_clear_ghosts():\n         app._on_resize = _resize_clear_ghosts\n \n         def spinner_loop():\n-            last_idle_refresh = 0.0\n             while not self._should_exit:\n                 if not self._app:\n                     time.sleep(0.1)","@@ -10647,10 +10723,11 @@ def spinner_loop():\n                     self._invalidate(min_interval=0.1)\n                     time.sleep(0.1)\n                 else:\n-                    now = time.monotonic()\n-                    if now - last_idle_refresh >= 1.0:\n-                        last_idle_refresh = now\n-                        self._invalidate(min_interval=1.0)\n+                    # Do not repaint the idle prompt every second. In non-full-screen\n+                    # prompt_toolkit mode, background redraws can fight tmux/Ghostty/cmux\n+                    # viewport restoration after focus changes and visually move the\n+                    # command input area. Keep idle stable; input/agent events still\n+                    # invalidate explicitly when the UI actually changes.\n                     time.sleep(0.2)\n \n         spinner_thread = threading.Thread(target=spinner_loop, daemon=True)","@@ -10695,6 +10772,7 @@ def process_loop():\n \n                     if isinstance(user_input, str):\n                         user_input = _strip_leaked_bracketed_paste_wrappers(user_input)\n+                        user_input = _strip_leaked_terminal_responses(user_input)\n                     \n                     # Check for commands — but detect dragged/pasted file paths first.\n                     # See _detect_file_drop() for details."]},{"file":"hermes_cli/commands.py","hunks":["@@ -62,6 +62,8 @@ class CommandDef:\n                aliases=(\"reset\",)),\n     CommandDef(\"clear\", \"Clear screen and start a new session\", \"Session\",\n                cli_only=True),\n+    CommandDef(\"redraw\", \"Force a full UI repaint (recovers from terminal drift)\", \"Session\",\n+               cli_only=True),\n     CommandDef(\"history\", \"Show conversation history\", \"Session\",\n                cli_only=True),\n     CommandDef(\"save\", \"Save the current conversation\", \"Session\","]},{"file":"scripts/release.py","hunks":["@@ -61,6 +61,7 @@\n     \"[redacted:email]\": \"BadTechBandit\",\n     \"[redacted:email]\": \"Yoimex\",\n     \"[redacted:email]\": \"romanornr\",\n+    \"[redacted:email]\": \"foxion37\",\n     # contributors (from noreply pattern)\n     \"[redacted:email]\": \"davidvv\",\n     \"[redacted:email]\": \"xiaoqiang243\","]},{"file":"tests/cli/test_cli_force_redraw.py","hunks":["@@ -0,0 +1,73 @@\n+\"\"\"Tests for CLI redraw helpers used to recover from terminal buffer drift.\n+\n+Covers:\n+  - _force_full_redraw (#8688 cmux tab switch, /redraw, Ctrl+L)\n+  - the resize handler we install over prompt_toolkit's _on_resize (#5474)\n+\n+Both behaviors are exercised against fake prompt_toolkit renderer/output\n+objects — we're asserting the escape sequences the CLI sends, not that\n+the terminal physically repainted.\n+\"\"\"\n+\n+from unittest.mock import MagicMock\n+\n+import pytest\n+\n+from cli import HermesCLI\n+\n+\n[redacted:email]\n+def bare_cli():\n+    \"\"\"A HermesCLI with no __init__ — we only exercise the redraw helper.\"\"\"\n+    cli = object.__new__(HermesCLI)\n+    return cli\n+\n+\n+class TestForceFullRedraw:\n+    def test_no_app_is_safe(self, bare_cli):\n+        # _force_full_redraw must be a no-op when the TUI isn't running.\n+        bare_cli._app = None\n+        bare_cli._force_full_redraw()  # must not raise\n+\n+    def test_missing_app_attr_is_safe(self, bare_cli):\n+        # Simulate HermesCLI before the TUI has ever been constructed.\n+        bare_cli._force_full_redraw()  # must not raise\n+\n+    def test_sends_full_clear_and_invalidates(self, bare_cli):\n+        app = MagicMock()\n+        out = app.renderer.output\n+        bare_cli._app = app\n+\n+        bare_cli._force_full_redraw()\n+\n+        # Must erase screen, home cursor, and flush — in that order.\n+        out.reset_attributes.assert_called_once()\n+        out.erase_screen.assert_called_once()\n+        out.cursor_goto.assert_called_once_with(0, 0)\n+        out.flush.assert_called_once()\n+\n+        # Must reset prompt_toolkit's tracked screen/cursor state so the\n+        # next incremental redraw starts from a clean (0, 0) origin.\n+        app.renderer.reset.assert_called_once_with(leave_alternate_screen=False)\n+\n+        # Must schedule a repaint.\n+        app.invalidate.assert_called_once()\n+\n+    def test_swallows_renderer_exceptions(self, bare_cli):\n+        # If the renderer blows up for any reason, the helper must not\n+        # propagate — otherwise a stray Ctrl+L would crash the CLI.\n+        app = MagicMock()\n+        app.renderer.output.erase_screen.side_effect = RuntimeError(\"boom\")\n+        bare_cli._app = app\n+\n+        bare_cli._force_full_redraw()  # must not raise\n+\n+        # invalidate() is still attempted after a renderer failure.\n+        app.invalidate.assert_called_once()\n+\n+    def test_swallows_invalidate_exceptions(self, bare_cli):\n+        app = MagicMock()\n+        app.invalidate.side_effect = RuntimeError(\"boom\")\n+        bare_cli._app = app\n+\n+        bare_cli._force_full_redraw()  # must not raise"]},{"file":"tests/cli/test_cli_terminal_response_sanitizer.py","hunks":["@@ -0,0 +1,57 @@\n+\"\"\"Tests for defensive terminal control-response stripping in the CLI.\n+\n+Covers Cursor Position Report (CPR / DSR) responses that occasionally\n+leak into the input buffer after terminal resize storms or multiplexer\n+tab switches — see issue #14692.\n+\"\"\"\n+\n+from cli import _strip_leaked_terminal_responses\n+\n+\n+class TestStripLeakedTerminalResponses:\n+    def test_plain_text_unchanged(self):\n+        text = \"hello world\"\n+        assert _strip_leaked_terminal_responses(text) == text\n+\n+    def test_empty_text(self):\n+        assert _strip_leaked_terminal_responses(\"\") == \"\"\n+\n+    def test_strips_canonical_dsr_response(self):\n+        # Reports from issue #14692\n+        text = \"\\x1b[53;1R\"\n+        assert _strip_leaked_terminal_responses(text) == \"\"\n+\n+    def test_strips_dsr_response_in_middle_of_text(self):\n+        text = \"hello\\x1b[53;1Rworld\"\n+        assert _strip_leaked_terminal_responses(text) == \"helloworld\"\n+\n+    def test_strips_multiple_dsr_responses(self):\n+        text = \"a\\x1b[53;1Rb\\x1b[51;1Rc\\x1b[50;9Rd\"\n+        assert _strip_leaked_terminal_responses(text) == \"abcd\"\n+\n+    def test_strips_visible_form_dsr(self):\n+        # When an upstream filter has already stripped the ESC byte and\n+        # left the caret-escape representation in place.\n+        text = \"^[[53;1R\"\n+        assert _strip_leaked_terminal_responses(text) == \"\"\n+\n+    def test_strips_visible_form_dsr_in_middle_of_text(self):\n+        text = \"typed^[[53;1Rmore\"\n+        assert _strip_leaked_terminal_responses(text) == \"typedmore\"\n+\n+    def test_does_not_strip_user_text_with_R(self):\n+        # Don't over-match; user might genuinely type text containing [N;NR patterns.\n+        # Our regex requires the leading ESC or caret-escape, so bare\n+        # \"[53;1R\" as user text is preserved.\n+        text = \"see section [53;1R for details\"\n+        assert _strip_leaked_terminal_responses(text) == text\n+\n+    def test_does_not_strip_sgr_sequences(self):\n+        # Sanity: don't wipe legitimate terminal control sequences that\n+        # aren't DSR responses.\n+        text = \"\\x1b[31mred\\x1b[0m\"\n+        assert _strip_leaked_terminal_responses(text) == text\n+\n+    def test_preserves_multiline_content(self):\n+        text = \"line 1\\n\\x1b[53;1Rline 2\"\n+        assert _strip_leaked_terminal_responses(text) == \"line 1\\nline 2\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}