mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
feat(code): show "Took <duration>" after execute finishes (#4301)
`execute` tool calls now display how long the command took after it finishes. --- For `execute` shell tool calls, the status row now reports how long the command ran (e.g. `Took 5s`) once the "Running..." spinner stops, reusing the existing `format_duration` helper and the elapsed timer started in `set_running`. Other tools still hide their status on success since their output speaks for itself. Made by [Open SWE](https://openswe.vercel.app/agents/77f4f519-e99b-1012-d0fc-090de11baf58) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -1255,14 +1255,28 @@ class ToolCallMessage(Vertical):
|
||||
def set_success(self, result: str = "") -> None:
|
||||
"""Mark the tool call as successful.
|
||||
|
||||
For `execute` calls that actually ran (a start time was recorded via
|
||||
`set_running`), the elapsed run time is shown in place of the usual
|
||||
success marker; every other tool routes through `_show_success_status`.
|
||||
|
||||
Args:
|
||||
result: Tool output/result to display
|
||||
"""
|
||||
elapsed = time() - self._start_time if self._start_time is not None else None
|
||||
self._stop_animation()
|
||||
self._status = "success"
|
||||
# Strip redundant success trailer — the UI already conveys success
|
||||
self._output = _strip_success_exit_line(result)
|
||||
self._show_success_status()
|
||||
if self._tool_name == "execute" and elapsed is not None and self._status_widget:
|
||||
self._status_widget.remove_class("pending")
|
||||
# `execute` calls can run for a while, so keep the row and report
|
||||
# how long the command took once the spinner stops.
|
||||
self._status_widget.update(
|
||||
Content.styled(f"Took {format_duration(elapsed)}", "dim")
|
||||
)
|
||||
self._status_widget.display = True
|
||||
else:
|
||||
self._show_success_status()
|
||||
self._update_output_display()
|
||||
|
||||
def _show_success_status(self) -> None:
|
||||
|
||||
@@ -675,6 +675,81 @@ class TestToolCallMessageMarkupSafety:
|
||||
assert msg.has_expandable_args is True
|
||||
|
||||
|
||||
class TestToolCallMessageDuration:
|
||||
"""Tests for the post-run duration shown on `execute` tool calls."""
|
||||
|
||||
async def test_execute_shows_took_after_success(self) -> None:
|
||||
"""`execute` keeps its status row and reports how long it ran."""
|
||||
app = _tool_msg_app("execute", {"command": "sleep 1"})
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.msg.set_running()
|
||||
app.msg._start_time -= 5 # ty: ignore
|
||||
app.msg.set_success("done")
|
||||
await pilot.pause()
|
||||
|
||||
status = app.msg._status_widget
|
||||
assert status is not None
|
||||
assert status.display is True
|
||||
content = status._Static__content # ty: ignore
|
||||
assert isinstance(content, Content)
|
||||
assert content.plain == "Took 5s"
|
||||
|
||||
async def test_execute_shows_fractional_seconds(self) -> None:
|
||||
"""Sub-minute `execute` runs report tenths — `elapsed` is a float.
|
||||
|
||||
The running spinner truncates to whole seconds, but `set_success`
|
||||
passes the raw float to `format_duration`, so a regression that
|
||||
truncated `elapsed` to `int` would be caught here.
|
||||
"""
|
||||
app = _tool_msg_app("execute", {"command": "true"})
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.msg.set_running()
|
||||
app.msg._start_time -= 0.3 # ty: ignore
|
||||
app.msg.set_success("done")
|
||||
await pilot.pause()
|
||||
|
||||
status = app.msg._status_widget
|
||||
assert status is not None
|
||||
content = status._Static__content # ty: ignore
|
||||
assert isinstance(content, Content)
|
||||
assert content.plain == "Took 0.3s"
|
||||
|
||||
async def test_execute_without_run_falls_back_to_success_status(self) -> None:
|
||||
"""`execute` success with no recorded start time hides the row.
|
||||
|
||||
Without a prior `set_running`, `_start_time` is `None`, so the
|
||||
`elapsed is not None` guard must route to `_show_success_status`
|
||||
(which hides the row here because output is present) rather than
|
||||
computing a duration from `None` and crashing.
|
||||
"""
|
||||
app = _tool_msg_app("execute", {"command": "true"})
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.msg.set_success("done")
|
||||
await pilot.pause()
|
||||
|
||||
status = app.msg._status_widget
|
||||
assert status is not None
|
||||
assert status.display is False
|
||||
|
||||
async def test_non_execute_hides_status_on_success(self) -> None:
|
||||
"""Non-`execute` tools hide the status row and never show a duration."""
|
||||
app = _tool_msg_app("read_file", {"file_path": "a.py"})
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app.msg.set_running()
|
||||
app.msg.set_success("contents")
|
||||
await pilot.pause()
|
||||
|
||||
status = app.msg._status_widget
|
||||
assert status is not None
|
||||
assert status.display is False
|
||||
content = status._Static__content # ty: ignore
|
||||
assert "Took" not in getattr(content, "plain", str(content))
|
||||
|
||||
|
||||
class TestToolCallMessageTodos:
|
||||
"""Tests for `write_todos` output formatting."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user