fix(display): fix subagent progress tree-view visual nits

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.
This commit is contained in:
teknium1
2026-02-28 23:29:49 -08:00
parent 4ec386cc72
commit 41d8a80226
2 changed files with 34 additions and 16 deletions

View File

@@ -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
# =========================================================================

View File

@@ -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: