-
Notifications
You must be signed in to change notification settings - Fork 828
feat(web): add todo list display in prompt toolbar #1290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
500019e
dcefbc2
7fa0f6a
52ea29c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { type ReactElement, memo } from "react"; | ||
| import { | ||
| CheckCircle2Icon, | ||
| CheckSquare2Icon, | ||
| ChevronDownIcon, | ||
| CircleDotIcon, | ||
| CircleIcon, | ||
| } from "lucide-react"; | ||
| import { cn } from "@/lib/utils"; | ||
| import type { TodoItem } from "@/features/tool/store"; | ||
|
|
||
| // ─── Panel ─────────────────────────────────────────────────── | ||
|
|
||
| type ToolbarTodoPanelProps = { | ||
| items: TodoItem[]; | ||
| }; | ||
|
|
||
| export const ToolbarTodoPanel = memo(function ToolbarTodoPanelComponent({ | ||
| items, | ||
| }: ToolbarTodoPanelProps): ReactElement { | ||
| return ( | ||
| <> | ||
| {items.map((item, index) => ( | ||
| <div | ||
| key={`${index}-${item.title}`} | ||
| className="flex items-center gap-2 px-3 py-1 text-xs" | ||
| > | ||
| {item.status === "done" && ( | ||
| <CheckCircle2Icon className="size-3 flex-shrink-0 text-emerald-500" /> | ||
| )} | ||
| {item.status === "in_progress" && ( | ||
| <CircleDotIcon className="size-3 flex-shrink-0 text-blue-500" /> | ||
| )} | ||
| {item.status === "pending" && ( | ||
| <CircleIcon className="size-3 flex-shrink-0 text-muted-foreground" /> | ||
| )} | ||
| <span | ||
| className={cn( | ||
| "truncate", | ||
| item.status === "done" | ||
| ? "line-through text-muted-foreground" | ||
| : item.status === "in_progress" | ||
| ? "text-foreground font-medium" | ||
| : "text-muted-foreground", | ||
| )} | ||
| > | ||
| {item.title} | ||
| </span> | ||
| </div> | ||
| ))} | ||
| </> | ||
| ); | ||
| }); | ||
|
|
||
| // ─── Tab ───────────────────────────────────────────────────── | ||
|
|
||
| type ToolbarTodoTabProps = { | ||
| items: TodoItem[]; | ||
| isActive: boolean; | ||
| onToggle: () => void; | ||
| }; | ||
|
|
||
| export const ToolbarTodoTab = memo(function ToolbarTodoTabComponent({ | ||
| items, | ||
| isActive, | ||
| onToggle, | ||
| }: ToolbarTodoTabProps): ReactElement { | ||
| const doneCount = items.filter((i) => i.status === "done").length; | ||
| const totalCount = items.length; | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={onToggle} | ||
| className={cn( | ||
| "flex items-center gap-1.5 h-7 px-2.5 rounded-full text-xs font-medium transition-colors cursor-pointer border", | ||
| isActive | ||
| ? "bg-secondary text-foreground border-border shadow-sm" | ||
| : "bg-transparent text-muted-foreground border-border/60 hover:text-foreground hover:border-border", | ||
| )} | ||
| > | ||
| <CheckSquare2Icon className="size-3" /> | ||
| <span> | ||
| {doneCount}/{totalCount} Tasks | ||
| </span> | ||
| <ChevronDownIcon | ||
| className={cn( | ||
| "size-3 transition-transform duration-200", | ||
| isActive && "rotate-180", | ||
| )} | ||
| /> | ||
| </button> | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -134,7 +134,7 @@ import { | |||||||||||||||||||||
| } from "./wireTypes"; | ||||||||||||||||||||||
| import { createMessageId, getApiBaseUrl } from "./utils"; | ||||||||||||||||||||||
| import { kimiCliVersion } from "@/lib/version"; | ||||||||||||||||||||||
| import { handleToolResult } from "@/features/tool/store"; | ||||||||||||||||||||||
| import { handleToolResult, useToolEventsStore, type TodoItem } from "@/features/tool/store"; | ||||||||||||||||||||||
| import { v4 as uuidV4 } from "uuid"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Regex patterns moved to top level for performance | ||||||||||||||||||||||
|
|
@@ -1291,6 +1291,18 @@ export function useSessionStream( | |||||||||||||||||||||
| isReplay, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Extract todo list from display blocks | ||||||||||||||||||||||
| if (!isReplay && Array.isArray(return_value.display)) { | ||||||||||||||||||||||
|
||||||||||||||||||||||
| if (!isReplay && Array.isArray(return_value.display)) { | |
| if (Array.isArray(return_value.display)) { |
Copilot
AI
Feb 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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[], | |
| ); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 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
prevQueueLenReftracking (lines 59-65 ofindex.tsx), but the todo tab lacks equivalent logic.Root Cause
When the first queue item is added, the queue tab auto-opens:
There is no corresponding effect for the todo tab. When
todoItemstransitions from empty to non-empty (i.e., the firstSetTodoListtool 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
Was this helpful? React with 👍 or 👎 to provide feedback.