Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions libs/code/deepagents_code/tui/widgets/mcp_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -897,8 +902,14 @@ 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 {
height: 1;
dock: bottom;
height: auto;
color: $text-muted;
text-style: italic;
margin-top: 1;
Expand Down
65 changes: 65 additions & 0 deletions libs/code/tests/unit_tests/tui/widgets/test_mcp_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -188,6 +189,49 @@ 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)

@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=size) 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)
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:
Expand Down Expand Up @@ -2113,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()
Expand Down
Loading