tenseleyflow/loader / e8e7858

Browse files

Auto-update todo widget from DoD state after each tool call and on turn completion

Authored by espadonne
SHA
e8e7858592ed0975a55c0b055defea4678396d05
Parents
451fcb7
Tree
137e488

5 changed files

StatusFile+-
M src/loader/runtime/dod.py 21 0
M src/loader/runtime/events.py 1 0
M src/loader/runtime/finalization.py 10 0
M src/loader/runtime/tool_batches.py 8 0
M src/loader/ui/adapter.py 4 0
src/loader/runtime/dod.pymodified
@@ -385,6 +385,27 @@ def _append_unique(items: list[str], value: str) -> None:
385385
         items.append(value)
386386
 
387387
 
388
+def synthesize_todo_items(dod: DefinitionOfDone) -> list[dict[str, str]]:
389
+    """Build a todo item list from the current DoD state.
390
+
391
+    This allows the TUI to show live progress without the model needing
392
+    to call TodoWrite explicitly.
393
+    """
394
+    items: list[dict[str, str]] = []
395
+    seen: set[str] = set()
396
+    for label in dod.completed_items:
397
+        if label in seen:
398
+            continue
399
+        seen.add(label)
400
+        items.append({"content": label, "status": "completed", "active_form": label})
401
+    for label in dod.pending_items:
402
+        if label in seen:
403
+            continue
404
+        seen.add(label)
405
+        items.append({"content": label, "status": "in_progress", "active_form": label})
406
+    return items
407
+
408
+
388409
 def _count_lines(content: str) -> int:
389410
     if not content:
390411
         return 0
src/loader/runtime/events.pymodified
@@ -37,6 +37,7 @@ class AgentEvent:
3737
     confirm_details: str | None = None
3838
     is_error: bool = False
3939
     dod_status: str | None = None
40
+    todo_items: list[dict[str, str]] | None = None
4041
     pending_items_count: int | None = None
4142
     last_verification_result: str | None = None
4243
     workflow_mode: str | None = None
src/loader/runtime/finalization.pymodified
@@ -16,6 +16,7 @@ from .dod import (
1616
     build_verification_summary,
1717
     derive_verification_commands,
1818
     ensure_active_verification_attempt,
19
+    synthesize_todo_items,
1920
 )
2021
 from .events import AgentEvent, TurnSummary
2122
 from .evidence_provenance import (
@@ -186,6 +187,10 @@ class TurnFinalizer:
186187
             summary.workflow_timeline = list(self.context.session.workflow_timeline)
187188
             self.dod_store.save(dod)
188189
             await self.emit_dod_status(emit, dod)
190
+            await emit(AgentEvent(
191
+                type="todo_update",
192
+                todo_items=synthesize_todo_items(dod),
193
+            ))
189194
             return CompletionGateResult(
190195
                 should_continue=False,
191196
                 reason_code="non_mutating_response_accepted",
@@ -280,6 +285,11 @@ class TurnFinalizer:
280285
             summary.definition_of_done = dod
281286
             self.dod_store.save(dod)
282287
             await self.emit_dod_status(emit, dod)
288
+            # Auto-complete all todo items when DoD is done
289
+            await emit(AgentEvent(
290
+                type="todo_update",
291
+                todo_items=synthesize_todo_items(dod),
292
+            ))
283293
             verified_response = candidate_response
284294
             verification_summary = build_verification_summary(dod.evidence)
285295
             if verification_summary not in verified_response:
src/loader/runtime/tool_batches.pymodified
@@ -15,6 +15,7 @@ from .dod import (
1515
     ensure_active_verification_attempt,
1616
     is_state_mutating_tool_call,
1717
     record_successful_tool_call,
18
+    synthesize_todo_items,
1819
 )
1920
 from .events import AgentEvent, TurnSummary
2021
 from .evidence_provenance import EvidenceProvenance, EvidenceProvenanceStatus
@@ -152,6 +153,13 @@ class ToolBatchRunner:
152153
                     emit=emit,
153154
                     summary=summary,
154155
                 )
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
+                    ))
155163
                 if loop_response is not None:
156164
                     result.halted = True
157165
                     result.final_response = loop_response
src/loader/ui/adapter.pymodified
@@ -417,6 +417,10 @@ class EventAdapter:
417417
             case "response":
418418
                 self.app.post_message(ResponseComplete(content=event.content))
419419
 
420
+            case "todo_update":
421
+                if event.todo_items:
422
+                    self.app.post_message(TodoListUpdated(todos=event.todo_items))
423
+
420424
             case "confirmation":
421425
                 # Confirmation is handled via async callback, but we can post a message
422426
                 # for UI updates if needed