Skip to content
Closed
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
30 changes: 29 additions & 1 deletion cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11548,7 +11548,7 @@ def _on_tool_progress(self, event_type: str, function_name: str = None, preview:
can show a live elapsed timer (the TUI poll loop already invalidates
every ~0.15s, so the counter updates automatically).

When tool_progress_mode is "all" or "new", also prints a persistent
When tool_progress_mode is "all", "new", or "verbose", also prints a persistent
stacked line to scrollback on tool.completed so users can see the
full history of tool calls (not just the current one in the spinner).
"""
Expand Down Expand Up @@ -11662,6 +11662,34 @@ def _on_tool_progress(self, event_type: str, function_name: str = None, preview:
pass
self._invalidate()
return

if event_type == "subagent.tool" and function_name and self.tool_progress_mode in ("all", "new", "verbose"):
if self.tool_progress_mode == "new" and function_name == self._last_scrollback_tool:
self._invalidate()
return
self._last_scrollback_tool = function_name
try:
prefix = ""
task_count = kwargs.get("task_count", 1)
if task_count > 1:
idx = kwargs.get("task_index", 0)
prefix = f"[{idx + 1}] "
from agent.display import get_tool_emoji
emoji = get_tool_emoji(function_name)
short = preview or ""
from agent.display import get_tool_preview_max_len
_pl = get_tool_preview_max_len()
if _pl > 0 and short and len(short) > _pl:
short = short[:_pl - 3] + "..."
line = f" {prefix}β”œβ”€ {emoji} {function_name}"
if short:
line += f' "{short}"'
_cprint(f" {line}")
except Exception:
pass
self._invalidate()
return

if event_type != "tool.started":
return
if function_name and not function_name.startswith("_"):
Expand Down
91 changes: 90 additions & 1 deletion tests/cli/test_tool_progress_scrollback.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ def test_pending_info_consumed_on_completed(self):
assert len(cli._pending_tool_info.get("terminal", [])) == 1
assert cli._pending_tool_info["terminal"][0] == {"command": "pwd"}


class TestMoAReferenceBlocks:
"""moa.reference renders a labelled thinking-style block; moa.aggregating
updates the spinner. Both are display-only and must commit regardless of
Expand Down Expand Up @@ -293,3 +292,93 @@ def test_aggregating_event_updates_spinner_only(self):
assert "aggregating" in cli._spinner_text
# aggregating is a spinner-only transition; no committed scrollback line.
mock_print.assert_not_called()

class TestSubagentEventScrollback:
"""Scrollback lines for delegate_task subagent events."""

def test_subagent_tool_prints_scrollback_line(self):
Comment thread
rabi marked this conversation as resolved.
"""subagent.tool prints an indented tree line with tool name."""
cli = _make_cli(tool_progress="all")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress(
"subagent.tool", "read_file", "src/main.py", None,
task_index=0, task_count=1, goal="read the file",
)
mock_print.assert_called_once()
line = mock_print.call_args[0][0]
assert "read_file" in line

def test_subagent_tool_prints_batch_prefix_for_multi_task(self):
"""Batch delegate_task children include a 1-indexed [N] prefix."""
cli = _make_cli(tool_progress="all")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress(
"subagent.tool", "read_file", "src/main.py", None,
task_index=1, task_count=3, goal="read the file",
)
line = mock_print.call_args[0][0]
assert "[2]" in line

def test_subagent_tool_hidden_in_off_mode(self):
"""In 'off' mode, subagent.tool is not printed."""
cli = _make_cli(tool_progress="off")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress(
"subagent.tool", "terminal", "ls", None,
task_index=0, task_count=1, goal="list",
)
mock_print.assert_not_called()

def test_subagent_tool_prints_in_verbose_mode(self):
"""Verbose mode still commits child tool lines to scrollback."""
cli = _make_cli(tool_progress="verbose")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress(
"subagent.tool", "read_file", "src/main.py", None,
task_index=0, task_count=1, goal="read the file",
)
mock_print.assert_called_once()

def test_subagent_tool_new_mode_skips_consecutive_repeats(self):
"""New mode suppresses consecutive duplicate child tool names."""
cli = _make_cli(tool_progress="new")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress(
"subagent.tool", "read_file", "src/main.py", None,
task_index=0, task_count=1, goal="read the file",
)
cli._on_tool_progress(
"subagent.tool", "read_file", "src/other.py", None,
task_index=0, task_count=1, goal="read the other file",
)
assert mock_print.call_count == 1

def test_subagent_tool_new_mode_prints_again_after_tool_changes(self):
"""A different child tool breaks the new-mode repeat streak."""
cli = _make_cli(tool_progress="new")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress(
"subagent.tool", "read_file", "src/main.py", None,
task_index=0, task_count=1, goal="read the file",
)
cli._on_tool_progress(
"subagent.tool", "terminal", "ls", None,
task_index=0, task_count=1, goal="list files",
)
cli._on_tool_progress(
"subagent.tool", "read_file", "src/other.py", None,
task_index=0, task_count=1, goal="read the other file",
)
assert mock_print.call_count == 3

def test_other_subagent_events_ignored(self):
"""subagent.start/.complete/progress produce no output and don't affect spinner."""
cli = _make_cli(tool_progress="all")
with patch.object(_cli_mod, "_cprint") as mock_print:
for evt in ("subagent.start", "subagent.complete", "subagent.progress", "subagent_progress"):
cli._on_tool_progress(
evt, None, "some text", None,
task_index=0, task_count=1, goal="task", status="done",
)
mock_print.assert_not_called()
assert "some text" not in getattr(cli, "_spinner_text", "")