From 41d8a802268d7caf2e6a9bdc3e22df7274964f7c Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sat, 28 Feb 2026 23:29:49 -0800 Subject: [PATCH] fix(display): fix subagent progress tree-view visual nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to the subagent progress display from PR #186: 1. Task index prefix: show 1-indexed prefix ([1], [2], ...) for ALL tasks in batch mode (task_count > 1). Single tasks get no prefix. Previously task 0 had no prefix while others did, making batch output confusing. 2. Completion indicator: use spinner.print_above() instead of raw print() for per-task completion lines (✓ [1/2] ...). Raw print collided with the active spinner, mushing the completion text onto the spinner line. Now prints cleanly above. Added task_count parameter to _build_child_progress_callback and _run_single_child. Updated tests accordingly. --- tests/agent/test_subagent_progress.py | 29 +++++++++++++++++---------- tools/delegate_tool.py | 21 ++++++++++++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/tests/agent/test_subagent_progress.py b/tests/agent/test_subagent_progress.py index 0aa39f4c8..b6e5e7525 100644 --- a/tests/agent/test_subagent_progress.py +++ b/tests/agent/test_subagent_progress.py @@ -170,8 +170,8 @@ class TestBuildChildProgressCallback: parent_cb.assert_not_called() - def test_task_index_prefix_in_output(self): - """Multi-task mode should show task index prefix.""" + def test_task_index_prefix_in_batch_mode(self): + """Batch mode (task_count > 1) should show 1-indexed prefix for all tasks.""" buf = io.StringIO() spinner = KawaiiSpinner("delegating") spinner._out = buf @@ -181,15 +181,22 @@ class TestBuildChildProgressCallback: parent._delegate_spinner = spinner parent.tool_progress_callback = None - # task_index > 0 should add prefix - cb = _build_child_progress_callback(2, parent) - cb("web_search", "test") - + # task_index=0 in a batch of 3 → prefix "[1]" + cb0 = _build_child_progress_callback(0, parent, task_count=3) + cb0("web_search", "test") output = buf.getvalue() - assert "[2]" in output + assert "[1]" in output - def test_task_index_zero_no_prefix(self): - """Single task (index 0) should not show index prefix.""" + # task_index=2 in a batch of 3 → prefix "[3]" + buf.truncate(0) + buf.seek(0) + cb2 = _build_child_progress_callback(2, parent, task_count=3) + cb2("web_search", "test") + output = buf.getvalue() + assert "[3]" in output + + def test_single_task_no_prefix(self): + """Single task (task_count=1) should not show index prefix.""" buf = io.StringIO() spinner = KawaiiSpinner("delegating") spinner._out = buf @@ -199,11 +206,11 @@ class TestBuildChildProgressCallback: parent._delegate_spinner = spinner parent.tool_progress_callback = None - cb = _build_child_progress_callback(0, parent) + cb = _build_child_progress_callback(0, parent, task_count=1) cb("web_search", "test") output = buf.getvalue() - assert "[0]" not in output + assert "[" not in output # ========================================================================= diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 1f5c64b5b..c960cc36c 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -77,7 +77,7 @@ def _strip_blocked_tools(toolsets: List[str]) -> List[str]: return [t for t in toolsets if t not in blocked_toolset_names] -def _build_child_progress_callback(task_index: int, parent_agent) -> Optional[callable]: +def _build_child_progress_callback(task_index: int, parent_agent, task_count: int = 1) -> Optional[callable]: """Build a callback that relays child agent tool calls to the parent display. Two display paths: @@ -93,7 +93,8 @@ def _build_child_progress_callback(task_index: int, parent_agent) -> Optional[ca if not spinner and not parent_cb: return None # No display → no callback → zero behavior change - prefix = f"[{task_index}] " if task_index > 0 else "" + # Show 1-indexed prefix only in batch mode (multiple tasks) + prefix = f"[{task_index + 1}] " if task_count > 1 else "" # Gateway: batch tool names, flush periodically _BATCH_SIZE = 5 @@ -163,6 +164,7 @@ def _run_single_child( model: Optional[str], max_iterations: int, parent_agent, + task_count: int = 1, ) -> Dict[str, Any]: """ Spawn and run a single child agent. Called from within a thread. @@ -183,7 +185,7 @@ def _run_single_child( parent_api_key = parent_agent._client_kwargs.get("api_key") # Build progress callback to relay tool calls to parent display - child_progress_cb = _build_child_progress_callback(task_index, parent_agent) + child_progress_cb = _build_child_progress_callback(task_index, parent_agent, task_count) child = AIAgent( base_url=parent_agent.base_url, @@ -344,6 +346,7 @@ def delegate_task( model=model, max_iterations=effective_max_iter, parent_agent=parent_agent, + task_count=1, ) results.append(result) else: @@ -368,6 +371,7 @@ def delegate_task( model=model, max_iterations=effective_max_iter, parent_agent=parent_agent, + task_count=n_tasks, ) futures[future] = i @@ -387,14 +391,21 @@ def delegate_task( results.append(entry) completed_count += 1 - # Print per-task completion line (visible in CLI via patch_stdout) + # Print per-task completion line above the spinner idx = entry["task_index"] label = task_labels[idx] if idx < len(task_labels) else f"Task {idx}" dur = entry.get("duration_seconds", 0) status = entry.get("status", "?") icon = "✓" if status == "completed" else "✗" remaining = n_tasks - completed_count - print(f" {icon} [{idx+1}/{n_tasks}] {label} ({dur}s)") + completion_line = f"{icon} [{idx+1}/{n_tasks}] {label} ({dur}s)" + if spinner_ref: + try: + spinner_ref.print_above(completion_line) + except Exception: + print(f" {completion_line}") + else: + print(f" {completion_line}") # Update spinner text to show remaining count if spinner_ref and remaining > 0: