tenseleyflow/loader / 984bfec

Browse files

Show planned tool calls as pending todo items before execution, cross off as each completes

Authored by espadonne
SHA
984bfec31628c96292441ad245cfe0005aeb4351
Parents
c2f3631
Tree
4ec6a75

2 changed files

StatusFile+-
M src/loader/runtime/tool_batches.py 60 7
M tests/test_tool_batches.py 2 1
src/loader/runtime/tool_batches.pymodified
@@ -4,6 +4,7 @@ from __future__ import annotations
44
 
55
 from collections.abc import Awaitable, Callable
66
 from dataclasses import dataclass, field
7
+from pathlib import Path
78
 
89
 from ..llm.base import ToolCall
910
 from .context import RuntimeContext
@@ -84,6 +85,26 @@ class ToolBatchRunner:
8485
 
8586
         result = ToolBatchResult(consecutive_errors=consecutive_errors)
8687
 
88
+        # Pre-populate planned items for the entire batch so the todo
89
+        # widget shows what's coming, not just what's done.
90
+        planned_labels = _batch_planned_labels(tool_calls)
91
+        completed_labels: list[str] = []
92
+
93
+        async def _emit_batch_todos() -> None:
94
+            """Emit a todo update combining DoD state with batch progress."""
95
+            items = synthesize_todo_items(dod)
96
+            for label in planned_labels:
97
+                if label in completed_labels:
98
+                    continue
99
+                # Don't duplicate items already in DoD
100
+                if any(item["content"] == label for item in items):
101
+                    continue
102
+                items.append({"content": label, "status": "in_progress", "active_form": label})
103
+            if items:
104
+                await emit(AgentEvent(type="todo_update", todo_items=items))
105
+
106
+        await _emit_batch_todos()
107
+
87108
         for tool_call in tool_calls:
88109
             cfg = self.context.config.reasoning
89110
 
@@ -153,13 +174,11 @@ class ToolBatchRunner:
153174
                     emit=emit,
154175
                     summary=summary,
155176
                 )
156
-                # Emit live todo progress from DoD state after each success
157
-                todo_items = synthesize_todo_items(dod)
158
-                if todo_items:
159
-                    await emit(AgentEvent(
160
-                        type="todo_update",
161
-                        todo_items=todo_items,
162
-                    ))
177
+                # Mark this tool's label as completed and emit live progress
178
+                label = _tool_call_label(tool_call)
179
+                if label:
180
+                    completed_labels.append(label)
181
+                await _emit_batch_todos()
163182
                 if loop_response is not None:
164183
                     result.halted = True
165184
                     result.final_response = loop_response
@@ -414,3 +433,37 @@ def _stale_verification_detail(tool_call: ToolCall) -> str:
414433
         if command:
415434
             return f"bash ran `{command}`"
416435
     return f"{tool_call.name} changed the workspace"
436
+
437
+
438
+def _tool_call_label(tool_call: ToolCall) -> str:
439
+    """Human-readable label for one tool call."""
440
+    name = tool_call.name
441
+    if name in ("write", "edit", "patch"):
442
+        path = str(tool_call.arguments.get("file_path", "")).strip()
443
+        if path:
444
+            short = Path(path).name
445
+            verb = "Write" if name == "write" else "Edit"
446
+            return f"{verb} {short}"
447
+    if name == "bash":
448
+        cmd = str(tool_call.arguments.get("command", "")).strip()
449
+        if cmd:
450
+            return f"Run {cmd[:40]}"
451
+    if name == "read":
452
+        path = str(tool_call.arguments.get("file_path", "")).strip()
453
+        if path:
454
+            return f"Read {Path(path).name}"
455
+    if name == "glob":
456
+        pattern = str(tool_call.arguments.get("pattern", "")).strip()
457
+        if pattern:
458
+            return f"Search {pattern[:30]}"
459
+    return ""
460
+
461
+
462
+def _batch_planned_labels(tool_calls: list[ToolCall]) -> list[str]:
463
+    """Build labels for all tool calls in a batch (for upfront planning display)."""
464
+    labels = []
465
+    for tc in tool_calls:
466
+        label = _tool_call_label(tc)
467
+        if label and label not in labels:
468
+            labels.append(label)
469
+    return labels
tests/test_tool_batches.pymodified
@@ -227,7 +227,8 @@ async def test_tool_batch_runner_uses_context_for_confidence_gate(temp_dir: Path
227227
     assert "Please inspect the project." in captured["context"]
228228
     assert context.session.messages[-1].role == Role.USER
229229
     assert "[LOW CONFIDENCE WARNING]" in context.session.messages[-1].content
230
-    assert [event.type for event in events] == ["confidence"]
230
+    event_types = [event.type for event in events]
231
+    assert "confidence" in event_types
231232
 
232233
 
233234
 @pytest.mark.asyncio