Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/desktop/src/components/assistant-ui/streaming.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<TodoHarness
message={assistantTodoMessage([
{ content: 'Step one', id: 's1', status: 'completed' },
{ content: 'Step two', id: 's2', status: 'in_progress' },
{ content: 'Step three', id: 's3', status: 'pending' }
], false)}
/>
)

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(
<TodoHarness
message={assistantTodoMessage([
{ content: 'Working on it', id: 'w1', status: 'in_progress' }
], true)}
/>
)

const spinningIcon = container.querySelector('.animate-spin')
expect(spinningIcon).toBeTruthy()
})
})
2 changes: 1 addition & 1 deletion apps/desktop/src/components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && <HoistedTodoPanel todos={hoistedTodos} />}
{hoistedTodos.length > 0 && <HoistedTodoPanel isLive={messageStatus === 'running'} todos={hoistedTodos} />}
<MessagePrimitive.Parts components={MESSAGE_PARTS_COMPONENTS} />
{messageStatus === 'running' && <StreamStallIndicator activity={`${content.length}:${messageText.length}`} />}
{previewTargets.length > 0 && (
Expand Down
25 changes: 20 additions & 5 deletions apps/desktop/src/components/assistant-ui/todo-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<span
aria-label={`In progress: ${label}`}
className="grid size-[1.1rem] shrink-0 place-items-center rounded-full border border-ring/65 bg-[color-mix(in_srgb,var(--dt-ring)_14%,transparent)]"
>
<Loader2Icon className="size-3 animate-spin text-ring" />
{isLive ? (
<Loader2Icon className="size-3 animate-spin text-ring" />
) : (
<span className="size-1.5 rounded-full bg-ring/70" />
)}
</span>
)
}
Expand All @@ -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
}
Expand All @@ -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}
>
<Checkmark label={todo.content} status={todo.status} />
<Checkmark isLive={isLive} label={todo.content} status={todo.status} />
<span className="min-w-0 wrap-anywhere text-[0.8rem] leading-[1.2rem] text-foreground">{todo.content}</span>
</li>
))}
Expand Down
Loading