feat(web): add todo list display in prompt toolbar#1290
Conversation
Show the current SetTodoList state in the web UI toolbar. A new "Tasks" tab appears when todo items exist, displaying each item with status icons (pending/in_progress/done) and a progress counter (x/y Tasks). The tab auto-closes when the list is cleared and todo items are reset on session change. Entire-Checkpoint: 9c89e1470297
Entire-Checkpoint: 3dfeb22da51f
| if (!isReplay && Array.isArray(return_value.display)) { | ||
| const todoBlock = return_value.display.find( | ||
| (d: { type: string }) => d.type === "todo", | ||
| ); | ||
| if (todoBlock) { | ||
| useToolEventsStore.getState().setTodoItems( | ||
| (todoBlock as { type: string; items: TodoItem[] }).items, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Todo toolbar state not restored during history replay (reconnect/page refresh)
When the page is refreshed or the WebSocket reconnects, history events are replayed. The !isReplay guard at useSessionStream.ts:1296 causes all replayed ToolResult events (including SetTodoList results) to be skipped for toolbar state extraction. This means the toolbar's "Tasks" tab disappears after a reconnection even though the chat messages correctly show the todo display blocks from the replay.
Root cause and impact
The code follows the same !isReplay pattern used by handleToolResult at web/src/features/tool/store.ts:48, which tracks newFiles — a notification-style badge that intentionally shouldn't re-trigger on replay. However, todoItems represents persistent UI state (the current todo list), not a transient notification. The last SetTodoList call's display block contains the full current todo list, and skipping it during replay loses that state.
Scenario:
- User triggers
SetTodoList→ toolbar shows "2/5 Tasks" tab - User refreshes the page (or WebSocket reconnects)
- History replay runs — all events have
isReplay = true - The
if (!isReplay && ...)check skips todo extraction - Toolbar shows no "Tasks" tab, but the chat message still renders the todo display block
Impact: Inconsistent UI after any reconnection — chat shows the todo list in messages but the toolbar indicator is missing until the next SetTodoList tool call.
Prompt for agents
In web/src/hooks/useSessionStream.ts around line 1296, the todo extraction from display blocks should also run during replay to restore the toolbar state. However, during replay you only want the LAST todo state (not intermediate ones). One approach: remove the `!isReplay` guard so todo items are always extracted. Since each SetTodoList call replaces the full list via `setTodoItems`, the last replayed event will set the correct final state. Change line 1296 from `if (!isReplay && Array.isArray(return_value.display))` to `if (Array.isArray(return_value.display))`. This aligns with the fact that todoItems represents persistent state rather than transient notifications.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Pull request overview
Adds a prompt-toolbar “Tasks” UI that surfaces the current SetTodoList state in the web frontend, backed by a Zustand store updated from tool-result display blocks.
Changes:
- Introduce
ToolbarTodoTab/ToolbarTodoPanelto render task progress and per-item status. - Track
todoItemsinuseToolEventsStoreand update/reset it fromuseSessionStream. - Document the feature in English/Chinese release notes and root
CHANGELOG.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/hooks/useSessionStream.ts | Extracts todo display blocks from tool results and clears todos on session switch. |
| web/src/features/tool/store.ts | Adds todoItems state + setters/clearer to the tool events store. |
| web/src/features/chat/components/prompt-toolbar/toolbar-todo.tsx | New tab/panel components for rendering todo progress and list. |
| web/src/features/chat/components/prompt-toolbar/index.tsx | Wires the todo tab/panel into the prompt toolbar and auto-closes when empty. |
| docs/zh/release-notes/changelog.md | Adds unreleased note for the new toolbar todo display (ZH). |
| docs/en/release-notes/changelog.md | Adds unreleased note for the new toolbar todo display (EN). |
| CHANGELOG.md | Adds unreleased note for the new toolbar todo display. |
Comments suppressed due to low confidence (1)
web/src/features/chat/components/prompt-toolbar/index.tsx:65
- PR description mentions the todo panel auto-opens when the todo list becomes active, but the toolbar only auto-opens the queue tab. Consider adding an effect similar to the queue auto-open that opens the
todotab whentodoItemstransitions from empty to non-empty (and keep the existing auto-close behavior when it becomes empty).
// Auto-open queue tab when first item is added
useEffect(() => {
if (prevQueueLenRef.current === 0 && queue.length > 0) {
setActiveTab("queue");
}
prevQueueLenRef.current = queue.length;
}, [queue.length]);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| useToolEventsStore.getState().setTodoItems( | ||
| (todoBlock as { type: string; items: TodoItem[] }).items, | ||
| ); |
There was a problem hiding this comment.
ToolResult.return_value.display items are typed (and handled elsewhere) as display blocks with a data payload, but this code assumes the todo list lives directly on the block as .items. If the backend sends { type: "todo", data: { items: [...] } } (or any variant without a top-level items), this will set todoItems to undefined and downstream code like todoItems.length will throw. Extract items from (todoBlock as any).items ?? (todoBlock as any).data?.items, validate with Array.isArray, and only then update the store (optionally normalize status values).
| useToolEventsStore.getState().setTodoItems( | |
| (todoBlock as { type: string; items: TodoItem[] }).items, | |
| ); | |
| const rawItems = | |
| (todoBlock as any).items ?? (todoBlock as any).data?.items; | |
| if (Array.isArray(rawItems)) { | |
| useToolEventsStore.getState().setTodoItems( | |
| rawItems as TodoItem[], | |
| ); | |
| } |
| } | ||
|
|
||
| // Extract todo list from display blocks | ||
| if (!isReplay && Array.isArray(return_value.display)) { |
There was a problem hiding this comment.
The todo extraction is skipped during history replay (!isReplay). That means when a user reconnects to an existing session (or refreshes the page), the toolbar won't reflect the current SetTodoList state until a new todo update happens in the live stream. If the goal is to show the current todo state for the session, consider applying the extraction during replay as well (it will naturally converge to the latest todo block by the end of the replay), while keeping any “auto-open” behavior gated to non-replay if needed.
| if (!isReplay && Array.isArray(return_value.display)) { | |
| if (Array.isArray(return_value.display)) { |
| // eslint-disable-next-line react/no-array-index-key | ||
| key={i} |
There was a problem hiding this comment.
ToolbarTodoPanel uses the array index as the React key. If the todo list can reorder (or items can be inserted/removed), this can cause incorrect row/state reuse. Prefer a more stable key (e.g., a backend-provided id, or a combination like title + status, possibly with the index only as a last-resort disambiguator).
| // eslint-disable-next-line react/no-array-index-key | |
| key={i} | |
| key={`${item.title}-${item.status}-${i}`} |
- Cast todoBlock through unknown to satisfy TS2352 type overlap requirement - Use item.title as React key instead of array index to fix noArrayIndexKey lint error Entire-Checkpoint: 28db10720a96
| useEffect(() => { | ||
| if (activeTab === "queue" && !hasQueue) setActiveTab(null); | ||
| if (activeTab === "changes" && !hasChanges) setActiveTab(null); | ||
| }, [activeTab, hasQueue, hasChanges]); | ||
| if (activeTab === "todo" && !hasTodo) setActiveTab(null); | ||
| }, [activeTab, hasQueue, hasChanges, hasTodo]); |
There was a problem hiding this comment.
🟡 Todo tab missing auto-open behavior described in PR
The PR description states the Tasks tab "auto-opens an expandable panel with per-item status icons", but the implementation has no auto-open logic for the todo tab. The queue tab has auto-open behavior via prevQueueLenRef tracking (lines 59-65 of index.tsx), but the todo tab lacks equivalent logic.
Root Cause
When the first queue item is added, the queue tab auto-opens:
useEffect(() => {
if (prevQueueLenRef.current === 0 && queue.length > 0) {
setActiveTab("queue");
}
prevQueueLenRef.current = queue.length;
}, [queue.length]);There is no corresponding effect for the todo tab. When todoItems transitions from empty to non-empty (i.e., the first SetTodoList tool result arrives), the tab appears in the toolbar but does not automatically expand its panel. The user must manually click the tab to see the todo items.
Impact: The todo panel never auto-opens, contradicting the stated feature behavior. Users won't see todo progress unless they notice and click the small tab button.
(Refers to lines 67-72)
Prompt for agents
In web/src/features/chat/components/prompt-toolbar/index.tsx, add auto-open logic for the todo tab similar to the queue tab. Add a prevTodoLenRef = useRef(0) alongside prevQueueLenRef (around line 50), then add a useEffect that sets activeTab to 'todo' when todoItems transitions from 0 to >0:
const prevTodoLenRef = useRef(0);
useEffect(() => {
if (prevTodoLenRef.current === 0 && todoItems.length > 0) {
setActiveTab("todo");
}
prevTodoLenRef.current = todoItems.length;
}, [todoItems.length]);
Place this after the existing queue auto-open effect (after line 65).
Was this helpful? React with 👍 or 👎 to provide feedback.
web/src/features/chat/components/prompt-toolbar/toolbar-todo.tsx
Outdated
Show resolved
Hide resolved
Prevents duplicate React key warnings when multiple todo items share the same title, consistent with the pattern in display-content.tsx TodoContent. Entire-Checkpoint: f598a8c28cfd
Related Issue
Resolve #(issue_number)
Description
ToolbarTodoTabandToolbarTodoPanelcomponents that display the currentSetTodoListstate in the web UI prompt toolbarx/y Tasksprogress counter and auto-opens an expandable panel with per-item status icons (pending / in_progress / done)useToolEventsStore) trackstodoItems; stream hook extracts todo data fromdisplayblocks in tool results and clears it on session changeChecklist
make gen-changelogto update the changelog.make gen-docsto update the user documentation.