mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
Display todos (#219)
This commit is contained in:
parent
5a3f867564
commit
900a9014dc
4 changed files with 263 additions and 4 deletions
|
|
@ -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 (
|
||||
<div key={message.id} className="py-2 pl-4">
|
||||
{message.toolUsage && (
|
||||
<ToolUsageBadge usage={message.toolUsage} />
|
||||
<div key={message.id} className="py-2 space-y-3">
|
||||
{message.toolUsage?.todoData ? (
|
||||
<TodoListDisplay todos={message.toolUsage.todoData.todos} />
|
||||
) : (
|
||||
message.toolUsage && (
|
||||
<div className="pl-4">
|
||||
<ToolUsageBadge usage={message.toolUsage} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
97
apps/web/src/components/ui/TodoListDisplay.test.tsx
Normal file
97
apps/web/src/components/ui/TodoListDisplay.test.tsx
Normal file
|
|
@ -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(<TodoListDisplay todos={mockTodos} />);
|
||||
|
||||
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(<TodoListDisplay todos={mockTodos} />);
|
||||
|
||||
// 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(<TodoListDisplay todos={mockTodos} />);
|
||||
|
||||
// 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(<TodoListDisplay todos={mockTodos} />);
|
||||
|
||||
// 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(<TodoListDisplay todos={todosWithoutInProgress} />);
|
||||
|
||||
// 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(<TodoListDisplay todos={[]} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nothing when todos is undefined', () => {
|
||||
const { container } = render(
|
||||
<TodoListDisplay todos={undefined as unknown as TodoItem[]} />,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
133
apps/web/src/components/ui/TodoListDisplay.tsx
Normal file
133
apps/web/src/components/ui/TodoListDisplay.tsx
Normal file
|
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg bg-secondary/10 border border-border/50 p-3 space-y-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-2 w-full text-left hover:bg-secondary/20 rounded p-1 -m-1 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-3 w-3 text-muted-foreground transition-transform',
|
||||
isExpanded && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Todo List Updated
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground/70 ml-auto">
|
||||
{completed}/{todos.length}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Collapsed view - show current task */}
|
||||
{!isExpanded && currentTask && (
|
||||
<div className="flex items-start gap-2 text-sm pl-5">
|
||||
<span
|
||||
className={cn(
|
||||
'mt-0.5 select-none',
|
||||
getStatusColor(currentTask.status),
|
||||
)}
|
||||
>
|
||||
{getStatusIcon(currentTask.status)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'leading-relaxed',
|
||||
currentTask.status === 'completed' &&
|
||||
'line-through text-muted-foreground/70',
|
||||
)}
|
||||
>
|
||||
{currentTask.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded view - show all tasks */}
|
||||
{isExpanded && (
|
||||
<ul className="space-y-1.5 pl-5">
|
||||
{todos.map((todo) => (
|
||||
<li key={todo.id} className="flex items-start gap-2 text-sm">
|
||||
<span
|
||||
className={cn(
|
||||
'mt-0.5 select-none',
|
||||
getStatusColor(todo.status),
|
||||
)}
|
||||
>
|
||||
{getStatusIcon(todo.status)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'leading-relaxed',
|
||||
todo.status === 'completed' &&
|
||||
'line-through text-muted-foreground/70',
|
||||
)}
|
||||
>
|
||||
{todo.content}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue