fix(cli): truncate multi-line shell commands in HITL approval (#3314)

Closes #1095

---

The existing 120-char truncation in the approval prompt only collapsed
horizontally long commands. Multi-line commands where each line was
short (e.g. a `python3 -c` one-liner with embedded newlines) bypassed
the per-line threshold and rendered untruncated — pushing the
approve/reject options far down the terminal.

Adds a line-count threshold (`_SHELL_COMMAND_TRUNCATE_LINES = 5`)
alongside the existing character threshold and applies both inside the
same `press 'e' to expand` affordance, so the user experience is
unchanged for short commands and consistent (one ellipsis, one expand
hint) for any command that exceeds either dimension.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Mason Daugherty <61371264+mdrxy@users.noreply.github.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
open-swe[bot]
2026-05-11 18:29:18 +00:00
committed by GitHub
parent fc80075c65
commit 7ab6e14565
2 changed files with 87 additions and 6 deletions
+47 -6
View File
@@ -36,10 +36,52 @@ from deepagents_cli.widgets.tool_renderers import get_renderer
# Max length for truncated shell command display
_SHELL_COMMAND_TRUNCATE_LENGTH: int = 120
# Max number of lines for truncated shell command display
_SHELL_COMMAND_TRUNCATE_LINES: int = 5
_WARNING_PREVIEW_LIMIT: int = 3
_WARNING_TEXT_TRUNCATE_LENGTH: int = 220
def _is_command_too_long(command: str) -> bool:
"""Whether a shell command exceeds the display thresholds (char or line).
Args:
command: The shell command string to check.
Returns:
`True` if the command is longer than `_SHELL_COMMAND_TRUNCATE_LENGTH`
characters or has more than `_SHELL_COMMAND_TRUNCATE_LINES` lines.
"""
if len(command) > _SHELL_COMMAND_TRUNCATE_LENGTH:
return True
return command.count("\n") + 1 > _SHELL_COMMAND_TRUNCATE_LINES
def _truncate_command(command: str) -> str:
"""Truncate a shell command for compact display.
Applies line truncation first (keeping at most `_SHELL_COMMAND_TRUNCATE_LINES`
lines), then character truncation, so multi-line commands collapse before
long single lines are cut. A single ellipsis is appended at the end.
Args:
command: The shell command string to truncate.
Returns:
The truncated command string, with a trailing ellipsis if any truncation
was applied; otherwise the original command unchanged.
"""
ellipsis = get_glyphs().ellipsis
lines = command.split("\n")
truncated = len(lines) > _SHELL_COMMAND_TRUNCATE_LINES
if truncated:
command = "\n".join(lines[:_SHELL_COMMAND_TRUNCATE_LINES])
if len(command) > _SHELL_COMMAND_TRUNCATE_LENGTH:
command = command[:_SHELL_COMMAND_TRUNCATE_LENGTH]
truncated = True
return command + ellipsis if truncated else command
class ApprovalMenu(Container):
"""Approval menu using standard Textual patterns.
@@ -143,7 +185,7 @@ class ApprovalMenu(Container):
if req.get("name", "") not in SHELL_TOOL_NAMES:
return False
command = str(req.get("args", {}).get("command", ""))
return len(command) > _SHELL_COMMAND_TRUNCATE_LENGTH
return _is_command_too_long(command)
def _get_command_display(self, *, expanded: bool) -> Content:
"""Get the command display content (truncated or full).
@@ -165,14 +207,13 @@ class ApprovalMenu(Container):
command = strip_dangerous_unicode(command_raw)
issues = detect_dangerous_unicode(command_raw)
if expanded or len(command) <= _SHELL_COMMAND_TRUNCATE_LENGTH:
too_long = _is_command_too_long(command)
if expanded or not too_long:
command_display = command
else:
command_display = (
command[:_SHELL_COMMAND_TRUNCATE_LENGTH] + get_glyphs().ellipsis
)
command_display = _truncate_command(command)
if not expanded and len(command) > _SHELL_COMMAND_TRUNCATE_LENGTH:
if not expanded and too_long:
display = Content.from_markup(
"[bold]$cmd[/bold] [dim](press 'e' to expand)[/dim]",
cmd=command_display,
@@ -7,6 +7,7 @@ import pytest
from deepagents_cli.config import get_glyphs
from deepagents_cli.widgets.approval import (
_SHELL_COMMAND_TRUNCATE_LENGTH,
_SHELL_COMMAND_TRUNCATE_LINES,
ApprovalMenu,
)
@@ -59,6 +60,22 @@ class TestCheckExpandableCommand:
menu = ApprovalMenu({"name": "shell", "args": {}})
assert menu._has_expandable_command is False
def test_multiline_command_over_line_threshold_is_expandable(self) -> None:
"""Multi-line commands that exceed the line threshold are expandable.
Each line stays well under the character threshold, so this regresses
only if the line-count check is missing.
"""
command = "\n".join(["echo line"] * (_SHELL_COMMAND_TRUNCATE_LINES + 1))
menu = ApprovalMenu({"name": "shell", "args": {"command": command}})
assert menu._has_expandable_command is True
def test_multiline_command_at_line_threshold_not_expandable(self) -> None:
"""Commands at exactly the line threshold are not expandable."""
command = "\n".join(["echo line"] * _SHELL_COMMAND_TRUNCATE_LINES)
menu = ApprovalMenu({"name": "shell", "args": {"command": command}})
assert menu._has_expandable_command is False
class TestGetCommandDisplay:
"""Tests for `ApprovalMenu._get_command_display`."""
@@ -137,6 +154,29 @@ class TestGetCommandDisplay:
assert "U+202E" in display.plain
assert "raw:" in display.plain
def test_multiline_command_truncated_to_max_lines(self) -> None:
"""Multi-line commands collapse to the line cap with an expand hint."""
lines = [f"echo {i}" for i in range(_SHELL_COMMAND_TRUNCATE_LINES + 3)]
command = "\n".join(lines)
menu = ApprovalMenu({"name": "shell", "args": {"command": command}})
display = menu._get_command_display(expanded=False)
plain = display.plain
assert get_glyphs().ellipsis in plain
assert "press 'e' to expand" in plain
for kept in lines[:_SHELL_COMMAND_TRUNCATE_LINES]:
assert kept in plain
assert lines[_SHELL_COMMAND_TRUNCATE_LINES] not in plain
def test_multiline_command_shows_full_when_expanded(self) -> None:
"""Expanded multi-line commands show every line."""
lines = [f"echo {i}" for i in range(_SHELL_COMMAND_TRUNCATE_LINES + 3)]
command = "\n".join(lines)
menu = ApprovalMenu({"name": "shell", "args": {"command": command}})
display = menu._get_command_display(expanded=True)
for line in lines:
assert line in display.plain
assert "press 'e' to expand" not in display.plain
class TestToggleExpand:
"""Tests for `ApprovalMenu.action_toggle_expand`."""