diff --git a/apps/web/src/app/(authenticated)/usage/Messages.tsx b/apps/web/src/app/(authenticated)/usage/Messages.tsx
index 27f93e1cc9..f3bb448fd0 100644
--- a/apps/web/src/app/(authenticated)/usage/Messages.tsx
+++ b/apps/web/src/app/(authenticated)/usage/Messages.tsx
@@ -11,6 +11,7 @@ import { formatTimestamp } from '@/lib/formatters';
import { useAutoScroll } from '@/hooks/useAutoScroll';
import { CodeBlock } from '@/components/ui/CodeBlock';
import { ToolUsageBadge } from '@/components/ui/ToolUsageBadge';
+import { TodoListDisplay } from '@/components/ui/TodoListDisplay';
import { parseToolUsage } from '@/lib/toolUsageParser';
// Custom component to render links as plain text to avoid broken/nonsensical links
@@ -317,12 +318,18 @@ export const Messages = ({
);
}
- // For other tool messages, render only the tool usage badge in the space between bubbles
+ // For tool messages, render the tool usage badge and todo list if present
if (isTool) {
return (
-
- {message.toolUsage && (
-
+
+ {message.toolUsage?.todoData ? (
+
+ ) : (
+ message.toolUsage && (
+
+
+
+ )
)}
);
diff --git a/apps/web/src/components/ui/TodoListDisplay.test.tsx b/apps/web/src/components/ui/TodoListDisplay.test.tsx
new file mode 100644
index 0000000000..2be38b5922
--- /dev/null
+++ b/apps/web/src/components/ui/TodoListDisplay.test.tsx
@@ -0,0 +1,97 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { TodoListDisplay, type TodoItem } from './TodoListDisplay';
+
+describe('TodoListDisplay', () => {
+ const mockTodos = [
+ {
+ id: '1',
+ content: 'Complete the feature',
+ status: 'completed' as const,
+ },
+ {
+ id: '2',
+ content: 'Write tests',
+ status: 'in_progress' as const,
+ },
+ {
+ id: '3',
+ content: 'Update documentation',
+ status: 'pending' as const,
+ },
+ ];
+
+ it('renders todo list with header and progress', () => {
+ render(
);
+
+ expect(screen.getByText('Todo List Updated')).toBeInTheDocument();
+ expect(screen.getByText('1/3')).toBeInTheDocument(); // completed/total
+ });
+
+ it('shows current task in collapsed view by default', () => {
+ render(
);
+
+ // Should show the in-progress task by default
+ expect(screen.getByText('Write tests')).toBeInTheDocument();
+ // Should not show other tasks in collapsed view
+ expect(screen.queryByText('Complete the feature')).not.toBeInTheDocument();
+ expect(screen.queryByText('Update documentation')).not.toBeInTheDocument();
+ });
+
+ it('shows all tasks when expanded', () => {
+ render(
);
+
+ // Click to expand
+ const expandButton = screen.getByRole('button');
+ fireEvent.click(expandButton);
+
+ // Should show all tasks
+ expect(screen.getByText('Complete the feature')).toBeInTheDocument();
+ expect(screen.getByText('Write tests')).toBeInTheDocument();
+ expect(screen.getByText('Update documentation')).toBeInTheDocument();
+ });
+
+ it('applies correct styling for completed items', () => {
+ render(
);
+
+ // Expand to see all items
+ const expandButton = screen.getByRole('button');
+ fireEvent.click(expandButton);
+
+ const completedItem = screen.getByText('Complete the feature');
+ expect(completedItem).toHaveClass('line-through');
+ });
+
+ it('shows pending task when no in-progress task exists', () => {
+ const todosWithoutInProgress = [
+ {
+ id: '1',
+ content: 'Complete the feature',
+ status: 'completed' as const,
+ },
+ {
+ id: '3',
+ content: 'Update documentation',
+ status: 'pending' as const,
+ },
+ ];
+
+ render(
);
+
+ // Should show the pending task since no in-progress exists
+ expect(screen.getByText('Update documentation')).toBeInTheDocument();
+ });
+
+ it('renders nothing when todos array is empty', () => {
+ const { container } = render(
);
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('renders nothing when todos is undefined', () => {
+ const { container } = render(
+
,
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/apps/web/src/components/ui/TodoListDisplay.tsx b/apps/web/src/components/ui/TodoListDisplay.tsx
new file mode 100644
index 0000000000..314db1cbb2
--- /dev/null
+++ b/apps/web/src/components/ui/TodoListDisplay.tsx
@@ -0,0 +1,133 @@
+import { useState } from 'react';
+import { ChevronRight } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export type TodoItem = {
+ id: string;
+ content: string;
+ status: 'pending' | 'in_progress' | 'completed';
+};
+
+type TodoListDisplayProps = {
+ todos: TodoItem[];
+ className?: string;
+};
+
+const getStatusIcon = (status: TodoItem['status']) => {
+ switch (status) {
+ case 'completed':
+ return '●'; // Filled circle for completed
+ case 'in_progress':
+ return '●'; // Filled circle for in progress
+ case 'pending':
+ return '○'; // Empty circle for pending
+ default:
+ return '○';
+ }
+};
+
+const getStatusColor = (status: TodoItem['status']) => {
+ switch (status) {
+ case 'completed':
+ return 'text-green-500';
+ case 'in_progress':
+ return 'text-yellow-500';
+ case 'pending':
+ return 'text-gray-400';
+ default:
+ return 'text-gray-400';
+ }
+};
+
+export const TodoListDisplay = ({ todos, className }: TodoListDisplayProps) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ if (!todos || todos.length === 0) {
+ return null;
+ }
+
+ // Get counts for each status
+ const completed = todos.filter((t) => t.status === 'completed').length;
+
+ // Find the most relevant current task (first in-progress, or first pending if no in-progress)
+ const currentTask =
+ todos.find((t) => t.status === 'in_progress') ||
+ todos.find((t) => t.status === 'pending');
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ className="flex items-center gap-2 w-full text-left hover:bg-secondary/20 rounded p-1 -m-1 transition-colors"
+ >
+
+
+ Todo List Updated
+
+
+ {completed}/{todos.length}
+
+
+
+ {/* Collapsed view - show current task */}
+ {!isExpanded && currentTask && (
+
+
+ {getStatusIcon(currentTask.status)}
+
+
+ {currentTask.content}
+
+
+ )}
+
+ {/* Expanded view - show all tasks */}
+ {isExpanded && (
+
+ {todos.map((todo) => (
+
+
+ {getStatusIcon(todo.status)}
+
+
+ {todo.content}
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/apps/web/src/lib/toolUsageParser.ts b/apps/web/src/lib/toolUsageParser.ts
index 56a305393f..999e955931 100644
--- a/apps/web/src/lib/toolUsageParser.ts
+++ b/apps/web/src/lib/toolUsageParser.ts
@@ -5,6 +5,13 @@
export type ToolUsage = {
action: string;
details?: string;
+ todoData?: {
+ todos: Array<{
+ id: string;
+ content: string;
+ status: 'pending' | 'in_progress' | 'completed';
+ }>;
+ };
};
/**
@@ -108,6 +115,21 @@ export function extractToolUsageFromAsk(message: {
action: 'Edited',
details: path,
};
+ case 'updateTodoList':
+ // Handle todo list updates
+ if (toolData.todos && Array.isArray(toolData.todos)) {
+ return {
+ action: 'Updated',
+ details: 'todo list',
+ todoData: {
+ todos: toolData.todos,
+ },
+ };
+ }
+ return {
+ action: 'Updated',
+ details: 'todo list',
+ };
default:
return null;
}