From 5d9b1866681f69b94ffc52bef43a500fa2052f1d Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Tue, 9 Jun 2026 15:11:16 +0800 Subject: [PATCH] fix(desktop): stop spinning animation on archived in_progress todo items When an assistant turn completes, the hoisted todo panel could still show a spinning Loader2Icon for archived in_progress items, making the completed session appear stuck. Add an isLive prop to HoistedTodoPanel that controls whether in_progress items render with animate-spin (live) or a static dot (archived). Thread passes messageStatus === 'running' so the panel distinguishes streaming from completed state. Fixes #42662 --- .../assistant-ui/streaming.test.tsx | 35 +++++++++++++++++++ .../src/components/assistant-ui/thread.tsx | 2 +- .../src/components/assistant-ui/todo-tool.tsx | 25 ++++++++++--- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/streaming.test.tsx b/apps/desktop/src/components/assistant-ui/streaming.test.tsx index c15b4696a217..0b40c6cf7a31 100644 --- a/apps/desktop/src/components/assistant-ui/streaming.test.tsx +++ b/apps/desktop/src/components/assistant-ui/streaming.test.tsx @@ -743,4 +743,39 @@ describe('assistant-ui streaming renderer', () => { expect(thinkingDisclosure).toBeTruthy() expect(Boolean(thinkingDisclosure?.contains(todoPanel as Node))).toBe(false) }) + + it('shows static dot for archived in_progress items after turn completes', () => { + const { container } = render( + + ) + + const todoPanel = container.querySelector('[data-slot="aui_todo-hoisted"]') + expect(todoPanel).toBeTruthy() + + // After turn completion, in_progress items should NOT show spinning icon + const spinningIcon = container.querySelector('.animate-spin') + expect(spinningIcon).toBeNull() + + // The in_progress item should still be rendered with its label + expect(container.textContent).toContain('Step two') + }) + + it('shows spinning icon for in_progress items during live streaming', () => { + const { container } = render( + + ) + + const spinningIcon = container.querySelector('.animate-spin') + expect(spinningIcon).toBeTruthy() + }) }) diff --git a/apps/desktop/src/components/assistant-ui/thread.tsx b/apps/desktop/src/components/assistant-ui/thread.tsx index ca7533c4c04d..7a9bac60d26c 100644 --- a/apps/desktop/src/components/assistant-ui/thread.tsx +++ b/apps/desktop/src/components/assistant-ui/thread.tsx @@ -237,7 +237,7 @@ const AssistantMessage: FC<{ onBranchInNewChat?: (messageId: string) => void }> className="wrap-anywhere min-w-0 max-w-full overflow-hidden text-pretty text-[length:var(--conversation-text-font-size)] leading-(--dt-line-height) text-foreground" data-slot="aui_assistant-message-content" > - {hoistedTodos.length > 0 && } + {hoistedTodos.length > 0 && } {messageStatus === 'running' && } {previewTargets.length > 0 && ( diff --git a/apps/desktop/src/components/assistant-ui/todo-tool.tsx b/apps/desktop/src/components/assistant-ui/todo-tool.tsx index 549c8c3bd9d8..7772cf9f68db 100644 --- a/apps/desktop/src/components/assistant-ui/todo-tool.tsx +++ b/apps/desktop/src/components/assistant-ui/todo-tool.tsx @@ -39,14 +39,22 @@ const headerLabel = (todos: readonly TodoItem[]): string => todos.at(-1)?.content ?? 'Tasks' -const Checkmark: FC<{ status: TodoStatus; label: string }> = ({ status, label }) => { +const Checkmark: FC<{ isLive: boolean; label: string; status: TodoStatus }> = ({ + isLive, + label, + status, +}) => { if (status === 'in_progress') { return ( - + {isLive ? ( + + ) : ( + + )} ) } @@ -68,7 +76,10 @@ const Checkmark: FC<{ status: TodoStatus; label: string }> = ({ status, label }) ) } -export const HoistedTodoPanel: FC<{ todos: TodoItem[] }> = ({ todos }) => { +export const HoistedTodoPanel: FC<{ isLive?: boolean; todos: TodoItem[] }> = ({ + isLive = true, + todos, +}) => { if (!todos.length) { return null } @@ -95,11 +106,15 @@ export const HoistedTodoPanel: FC<{ todos: TodoItem[] }> = ({ todos }) => { // the row so the checkbox glyph dims with the text. className={cn( 'flex min-w-0 items-center gap-3 py-1.5 transition-opacity', - todo.status === 'in_progress' ? 'opacity-100' : 'opacity-45' + todo.status === 'in_progress' && isLive + ? 'opacity-100' + : todo.status === 'in_progress' && !isLive + ? 'opacity-70' + : 'opacity-45' )} key={todo.id} > - + {todo.content} ))}