mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): make execute command expandable in code TUI transcript (#4428)
The code TUI now lets you click a long `execute` command in the transcript to expand and view it in full. --- In the `deepagents-code` TUI, long `execute` shell commands were truncated at 120 chars in the transcript header with no way to see the full text — clicking a row toggled the *output*, not the command. This adds a collapsible full-command block that mirrors the existing `js_eval` code block: clicking the command header (or its hint) expands the full command, while clicking the output still toggles output. `on_click` is now region-aware so both the command and an expandable output can each be toggled independently. Made by [Open SWE](https://openswe.vercel.app/agents/de77dad1-3f8b-b84c-4d8e-553560c9db74) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -12125,12 +12125,17 @@ class DeepAgentsApp(App):
|
||||
child.toggle_body()
|
||||
return
|
||||
if isinstance(child, ToolCallMessage) and not child.has_class("-grouped"):
|
||||
if child.has_output and child.has_expandable_output:
|
||||
child.toggle_output()
|
||||
return
|
||||
# Prefer the collapsible command/code block when the row has one,
|
||||
# so Ctrl+O matches the "click or Ctrl+O to show command/code"
|
||||
# hint rendered beside it. The output stays reachable by clicking
|
||||
# its own region (see `ToolCallMessage.on_click`); rows without an
|
||||
# expandable command/code block fall through to the output.
|
||||
if child.has_expandable_args:
|
||||
child.toggle_args()
|
||||
return
|
||||
if child.has_output and child.has_expandable_output:
|
||||
child.toggle_output()
|
||||
return
|
||||
if child.has_output:
|
||||
child.toggle_output()
|
||||
return
|
||||
|
||||
@@ -27,6 +27,13 @@ the "offer a collapsible code block" threshold stay in lock-step from a single
|
||||
source of truth.
|
||||
"""
|
||||
|
||||
EXECUTE_HEADER_MAX_LENGTH = 120
|
||||
"""Width at which the `execute` header truncates the shell command.
|
||||
|
||||
Shared with `messages.py` so the header cutoff and the "offer a collapsible
|
||||
command block" threshold stay in lock-step from a single source of truth.
|
||||
"""
|
||||
|
||||
|
||||
def _format_timeout(seconds: int) -> str:
|
||||
"""Format timeout in human-readable units (e.g., 300 -> '5m', 3600 -> '1h').
|
||||
@@ -217,7 +224,9 @@ def format_tool_display(tool_name: str, tool_args: dict) -> str:
|
||||
elif tool_name == "execute":
|
||||
# Execute: show the command, and timeout only if non-default
|
||||
if "command" in tool_args:
|
||||
command = _sanitize_display_value(tool_args["command"], max_length=120)
|
||||
command = _sanitize_display_value(
|
||||
tool_args["command"], max_length=EXECUTE_HEADER_MAX_LENGTH
|
||||
)
|
||||
timeout = _coerce_timeout_seconds(tool_args.get("timeout"))
|
||||
from deepagents.backends import DEFAULT_EXECUTE_TIMEOUT
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from deepagents_code.config import (
|
||||
from deepagents_code.formatting import format_duration
|
||||
from deepagents_code.input import EMAIL_PREFIX_PATTERN, INPUT_HIGHLIGHT_PATTERN
|
||||
from deepagents_code.tool_display import (
|
||||
EXECUTE_HEADER_MAX_LENGTH,
|
||||
JS_EVAL_HEADER_MAX_LENGTH,
|
||||
format_tool_display,
|
||||
)
|
||||
@@ -1108,6 +1109,7 @@ class ToolCallMessage(Vertical):
|
||||
self._reject_reason: str | None = None
|
||||
# Widget references (set in on_mount)
|
||||
self._status_widget: Static | None = None
|
||||
self._header_widget: Static | None = None
|
||||
self._args_widget: Static | None = None
|
||||
self._args_hint_widget: Static | None = None
|
||||
self._preview_widget: Static | None = None
|
||||
@@ -1136,7 +1138,7 @@ class ToolCallMessage(Vertical):
|
||||
Widgets for header, arguments, status, and output display.
|
||||
"""
|
||||
tool_label = format_tool_display(self._tool_name, self._args)
|
||||
yield Static(tool_label, markup=False, classes="tool-header")
|
||||
yield Static(tool_label, markup=False, classes="tool-header", id="tool-header")
|
||||
# Task: dedicated description line (dim, truncated)
|
||||
if self._tool_name == "task":
|
||||
desc = self._args.get("description", "")
|
||||
@@ -1193,6 +1195,7 @@ class ToolCallMessage(Vertical):
|
||||
self.add_class("-ascii")
|
||||
|
||||
self._status_widget = self.query_one("#status", Static)
|
||||
self._header_widget = self.query_one("#tool-header", Static)
|
||||
self._args_widget = self.query_one("#args-full", Static)
|
||||
self._args_hint_widget = self.query_one("#args-hint", Static)
|
||||
self._preview_widget = self.query_one("#output-preview", Static)
|
||||
@@ -1508,18 +1511,64 @@ class ToolCallMessage(Vertical):
|
||||
def on_click(self, event: Click) -> None:
|
||||
"""Toggle output/argument expansion.
|
||||
|
||||
Prefer toggling output, but only when the output can actually
|
||||
expand/collapse. Otherwise fall through to the collapsible args/code
|
||||
block — `js_eval` commonly has a short, unexpandable result sitting
|
||||
A click on the header/args region (the truncated command or code line
|
||||
and its hint) toggles the collapsible args/code block directly, so an
|
||||
`execute` command or `js_eval` program can be expanded even when the
|
||||
output below it is *also* expandable. Otherwise prefer toggling output,
|
||||
falling through to the args/code block only when the output can't
|
||||
expand — `js_eval` commonly has a short, unexpandable result sitting
|
||||
below a multi-line, collapsible code block, and the old
|
||||
"output wins whenever it exists" rule left that code block stuck.
|
||||
"""
|
||||
event.stop() # Prevent click from bubbling up and scrolling
|
||||
if self._output and self.has_expandable_output:
|
||||
if self.has_expandable_args and self._click_targets_args_region(event.widget):
|
||||
self.toggle_args()
|
||||
elif self._output and self.has_expandable_output:
|
||||
self.toggle_output()
|
||||
elif self.has_expandable_args:
|
||||
self.toggle_args()
|
||||
|
||||
def _click_targets_args_region(self, widget: object) -> bool:
|
||||
"""Whether a click landed on the header/args block (not the output).
|
||||
|
||||
Walks up from the clicked widget to `self`, matching the cached
|
||||
header, collapsed-args, and args-hint widgets. The walk is bounded so a
|
||||
mock or detached node (which never reaches `self` via `.parent`) returns
|
||||
`False` instead of looping, preserving the generic "prefer output"
|
||||
routing for those cases.
|
||||
|
||||
Returns:
|
||||
`True` if the click landed on the header/args region.
|
||||
"""
|
||||
targets = tuple(
|
||||
target
|
||||
for target in (
|
||||
self._header_widget,
|
||||
self._args_widget,
|
||||
self._args_hint_widget,
|
||||
)
|
||||
if target is not None
|
||||
)
|
||||
if not targets:
|
||||
# A click can only arrive post-mount, where these refs are always
|
||||
# cached, so an empty tuple means a regression nulled them out. Log
|
||||
# it rather than silently routing every click to output (mirrors
|
||||
# `_update_args_display`).
|
||||
logger.debug("_click_targets_args_region: header/args refs not cached")
|
||||
return False
|
||||
# The header/args/hint widgets are direct children of `self` (a click on
|
||||
# rendered text reports a descendant, so the real match depth is 0-1).
|
||||
# 8 is generous headroom that also bounds the walk for a detached or mock
|
||||
# node, whose `.parent` chain never reaches `self`.
|
||||
node = widget
|
||||
for _ in range(8):
|
||||
if node is None or node is self:
|
||||
return False
|
||||
if any(node is target for target in targets):
|
||||
return True
|
||||
node = getattr(node, "parent", None)
|
||||
return False
|
||||
|
||||
def _format_output(
|
||||
self, output: str, *, is_preview: bool = False
|
||||
) -> FormattedOutput:
|
||||
@@ -2497,7 +2546,9 @@ class ToolCallMessage(Vertical):
|
||||
# "click to collapse" there is misleading.
|
||||
if self._has_expandable_output():
|
||||
self._hint_widget.update(
|
||||
Content.styled("click or Ctrl+O to collapse", "dim italic")
|
||||
Content.styled(
|
||||
f"{self._output_hint_keys()} to collapse", "dim italic"
|
||||
)
|
||||
)
|
||||
self._hint_widget.display = True
|
||||
else:
|
||||
@@ -2520,7 +2571,9 @@ class ToolCallMessage(Vertical):
|
||||
self._preview_row.display = False
|
||||
ellipsis = get_glyphs().ellipsis
|
||||
self._hint_widget.update(
|
||||
Content.styled(f"{ellipsis} click or Ctrl+O to expand", "dim")
|
||||
Content.styled(
|
||||
f"{ellipsis} {self._output_hint_keys()} to expand", "dim"
|
||||
)
|
||||
)
|
||||
self._hint_widget.display = True
|
||||
return
|
||||
@@ -2544,7 +2597,8 @@ class ToolCallMessage(Vertical):
|
||||
ellipsis = get_glyphs().ellipsis
|
||||
self._hint_widget.update(
|
||||
Content.styled(
|
||||
f"{ellipsis} {result.truncation} — click or Ctrl+O to expand",
|
||||
f"{ellipsis} {result.truncation} — "
|
||||
f"{self._output_hint_keys()} to expand",
|
||||
"dim",
|
||||
)
|
||||
)
|
||||
@@ -2552,6 +2606,21 @@ class ToolCallMessage(Vertical):
|
||||
else:
|
||||
self._hint_widget.display = False
|
||||
|
||||
def _output_hint_keys(self) -> str:
|
||||
"""Affordances to advertise in the output expand/collapse hint.
|
||||
|
||||
Ctrl+O routes to the collapsible command/code block whenever this row
|
||||
has one (see `action_toggle_tool_output`), so the output hint only
|
||||
advertises Ctrl+O when Ctrl+O would actually toggle the *output*. When a
|
||||
command/code block is present the output is reachable by clicking its
|
||||
own region instead.
|
||||
|
||||
Returns:
|
||||
`"click"` when an expandable command/code block owns Ctrl+O,
|
||||
otherwise `"click or Ctrl+O"`.
|
||||
"""
|
||||
return "click" if self.has_expandable_args else "click or Ctrl+O"
|
||||
|
||||
@property
|
||||
def has_output(self) -> bool:
|
||||
"""Check if this tool message has output to display.
|
||||
@@ -2596,6 +2665,10 @@ class ToolCallMessage(Vertical):
|
||||
`JS_EVAL_HEADER_MAX_LENGTH`), so the full program is offered as a
|
||||
collapsible block whenever it spans more than one non-blank line *or*
|
||||
a single line is long enough to be truncated in the header.
|
||||
- `execute`: the header truncates the shell command at
|
||||
`EXECUTE_HEADER_MAX_LENGTH`, so the full command is offered as a
|
||||
collapsible block when the command, after stripping surrounding
|
||||
whitespace, is longer than `EXECUTE_HEADER_MAX_LENGTH`.
|
||||
"""
|
||||
if self._tool_name == "ask_user":
|
||||
return bool(self._args)
|
||||
@@ -2604,6 +2677,10 @@ class ToolCallMessage(Vertical):
|
||||
if isinstance(code, str) and code.strip():
|
||||
non_blank = sum(1 for line in code.splitlines() if line.strip())
|
||||
return non_blank > 1 or len(code.strip()) > JS_EVAL_HEADER_MAX_LENGTH
|
||||
if self._tool_name == "execute":
|
||||
command = self._args.get("command")
|
||||
if isinstance(command, str) and command.strip():
|
||||
return len(command.strip()) > EXECUTE_HEADER_MAX_LENGTH
|
||||
return False
|
||||
|
||||
def _format_code_detail(self) -> Content:
|
||||
@@ -2626,6 +2703,22 @@ class ToolCallMessage(Vertical):
|
||||
# line above and the "show/hide code" hint below.
|
||||
return Content("\n").join((Content(""), Content(code_str), Content("")))
|
||||
|
||||
def _format_command_detail(self) -> Content:
|
||||
"""Render the full `execute` command for the collapsible block.
|
||||
|
||||
The command is shown verbatim and left-aligned, as plain uncolored
|
||||
`Content`, mirroring `_format_code_detail`. Hidden/deceptive Unicode is
|
||||
rendered as visible markers so a truncated header can't conceal it.
|
||||
|
||||
Returns:
|
||||
A plain `Content` renderable with a blank line of padding on
|
||||
top and bottom.
|
||||
"""
|
||||
command = self._args.get("command")
|
||||
command_str = command.strip("\n") if isinstance(command, str) else str(command)
|
||||
command_str = render_with_unicode_markers(command_str)
|
||||
return Content("\n").join((Content(""), Content(command_str), Content("")))
|
||||
|
||||
def _format_args_detail(self) -> Content:
|
||||
"""Render tool arguments as an indented `Content` block.
|
||||
|
||||
@@ -2662,13 +2755,14 @@ class ToolCallMessage(Vertical):
|
||||
self._args_hint_widget.display = False
|
||||
return
|
||||
|
||||
is_code = self._tool_name == "js_eval"
|
||||
noun = "code" if is_code else "arguments"
|
||||
if self._tool_name == "js_eval":
|
||||
noun, detail_fn = "code", self._format_code_detail
|
||||
elif self._tool_name == "execute":
|
||||
noun, detail_fn = "command", self._format_command_detail
|
||||
else:
|
||||
noun, detail_fn = "arguments", self._format_args_detail
|
||||
if self._args_expanded:
|
||||
detail = (
|
||||
self._format_code_detail() if is_code else self._format_args_detail()
|
||||
)
|
||||
self._args_widget.update(detail)
|
||||
self._args_widget.update(detail_fn())
|
||||
self._args_widget.display = True
|
||||
self._args_hint_widget.update(
|
||||
Content.styled(f"click or Ctrl+O to hide {noun}", "dim italic")
|
||||
|
||||
@@ -3641,6 +3641,30 @@ class TestAskUserLifecycle:
|
||||
tool.toggle_args.assert_called_once_with()
|
||||
tool.toggle_output.assert_not_called()
|
||||
|
||||
def test_ctrl_o_prefers_command_when_both_expandable(self) -> None:
|
||||
"""Ctrl+O toggles the command/code block even when output can also expand.
|
||||
|
||||
Matches the region-aware click routing and the "click or Ctrl+O to show
|
||||
command/code" hint: the output stays reachable by clicking its own row.
|
||||
"""
|
||||
from deepagents_code.widgets.messages import ToolCallMessage
|
||||
|
||||
app = DeepAgentsApp(agent=MagicMock())
|
||||
app._pending_ask_user_widget = None
|
||||
tool = MagicMock(spec=ToolCallMessage)
|
||||
tool.has_class.return_value = False
|
||||
tool.has_output = True
|
||||
tool.has_expandable_output = True # long stdout, expandable
|
||||
tool.has_expandable_args = True # long command, expandable
|
||||
container = MagicMock()
|
||||
container.children = [tool]
|
||||
|
||||
with patch.object(app, "query_one", return_value=container):
|
||||
app.action_toggle_tool_output()
|
||||
|
||||
tool.toggle_args.assert_called_once_with()
|
||||
tool.toggle_output.assert_not_called()
|
||||
|
||||
def test_ctrl_o_prefers_more_recent_tool_in_dom_order(self) -> None:
|
||||
"""The newest tool row in DOM order wins over an older one."""
|
||||
from deepagents_code.widgets.messages import ToolCallMessage
|
||||
|
||||
@@ -14,7 +14,10 @@ from textual.widgets import Markdown
|
||||
from deepagents_code import theme
|
||||
from deepagents_code.formatting import format_duration
|
||||
from deepagents_code.input import INPUT_HIGHLIGHT_PATTERN
|
||||
from deepagents_code.tool_display import JS_EVAL_HEADER_MAX_LENGTH
|
||||
from deepagents_code.tool_display import (
|
||||
EXECUTE_HEADER_MAX_LENGTH,
|
||||
JS_EVAL_HEADER_MAX_LENGTH,
|
||||
)
|
||||
from deepagents_code.widgets.messages import (
|
||||
AppMessage,
|
||||
AssistantMessage,
|
||||
@@ -1912,6 +1915,215 @@ class TestToolCallMessageExpandableArgs:
|
||||
assert msg._args_expanded is False
|
||||
|
||||
|
||||
class TestToolCallMessageExecuteCommandExpand:
|
||||
"""Tests for the collapsible full-command block on `execute` tool calls."""
|
||||
|
||||
def test_long_command_is_expandable(self) -> None:
|
||||
"""A command too long for the header offers a collapsible block."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
assert msg.has_expandable_args is True
|
||||
|
||||
def test_short_command_not_expandable(self) -> None:
|
||||
"""A command that fits in the header has nothing to expand."""
|
||||
short_cmd = "x" * (EXECUTE_HEADER_MAX_LENGTH - 1)
|
||||
msg = ToolCallMessage("execute", {"command": short_cmd})
|
||||
assert msg.has_expandable_args is False
|
||||
|
||||
def test_missing_command_not_expandable(self) -> None:
|
||||
"""An execute call without a command string is not expandable."""
|
||||
assert ToolCallMessage("execute", {}).has_expandable_args is False
|
||||
|
||||
def test_command_detail_is_plain_and_left_aligned(self) -> None:
|
||||
"""The command is plain `Content`, left-aligned, with blank padding."""
|
||||
command = "cd /tmp && \\\n make build\nmake test"
|
||||
msg = ToolCallMessage("execute", {"command": command})
|
||||
detail = msg._format_command_detail()
|
||||
|
||||
assert isinstance(detail, Content)
|
||||
assert not detail.spans
|
||||
assert detail.plain.split("\n") == [
|
||||
"",
|
||||
"cd /tmp && \\",
|
||||
" make build",
|
||||
"make test",
|
||||
"",
|
||||
]
|
||||
|
||||
def test_command_detail_marks_hidden_unicode(self) -> None:
|
||||
"""Hidden controls in the expanded command render as visible markers."""
|
||||
msg = ToolCallMessage("execute", {"command": "echo safe\n#\u202e hidden"})
|
||||
detail = msg._format_command_detail()
|
||||
|
||||
assert "\u202e" not in detail.plain
|
||||
assert "<U+202E RIGHT-TO-LEFT OVERRIDE>" in detail.plain
|
||||
|
||||
async def test_click_on_header_toggles_command(self) -> None:
|
||||
"""Clicking the command header expands the collapsible command block."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
|
||||
class _Harness(App[None]):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield self.msg
|
||||
|
||||
app = _Harness()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg = app.msg
|
||||
# Long stdout makes the output expandable too, so a generic click
|
||||
# would prefer output; a header click must still reach the command.
|
||||
msg.set_success("\n".join(str(i) for i in range(50)))
|
||||
await pilot.pause()
|
||||
assert msg.has_expandable_output is True
|
||||
|
||||
event = MagicMock()
|
||||
event.widget = msg._header_widget
|
||||
msg.on_click(event)
|
||||
await pilot.pause()
|
||||
event.stop.assert_called_once()
|
||||
assert msg._args_expanded is True
|
||||
assert msg._expanded is False
|
||||
|
||||
async def test_generic_click_still_prefers_output(self) -> None:
|
||||
"""A click outside the header region toggles output, not the command."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
|
||||
class _Harness(App[None]):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield self.msg
|
||||
|
||||
app = _Harness()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg = app.msg
|
||||
msg.set_success("\n".join(str(i) for i in range(50)))
|
||||
await pilot.pause()
|
||||
assert msg.has_expandable_output is True
|
||||
|
||||
event = MagicMock() # mock widget is outside the args region
|
||||
msg.on_click(event)
|
||||
await pilot.pause()
|
||||
assert msg._expanded is True
|
||||
assert msg._args_expanded is False
|
||||
|
||||
@staticmethod
|
||||
def _harness(msg: ToolCallMessage) -> App[None]:
|
||||
"""Build a minimal single-widget app hosting `msg`."""
|
||||
|
||||
class _Harness(App[None]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield msg
|
||||
|
||||
return _Harness()
|
||||
|
||||
async def test_click_targets_args_region_walks_parents(self) -> None:
|
||||
"""A click on a descendant of the header still routes to the command.
|
||||
|
||||
A real Textual click reports the rendered-text node *inside* the
|
||||
`Static`, not the `Static` itself, so the region check must walk up the
|
||||
`.parent` chain rather than compare identities directly.
|
||||
"""
|
||||
msg = ToolCallMessage("execute", {"command": "echo hi"})
|
||||
app = self._harness(msg)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# Direct hit, one hop, and two hops all resolve to the header.
|
||||
one_hop = SimpleNamespace(parent=msg._header_widget)
|
||||
two_hops = SimpleNamespace(parent=one_hop)
|
||||
assert msg._click_targets_args_region(msg._header_widget) is True
|
||||
assert msg._click_targets_args_region(one_hop) is True
|
||||
assert msg._click_targets_args_region(two_hops) is True
|
||||
# A detached node and `self` never match.
|
||||
assert msg._click_targets_args_region(SimpleNamespace(parent=None)) is False
|
||||
assert msg._click_targets_args_region(msg) is False
|
||||
|
||||
async def test_click_on_args_hint_toggles_command(self) -> None:
|
||||
"""Clicking the args-hint line expands the command, not the output."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
app = self._harness(msg)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg.set_success("\n".join(str(i) for i in range(50)))
|
||||
await pilot.pause()
|
||||
assert msg.has_expandable_output is True
|
||||
|
||||
event = MagicMock()
|
||||
event.widget = msg._args_hint_widget
|
||||
msg.on_click(event)
|
||||
await pilot.pause()
|
||||
assert msg._args_expanded is True
|
||||
assert msg._expanded is False
|
||||
|
||||
async def test_click_on_expanded_command_collapses(self) -> None:
|
||||
"""Clicking the expanded command block collapses it again."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
app = self._harness(msg)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg.toggle_args()
|
||||
await pilot.pause()
|
||||
assert msg._args_expanded is True
|
||||
|
||||
event = MagicMock()
|
||||
event.widget = msg._args_widget
|
||||
msg.on_click(event)
|
||||
await pilot.pause()
|
||||
assert msg._args_expanded is False
|
||||
|
||||
async def test_expanded_command_uses_command_noun_and_detail(self) -> None:
|
||||
"""Expanding wires the `command` noun and `_format_command_detail`."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
app = self._harness(msg)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg.toggle_args()
|
||||
await pilot.pause()
|
||||
|
||||
hint = msg._args_hint_widget._Static__content # ty: ignore
|
||||
body = msg._args_widget._Static__content # ty: ignore
|
||||
assert "hide command" in hint.plain
|
||||
assert body.plain == msg._format_command_detail().plain
|
||||
|
||||
async def test_output_hint_omits_ctrl_o_when_command_expandable(self) -> None:
|
||||
"""With an expandable command, Ctrl+O owns it, so the output hint drops it."""
|
||||
long_cmd = "echo " + "x" * EXECUTE_HEADER_MAX_LENGTH
|
||||
msg = ToolCallMessage("execute", {"command": long_cmd})
|
||||
app = self._harness(msg)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg.set_success("\n".join(str(i) for i in range(50)))
|
||||
await pilot.pause()
|
||||
assert msg.has_expandable_args is True
|
||||
|
||||
hint = msg._hint_widget._Static__content # ty: ignore
|
||||
assert "click to expand" in hint.plain
|
||||
assert "Ctrl+O" not in hint.plain
|
||||
|
||||
async def test_output_hint_keeps_ctrl_o_without_expandable_command(self) -> None:
|
||||
"""A short command leaves Ctrl+O on the output, so the hint keeps it."""
|
||||
msg = ToolCallMessage("execute", {"command": "echo hi"})
|
||||
app = self._harness(msg)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
msg.set_success("\n".join(str(i) for i in range(50)))
|
||||
await pilot.pause()
|
||||
assert msg.has_expandable_args is False
|
||||
|
||||
hint = msg._hint_widget._Static__content # ty: ignore
|
||||
assert "Ctrl+O" in hint.plain
|
||||
|
||||
|
||||
class TestToolCallMessageShellCommand:
|
||||
"""Test ToolCallMessage shows full shell command for errors.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user