mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(cli): keep Thinking spinner during edit_file approval (#3310)
The CLI hides the Thinking spinner as soon as a tool call is rendered, which leaves a visual gap for `edit_file` — its argument streaming, HITL interrupt, and user approval can take multiple seconds with no feedback. Keep the spinner anchored beneath the tool call row for calls in the new `_TOOL_CALLS_KEEP_THINKING_SPINNER` set (currently just `edit_file`), and only clear it once the tool finishes.
This commit is contained in:
@@ -77,6 +77,9 @@ _hitl_adapter_cache: TypeAdapter | None = None
|
||||
|
||||
_ASK_USER_UNSUPPORTED_ERROR = "ask_user not supported by this UI"
|
||||
|
||||
_TOOL_CALLS_KEEP_THINKING_SPINNER = frozenset({"edit_file"})
|
||||
"""Tool calls whose argument/approval phase can be long enough to need feedback."""
|
||||
|
||||
|
||||
def _get_hitl_request_adapter(hitl_request_type: type) -> TypeAdapter:
|
||||
"""Return a cached `TypeAdapter(HITLRequest)`.
|
||||
@@ -929,8 +932,16 @@ async def execute_task_textual(
|
||||
buffer_name, parsed_args, buffer_id
|
||||
)
|
||||
|
||||
# Hide spinner before showing tool call
|
||||
if adapter._set_spinner:
|
||||
keep_thinking_spinner = (
|
||||
buffer_name in _TOOL_CALLS_KEEP_THINKING_SPINNER
|
||||
)
|
||||
|
||||
# Hide spinner before showing most tool calls.
|
||||
# `edit_file` can spend noticeable time between
|
||||
# argument streaming, HITL interrupt delivery, and
|
||||
# approval handling, so re-anchor Thinking below
|
||||
# the row instead of leaving the UI visually idle.
|
||||
if adapter._set_spinner and not keep_thinking_spinner:
|
||||
await adapter._set_spinner(None)
|
||||
|
||||
# Mount tool call message
|
||||
@@ -942,6 +953,8 @@ async def execute_task_textual(
|
||||
tool_msg = ToolCallMessage(buffer_name, parsed_args)
|
||||
await adapter._mount_message(tool_msg)
|
||||
adapter._current_tool_messages[buffer_id] = tool_msg
|
||||
if adapter._set_spinner and keep_thinking_spinner:
|
||||
await adapter._set_spinner("Thinking")
|
||||
|
||||
tool_call_buffers.pop(buffer_key, None)
|
||||
|
||||
|
||||
@@ -789,6 +789,59 @@ class TestExecuteTaskTextualParallelToolSpinner:
|
||||
|
||||
assert statuses[-1] == "Thinking"
|
||||
|
||||
async def test_edit_file_tool_keeps_thinking_spinner_while_pending(self) -> None:
|
||||
"""`edit_file` should not leave a visual gap before approval/execution."""
|
||||
statuses: list[str | None] = []
|
||||
|
||||
async def record_spinner(status: str | None) -> None:
|
||||
await asyncio.sleep(0)
|
||||
statuses.append(status)
|
||||
|
||||
chunks = [
|
||||
(
|
||||
(),
|
||||
"messages",
|
||||
(
|
||||
_tool_call_message(
|
||||
"edit_file",
|
||||
{
|
||||
"file_path": "example.py",
|
||||
"old_string": "old",
|
||||
"new_string": "new",
|
||||
},
|
||||
"tool-1",
|
||||
),
|
||||
{},
|
||||
),
|
||||
),
|
||||
(
|
||||
(),
|
||||
"messages",
|
||||
(
|
||||
ToolMessage(content="edited", tool_call_id="tool-1"),
|
||||
{},
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
adapter = TextualUIAdapter(
|
||||
mount_message=_mock_mount,
|
||||
update_status=_noop_status,
|
||||
request_approval=_mock_approval,
|
||||
set_spinner=record_spinner,
|
||||
)
|
||||
|
||||
await execute_task_textual(
|
||||
user_input="edit the file",
|
||||
agent=_FakeAgent(chunks),
|
||||
assistant_id="assistant",
|
||||
session_state=SimpleNamespace(thread_id="thread-1", auto_approve=True),
|
||||
adapter=adapter,
|
||||
)
|
||||
|
||||
assert statuses[:2] == ["Thinking", "Thinking"]
|
||||
assert None not in statuses
|
||||
|
||||
async def test_spinner_with_three_parallel_tools_out_of_order(self) -> None:
|
||||
"""Three parallel tools completed out of order; Thinking after all."""
|
||||
statuses: list[str | None] = []
|
||||
|
||||
Reference in New Issue
Block a user