From 340883443499485ddd226cf3268f0b06a0882232 Mon Sep 17 00:00:00 2001 From: Mason Daugherty <61371264+mdrxy@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:06:31 +0000 Subject: [PATCH 1/2] fix(code): let MCP footer wrap on narrow windows Co-authored-by: open-swe[bot] --- .../deepagents_code/tui/widgets/mcp_viewer.py | 2 +- .../unit_tests/tui/widgets/test_mcp_viewer.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/libs/code/deepagents_code/tui/widgets/mcp_viewer.py b/libs/code/deepagents_code/tui/widgets/mcp_viewer.py index 0d15ad1107e..2218451720b 100644 --- a/libs/code/deepagents_code/tui/widgets/mcp_viewer.py +++ b/libs/code/deepagents_code/tui/widgets/mcp_viewer.py @@ -898,7 +898,7 @@ class MCPViewerScreen(ModalScreen[str | None]): } MCPViewerScreen .mcp-viewer-help { - height: 1; + height: auto; color: $text-muted; text-style: italic; margin-top: 1; diff --git a/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py b/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py index 995b8402ddb..d94dd8ad02f 100644 --- a/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py +++ b/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py @@ -188,6 +188,22 @@ async def test_reconnect_hint_shown_when_pending(self) -> None: help_widget = screen.query_one(".mcp-viewer-help", Static) assert "Ctrl+R reconnect" in _widget_text(help_widget) + async def test_footer_wraps_in_narrow_window(self) -> None: + """A narrow viewer grows the footer instead of clipping its hints.""" + app = MCPViewerTestApp() + async with app.run_test(size=(50, 20)) as pilot: + screen = MCPViewerScreen( + server_info=_sample_info(), + pending_reconnect=True, + ) + app.push_screen(screen) + await pilot.pause() + + help_widget = screen.query_one(".mcp-viewer-help", Static) + assert help_widget.size.height > 1 + assert "Ctrl+R reconnect" in _widget_text(help_widget) + assert "Esc close" in _widget_text(help_widget) + async def test_ctrl_r_dismisses_with_reconnect_sentinel_when_pending( self, ) -> None: From 325d870af4994d14eb2161e9de03a0c15c7d774e Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Wed, 19 Aug 2026 15:54:24 -0400 Subject: [PATCH 2/2] fix(code): dock MCP modal footers so hints stay on screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `height: auto` alone let the viewer footer wrap, but `.mcp-list` has `min-height: 5` and won the fight for vertical space, so the wrapped footer was laid out past the modal's bottom edge and never painted. On an 80-column terminal every size below 24 rows lost the footer entirely — including `Esc close`, the way out of the modal. `dock: bottom` reserves the footer's rows before the list takes the remainder, which keeps every hint on screen at all sizes measured (80x14 through 120x24). Apply the same fix to the error modal, where wrapping pushed its footer out at narrow widths. Tests now assert the footer's region sits inside the modal rather than reading `Static.__content`, which returns the full string whether or not any of it reaches the screen. --- .../deepagents_code/tui/widgets/mcp_viewer.py | 13 ++++- .../unit_tests/tui/widgets/test_mcp_viewer.py | 57 +++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/libs/code/deepagents_code/tui/widgets/mcp_viewer.py b/libs/code/deepagents_code/tui/widgets/mcp_viewer.py index 2218451720b..f6eed005d2b 100644 --- a/libs/code/deepagents_code/tui/widgets/mcp_viewer.py +++ b/libs/code/deepagents_code/tui/widgets/mcp_viewer.py @@ -551,8 +551,13 @@ class MCPServerErrorScreen(ModalScreen[None]): color: $text; } + /* Same treatment as `.mcp-viewer-help`: `height: auto` so the hints + wrap instead of truncating on a narrow window, `dock: bottom` so the + rows they wrap onto are reserved before `.mcp-error-body` claims the + rest — otherwise wrapping pushes the footer past the modal's edge. */ MCPServerErrorScreen .mcp-error-help { - height: 1; + dock: bottom; + height: auto; color: $text-muted; text-style: italic; margin-top: 1; @@ -897,7 +902,13 @@ class MCPViewerScreen(ModalScreen[str | None]): margin-top: 2; } + /* `height: auto` lets the hints wrap instead of truncating on a narrow + window. `dock: bottom` then reserves those rows before `.mcp-list` takes + the remainder — without it the list's `min-height` wins the fight for + space and shoves the whole footer past the modal's bottom edge, hiding + `Esc close` entirely on terminals under ~24 rows. */ MCPViewerScreen .mcp-viewer-help { + dock: bottom; height: auto; color: $text-muted; text-style: italic; diff --git a/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py b/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py index d94dd8ad02f..4530481c0c6 100644 --- a/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py +++ b/libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py @@ -4,6 +4,7 @@ import pytest from textual.app import App, ComposeResult +from textual.containers import Vertical from textual.notifications import Notification from textual.widget import Widget from textual.widgets import Static @@ -188,10 +189,19 @@ async def test_reconnect_hint_shown_when_pending(self) -> None: help_widget = screen.query_one(".mcp-viewer-help", Static) assert "Ctrl+R reconnect" in _widget_text(help_widget) - async def test_footer_wraps_in_narrow_window(self) -> None: - """A narrow viewer grows the footer instead of clipping its hints.""" + @pytest.mark.parametrize("size", [(80, 24), (80, 14), (50, 20)]) + async def test_footer_hints_stay_on_screen(self, size: tuple[int, int]) -> None: + """Every hint renders, however narrow or short the window is. + + Asserts the footer's region sits inside the modal, not just that + its text is set: the bug being guarded here is that the string is + complete but laid out past the modal's bottom edge, where the + compositor never paints it. `_widget_text` cannot observe that. + `(80, 14)` and `(50, 20)` are both sizes where the footer used to + be pushed clean out of the modal. + """ app = MCPViewerTestApp() - async with app.run_test(size=(50, 20)) as pilot: + async with app.run_test(size=size) as pilot: screen = MCPViewerScreen( server_info=_sample_info(), pending_reconnect=True, @@ -200,10 +210,28 @@ async def test_footer_wraps_in_narrow_window(self) -> None: await pilot.pause() help_widget = screen.query_one(".mcp-viewer-help", Static) - assert help_widget.size.height > 1 + modal = screen.query_one(Vertical) + assert help_widget in app.screen._compositor.visible_widgets + assert modal.content_region.contains_region(help_widget.region) assert "Ctrl+R reconnect" in _widget_text(help_widget) assert "Esc close" in _widget_text(help_widget) + async def test_footer_wraps_rather_than_truncating(self) -> None: + """A footer too long for one line grows instead of losing hints.""" + app = MCPViewerTestApp() + async with app.run_test(size=(80, 24)) as pilot: + screen = MCPViewerScreen( + server_info=_sample_info(), + pending_reconnect=True, + ) + app.push_screen(screen) + await pilot.pause() + + help_widget = screen.query_one(".mcp-viewer-help", Static) + # The modal caps at `width: 80`, so the full hint string never + # fits on one line — it must occupy two. + assert help_widget.size.height == 2 + async def test_ctrl_r_dismisses_with_reconnect_sentinel_when_pending( self, ) -> None: @@ -2129,6 +2157,27 @@ async def test_error_modal_falls_back_when_error_missing(self) -> None: body = app.screen.query_one(".mcp-error-text", Static) assert "No error details were reported." in _widget_text(body) + async def test_error_modal_footer_wraps_in_narrow_window(self) -> None: + """The error modal's footer wraps rather than truncating.""" + server = MCPServerInfo( + name="broken", + transport="stdio", + status="error", + error="Server exited with code 1.", + ) + app = MCPViewerTestApp() + # 30 columns leaves 25 for the hints, two short of the 27 they + # need — the narrowest realistic window that forces a wrap. + async with app.run_test(size=(30, 12)) as pilot: + app.push_screen(MCPServerErrorScreen(server)) + await pilot.pause() + + help_widget = app.screen.query_one(".mcp-error-help", Static) + modal = app.screen.query_one(Vertical) + assert help_widget.size.height == 2 + assert modal.content_region.contains_region(help_widget.region) + assert "Esc close" in _widget_text(help_widget) + async def test_click_expands_tool(self) -> None: """Clicking a tool selects it and toggles expand.""" app = MCPViewerTestApp()