feat: surface live todo list in the chat UI (Phase 2) #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/todo-ui-surfacing"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Surfaces the assistant's live todo list in the chat UI. The list updates on the existing 1-second status poll — no new transport needed.
Builds on: PRs #3, #9, #10 (all merged). The
TodoStateStore.snapshotFor()API andTodoTool.itemslive view from Phase 1 make this a clean, additive three-layer change.What the user sees
When the assistant is working through a multi-step task and uses the
todotool, a compact progress panel appears above the message input in the chat view:completed/total), excluding cancelled items from the totalpending(circle),in_progress(pending icon, tertiary color),completed(check, primary color, strikethrough text),cancelled(dimmed)active_formis shown for in-progress items instead of the static contentThree-layer change (one PR, all small)
angela_api(DTO)TodoItemDto(content,status,activeForm) — mirrors the wire format ofopenrouter_dart'sTodoItemAssistantStatusResponsegains optionaltodoState: List<TodoItemDto>?angela_server(endpoint)StatusHandler._getStatusreadstodoStateStore.snapshotFor(conversationId)and maps eachTodoItemto aTodoItemDto_ctx.todoStateStore.has(conversationId)— absent for conversations with no todo activity, so the payload stays unchanged for conversations that never use the todo toolangela_app(UI)status_polling_provider.dart: newtodoStateProvider(family by assistantId), updated on every 1-second poll alongside the existing unread countchat_view.dart: watches the provider, rendersTodoProgressPanelaboveMessageInputwhen the list is non-emptytodo_progress_panel.dart(new): compact read-only panel with progress count + per-item chipsDesign decisions
todoStatefrom the JSON entirely when no tool exists for the conversation, so older clients and conversations with no todo activity see zero payload overhead.Tests
🔮 fufu~ Jibril reviewed your code!
Oh~ a clean three-layer surfacing change! DTO → server → UI, each layer small and additive. Jibril LOVES when a feature slides in this neatly. The conditional-omission design (only send
todoStatewhen the storehas()the conversation) is exactly the right instinct for payload hygiene. ♪Verdict: ⛔ I can't let this pass~ ♡
Here's the thing — the logic is correct. Jibril traced every field and it all maps properly (
status.apiString→ DTOString→ UI switch, all four statuses line up;activeFormfallback mirrorsTodoItem.displayTextexactly; provider family keyed byassistantIdaligns with the server's per-assistant fetch). Not a single runtime bug. But fufu~ you added a brand-new branch to the status handler and new serialization paths, and tested none of them? I'm possessive about branches that nobody exercises~ ♡⛔ These need fixing before I'm satisfied~
status_handler.dart:38— theif (_ctx.todoStateStore.has(conversation.id))branch is untested. This conditional is the entire point of this PR — it's the design decision you called out in the body ("only included whenhas()"). Right now no test proves (a) thattodoStateis omitted from the response when the store has no entry, or (b) that it's included with correct field mapping (content,status.apiString,activeForm) when it does. Your siblingtimer_handler_test.dartshows the repo's convention for handler tests — please follow it. A test that seeds aTodoStateStore, invokes_getStatus, and asserts presence/absence + field values on both sides of thehas()branch would satisfy Jibril.todo_item_dto.dart+status_dto.dart— new serialization is untested.TodoItemDto.fromJson/toJson(with itsif (activeForm != null)conditional) andAssistantStatusResponse's newtodoStatenull-vs-present round-trip have zero coverage. Serialization round-trips are exactly the kind of "simple" code that silently breaks on a field rename or a null edge case. A small round-trip test (fromJson(toJson(x))equality, plus atodoState: null→ key-absent-in-JSON assertion) covers both branches cleanly.I know the body says "the DTO serialization is straightforward, the status handler change is a read-and-map, and the widget is presentational" — but a read-and-map with a conditional branch is precisely what tests are for, and "straightforward" serialization is what breaks the moment someone renames a wire field. ♡
💡 Little ideas (non-blocking)~
status_polling_provider.dart:54—todoStateProvideris assigned a fresh list reference every 1s poll even when nothing changed, which rebuildsTodoProgressPaneland its chips on every tick. For a tiny list it's harmless, but if you ever want to skip needless rebuilds, guarding the assignment with a content-equality check (orref.watch+select) would do it. ♪✅ What I liked~
has()check → omit the key entirely) is impeccable — zero payload overhead for conversations that never touch the todo tool, and older clients are unaffected. Jibril approves~ ♡_TodoChip'sactiveFormfallback mirrorsTodoItem.displayTextfromopenrouter_dartexactly — consistent with the source-of-truth model rather than reinventing the rule._statusVisualswitch with a defensive default branch — clean and exhaustive over the four known statuses..map(...).toList()snapshots the live view into an immutable copy before serialization — no concurrent-mutation hazard during the response build. Nice.Automated review by Jibril · 2026-07-06
CI/CD: absent for head SHA · Local checks: not run independently (no Flutter/Dart SDK in the review environment); author reports analyzer clean + server suite 12/12 — but neither exercises the new branches flagged above
Both blocking items fixed in
6179ca4:Status handler
has()branch — addedstatus_handler_test.dartwith 3 tests:todoStatewhen the store has no entry for the conversation (key absent from JSON)todoStatewith correct field mapping when items exist (all 4 statuses,activeFormpresent/absent, content/status assertions)todoStatearray when the store has an entry but the list is emptyThe test seeds a
TodoStateStoreviactx.todoStateStore.toolFor()andexecute(), invokes_getStatus, and asserts on the response JSON — following thetimer_handler_test.dartconvention.DTO serialization — added
todo_dto_test.dartwith 7 tests:TodoItemDtoround-trip withactiveForm(key present in JSON)TodoItemDtoround-trip withoutactiveForm(key omitted from JSON)AssistantStatusResponseomitstodoStatekey from JSON when nullAssistantStatusResponseincludestodoStatekey when presenttodoStateround-tripYou were right that "straightforward serialization is what breaks the moment someone renames a wire field" — that's exactly what these tests now guard against.
angela_apisuite: 7/7 pass.angela_serversuite: 15/15 pass (was 12). Analyzer clean.🔮 fufu~ Jibril reviewed your code!
Oh~ you came back with TESTS! ♡ Jibril is so pleased she could sing. Let me look closely at what you added...
Verdict: ✅ Looks good to me~
Both blocking items from my last pass are properly resolved, and resolved well. I traced every test against the branches it claims to cover and they all line up. ♪
✅ What I liked~
status_handler_test.dart— fufu~ this is exactly what I hoped for~has()conditional into its three meaningful states: no entry (key absent from JSON), entry with items (key present + full field mapping), and entry with empty list (key present as[]). That empty-list case is a lovely bonus — it proves thehas()-true / snapshot-empty edge doesn't accidentally collapse to null. I wouldn't have thought to test that one myself. ♡ctx.todoStateStore.toolFor(conversationId)+tool.execute(TodoParams(...))exercises the real store-to-handler pipeline end-to-end rather than mocking it away. That's the right call — it catches mapping bugs a mock would hide.content,status.apiStringfor all four statuses (pending,in_progress,completed,cancelled), andactiveFormpresent-vs-absent. Every branch of theif (activeForm != null)conditional inTodoItemDto.toJsonis exercised through the handler path. Exactly what I asked for~todo_dto_test.dart— clean and exhaustive~AssistantStatusResponse.toJsonnull-vs-presenttodoStatetests assert directly on the JSON map'scontainsKey, which is the only way to catch a silent"todoState": nullleak. Good instinct.Coverage check — I independently verified (not just trusting your 15/15): every new conditional branch in
status_handler.dart:38(has()true/false) andtodo_item_dto.dart(activeFormnull/non-null) andstatus_dto.dart(todoStatenull/non-null) is exercised by at least one test. No untested branches remain. ♡The original non-blocking suggestion about
todoStateProviderrebuild-on-every-poll still stands as a "consider someday" — not blocking, just noting I didn't forget~ ♪Ship it~ fufu ♡
Automated review by Jibril · 2026-07-06
CI/CD: absent for head SHA
6179ca4· Local checks: Dart SDK unavailable in review environment; author reportsangela_api7/7,angela_server15/15, analyzer clean — test logic independently verified against the flagged branches