feat(chat): surface the agent TODO list as a pill above the composer - #569
Conversation
kiro-cli tracks its own task list via a server-side `todo_list` tool, but nothing in the dashboard showed it -- the user could not see what the agent believed it was working on without reading the transcript. The list arrives as an ordinary ACP `tool_call_update` identified only by `_meta.kiro.toolName`, NOT as the ACP `plan` session update (kiro-cli 2.14.0 never emits `plan`, so the pre-existing UPDATE_PLAN constant stays inert). Every todo command echoes the whole list in `rawOutput`, so the snapshot is stored wholesale rather than merged from deltas. Backend parses the snapshot in the shared ACP dispatch, emits it as an additive EVENT_TODO_UPDATE (the tool call still renders in the transcript), stores it per slot, and serializes it in `_ChatSlot.to_dict` -- the one dict feeding both /api/chat/slots and the WS `slots` snapshot, so the pill rehydrates on reconnect. A `todo_update` WS event pushes mid-turn changes. Frontend adds a collapsed pill that hugs its content and expands to a full-width list on click, reading the slot's todo off the same slots array that the snapshot populates.
Design Review (Fable 5) — 🟡 CONCERNSAdvisory design-level review of Design-Verdict: CONCERNS Sound feature built on the only viable hook, but it ships 4 temp screenshot binaries into the source tree and silently degrades whenever kiro-cli's undocumented wire shape shifts. Watch
Suggestions
[DESIGN-REVIEWED] 0c25c41 |
Arbiter — ✅ no blocking findingsArbiter found no unresolved long-term items that require action before merging Second-order review for Review detailsI've read both files. The line-level reviewers (Claude Opus 5, GPT 5.6) reported no findings. Only the design reviewer raised CONCERNS, with two sub-threshold items. Neither clears the narrow one-way-door / concrete-harm bar. Arbiter-Verdict: PASS No sub-threshold finding meets the long-term-impact bar. Considered but not escalated
Suggested follow-ups (open as issues — non-blocking)
[ARBITER-REVIEWED] 0c25c41 False positive or not applicable? A repository writer can comment: For a broader accepted-risk deferral, apply |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 5 Review — ✅ no blocking findingsReviewed No findings. Verdict recorded via the action's structured output for commit False positive or not applicable? A repository writer can comment: |
push_guard.py redact_credentials now strips everything after the URL authority (scheme://host[:port]) so path-embedded tokens, query strings, and fragments are all suppressed. Diagnostics retain the scheme, host, and git error class. The authority regex covers bracketed IPv6 addresses ([::1], zone-ids like %25eth0) and begins the redacted remainder at /, ?, or # — so query-only URLs (https://host?token=x) with no path component are also redacted. An exhaustive RFC 3986 authority-shape matrix pins the coverage so no further round of redaction bypass exists. Also: errors='replace' on all subprocess.run text calls, bounded patch-id replay check (REPLAY_HISTORY_WINDOW), explicit-refspec fetches, origin/<base> ancestry validation, single_commit gating, rebase-first recovery guidance. Closes the IPv6 + query-only credential-redaction bypass (GPT 5.6 blocking lane) and the CodeQL bare-substring sanitization alert (#569).
Phase 1 step 2 sync fetch now uses an explicit +refs/heads/<base>:refs/remotes/origin/<base> refspec so narrow-refspec clones (single-branch CI checkouts) cannot rebase onto a stale base in multi-commit workflows. Prior rounds on this branch: authority-only URL redaction with RFC 3986 shape matrix, errors='replace' decoding, bounded patch-id replay check, origin/<base>-ancestor-of-HEAD check, single_commit gating, rebase-first recovery text, posture allowlist entries, brand spelling fix, IPv6 and query-only redaction with CodeQL #569 assertion removed.
…irodotdev#569) kiro-cli tracks its own task list via a server-side `todo_list` tool, but nothing in the dashboard showed it -- the user could not see what the agent believed it was working on without reading the transcript. The list arrives as an ordinary ACP `tool_call_update` identified only by `_meta.kiro.toolName`, NOT as the ACP `plan` session update (kiro-cli 2.14.0 never emits `plan`, so the pre-existing UPDATE_PLAN constant stays inert). Every todo command echoes the whole list in `rawOutput`, so the snapshot is stored wholesale rather than merged from deltas. Backend parses the snapshot in the shared ACP dispatch, emits it as an additive EVENT_TODO_UPDATE (the tool call still renders in the transcript), stores it per slot, and serializes it in `_ChatSlot.to_dict` -- the one dict feeding both /api/chat/slots and the WS `slots` snapshot, so the pill rehydrates on reconnect. A `todo_update` WS event pushes mid-turn changes. Frontend adds a collapsed pill that hugs its content and expands to a full-width list on click, reading the slot's todo off the same slots array that the snapshot populates. Co-authored-by: Kyle Seaman <kseam@dev-dsk-kseam-1b-55230d27.us-east-1.amazon.com>
Problem
kiro-cli tracks its own task list — it calls a
todo_listtool to record multi-step work and tick items off as it goes. None of that reached the dashboard. To find out what the agent thought it was working on, or how far through a plan it was, you had to scroll the transcript and read tool calls.Why it matters
The agent's plan is the single most useful piece of state during a long turn, and it was the one thing the UI didn't show. Without it there's no glanceable answer to "how far along is this?" — which is exactly the question you ask while waiting.
Fix (symptom → root cause → change)
Symptom: the TODO list exists in the agent but never renders.
Root cause — and the trap. The obvious hook is wrong.
src/kiro_crew/acp/types.pyalready definedUPDATE_PLAN = "plan", parked inKNOWN_SESSION_UPDATESunder the comment "Updates we recognise but don't yet surface (plumbing-only)". That reads like a ready-made seam for this feature. It never fires. I probed a livekiro-cli acpsession (v2.14.0) three times and saw zeroplanupdates; kiro-cli's owntui.jsreceives aplanupdate but logs it as "not yet mapped", so neither side implements it. Building againstUPDATE_PLANwould have shipped a pill that silently never populates, with nothing in the code to explain why.What actually crosses the wire is an ordinary
tool_call_updatewhose real tool name appears only in_meta.kiro.toolName == "todo_list"— the visibletitleis LLM-authored prose ("Creating task list: …"), so it cannot be used to identify the tool. The full list rides inrawOutput:{ "tasks": [ { "id": "1", "task_description": "read config", "completed": false } ], "description": "Config workflow", "context": [], "modified_files": [] }Change. Every todo command (
create/complete/list) echoes the entire list, so this is a snapshot, never a delta — there is no incremental state to reconcile.acp/_dispatch.py—parse_todo_snapshot()identifies the tool via_meta, normalises the payload, redacts task text, and caps size. Emitted as an additiveEVENT_TODO_UPDATE; the tool call itself still flows through astool_result+tool_call_updateso it keeps rendering in the transcript.dashboard/state.py— stored on_ChatSlotand serialised into_dict(). That one dict feeds both/api/chat/slots(cold load) and the WSslotssnapshot, so the pill rehydrates on reconnect from a single edit. Deliberately not added as a top-level key to_broadcast, which rebuilds theslotsenvelope key-by-key and would silently drop it (the bug that made feat(source-panel): GitLab parity for the PR panel and self-hosted GitLab hosts #466'sgitlabHostsGenerationpush inert).dashboard/chat_runner.py— pushes atodo_updateWS delta so the pill moves mid-turn, gated onset_todo()reporting an actual change so a repeated snapshot doesn't fan out to every socket.TaskProgressBar, joining the existing bar stack besideSubagentProgressBar. The live delta patches the samedashboard.slotsarray the snapshot populates, so the two paths cannot disagree about a slot's list.Two decisions worth calling out for review:
completedis a plain boolean — there is no in-progress state. So "current task" is derived as the first incomplete item. That's an inference, not something the agent reports.completed/total/currentare computed server-side, not in the browser, so the pill's "N of M" label cannot drift from the list it summarises.Absent vs empty is load-bearing.
nullmeans the agent never used its todo tool (no pill); a present list with zero tasks means it cleared the list. Different states, preserved end to end.Tests
test/test_todo_list_surface.py(25 tests) — payload shapes captured from a realkiro-cli acpsession, not invented._metanot title, in both directions (afs_readcall with a task-list-shaped payload must not match; a real todo result with no_metamust not match).tool_result/tool_call_updatestill flow, so "handling" the todo can never silently drop a tool call from the transcript.set_todochange detection — gates the broadcast; an identical re-echo must report no change.rawOutputwrapper.website/src/test/TaskProgressBar.test.tsx(17 tests) — collapsed/expanded disclosure, server-derived counts, current-task derivation, all-complete state, per-slot isolation, progressbar a11y sync, partial-state tolerance, and both hydration paths (livetodo_updatedelta and reconnectslotssnapshot).The load-bearing tests are revert-verified — I broke change detection, event-swallowing, title-based identification, default-expanded, and hide-on-complete in turn, and confirmed each failed the test that names it and passed again restored.
Manual verification
Screenshots below were captured from the real component via a temporary Vite harness with a mocked slot store. Component, CSS, theme tokens, icons, and the collapse interaction are genuine.
What is not proven: no live agent turn ran, so the wire path (
todo_listtool call → ACP parse → slot store → websocket) is covered by unit tests only, not end to end. Two environment limits blocked it on my machine:kirocrew podcan't start (systemctlhas no D-Bus connection), and I could not authenticate a browser into adev-backend.shinstance. Flagging so a reviewer can weigh it rather than assuming end-to-end coverage.Screenshots
Collapsed — a small pill that hugs its content, directly above the composer:
Expanded on click — the full list, completed items checked and struck through:
Full context — collapsed above the composer, and light theme
An earlier revision was full-width when collapsed; it read as another horizontal bar competing with the composer. The screenshots caught it — unit tests never would have.
Gates
Backend 18003 passed. Frontend 4695 passed across 406 files. isort, flake8, mypy (481 files), tsc, eslint all clean.
Three pre-existing failures in
test/test_dashboard_origin.pyreproduce identically on a cleanorigin/mainworktree (aKIROCREW_PORT=6776in my host env) and are unrelated. Two further failures in the first full run were flakes:test_apps_registry.pysandbox-backend contention under xdist (passes in isolation on main and this branch) and aChatPage.responsivePanelordering flake (full re-run 406/406 green).