Auto-update todo widget from DoD state after each tool call and on turn completion
- SHA
e8e7858592ed0975a55c0b055defea4678396d05- Parents
-
451fcb7 - Tree
137e488
e8e7858
e8e7858592ed0975a55c0b055defea4678396d05451fcb7
137e488| Status | File | + | - |
|---|---|---|---|
| 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: | ||
| 385 | 385 | items.append(value) |
| 386 | 386 | |
| 387 | 387 | |
| 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 | + | |
| 388 | 409 | def _count_lines(content: str) -> int: |
| 389 | 410 | if not content: |
| 390 | 411 | return 0 |
src/loader/runtime/events.pymodified@@ -37,6 +37,7 @@ class AgentEvent: | ||
| 37 | 37 | confirm_details: str | None = None |
| 38 | 38 | is_error: bool = False |
| 39 | 39 | dod_status: str | None = None |
| 40 | + todo_items: list[dict[str, str]] | None = None | |
| 40 | 41 | pending_items_count: int | None = None |
| 41 | 42 | last_verification_result: str | None = None |
| 42 | 43 | workflow_mode: str | None = None |
src/loader/runtime/finalization.pymodified@@ -16,6 +16,7 @@ from .dod import ( | ||
| 16 | 16 | build_verification_summary, |
| 17 | 17 | derive_verification_commands, |
| 18 | 18 | ensure_active_verification_attempt, |
| 19 | + synthesize_todo_items, | |
| 19 | 20 | ) |
| 20 | 21 | from .events import AgentEvent, TurnSummary |
| 21 | 22 | from .evidence_provenance import ( |
@@ -186,6 +187,10 @@ class TurnFinalizer: | ||
| 186 | 187 | summary.workflow_timeline = list(self.context.session.workflow_timeline) |
| 187 | 188 | self.dod_store.save(dod) |
| 188 | 189 | await self.emit_dod_status(emit, dod) |
| 190 | + await emit(AgentEvent( | |
| 191 | + type="todo_update", | |
| 192 | + todo_items=synthesize_todo_items(dod), | |
| 193 | + )) | |
| 189 | 194 | return CompletionGateResult( |
| 190 | 195 | should_continue=False, |
| 191 | 196 | reason_code="non_mutating_response_accepted", |
@@ -280,6 +285,11 @@ class TurnFinalizer: | ||
| 280 | 285 | summary.definition_of_done = dod |
| 281 | 286 | self.dod_store.save(dod) |
| 282 | 287 | 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 | + )) | |
| 283 | 293 | verified_response = candidate_response |
| 284 | 294 | verification_summary = build_verification_summary(dod.evidence) |
| 285 | 295 | if verification_summary not in verified_response: |
src/loader/runtime/tool_batches.pymodified@@ -15,6 +15,7 @@ from .dod import ( | ||
| 15 | 15 | ensure_active_verification_attempt, |
| 16 | 16 | is_state_mutating_tool_call, |
| 17 | 17 | record_successful_tool_call, |
| 18 | + synthesize_todo_items, | |
| 18 | 19 | ) |
| 19 | 20 | from .events import AgentEvent, TurnSummary |
| 20 | 21 | from .evidence_provenance import EvidenceProvenance, EvidenceProvenanceStatus |
@@ -152,6 +153,13 @@ class ToolBatchRunner: | ||
| 152 | 153 | emit=emit, |
| 153 | 154 | summary=summary, |
| 154 | 155 | ) |
| 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 | + )) | |
| 155 | 163 | if loop_response is not None: |
| 156 | 164 | result.halted = True |
| 157 | 165 | result.final_response = loop_response |
src/loader/ui/adapter.pymodified@@ -417,6 +417,10 @@ class EventAdapter: | ||
| 417 | 417 | case "response": |
| 418 | 418 | self.app.post_message(ResponseComplete(content=event.content)) |
| 419 | 419 | |
| 420 | + case "todo_update": | |
| 421 | + if event.todo_items: | |
| 422 | + self.app.post_message(TodoListUpdated(todos=event.todo_items)) | |
| 423 | + | |
| 420 | 424 | case "confirmation": |
| 421 | 425 | # Confirmation is handled via async callback, but we can post a message |
| 422 | 426 | # for UI updates if needed |