diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx
deleted file mode 100644
index 8876c15a..00000000
--- a/src/components/TodoList.jsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React from 'react';
-import { Badge } from './ui/badge';
-import { CheckCircle2, Clock, Circle } from 'lucide-react';
-
-const TodoList = ({ todos, isResult = false }) => {
- if (!todos || !Array.isArray(todos)) {
- return null;
- }
-
- const getStatusIcon = (status) => {
- switch (status) {
- case 'completed':
- return ;
- case 'in_progress':
- return ;
- case 'pending':
- default:
- return ;
- }
- };
-
- const getStatusColor = (status) => {
- switch (status) {
- case 'completed':
- return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800';
- case 'in_progress':
- return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800';
- case 'pending':
- default:
- return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700';
- }
- };
-
- const getPriorityColor = (priority) => {
- switch (priority) {
- case 'high':
- return 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800';
- case 'medium':
- return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800';
- case 'low':
- default:
- return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700';
- }
- };
-
- return (
-
- {isResult && (
-
- Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
-
- )}
-
- {todos.map((todo, index) => (
-
-
- {getStatusIcon(todo.status)}
-
-
-
-
-
- {todo.content}
-
-
-
-
- {todo.priority}
-
-
- {todo.status.replace('_', ' ')}
-
-
-
-
-
- ))}
-
- );
-};
-
-export default TodoList;
diff --git a/src/components/chat/tools/components/ContentRenderers/TodoList.tsx b/src/components/chat/tools/components/ContentRenderers/TodoList.tsx
new file mode 100644
index 00000000..1df49564
--- /dev/null
+++ b/src/components/chat/tools/components/ContentRenderers/TodoList.tsx
@@ -0,0 +1,152 @@
+import { memo, useMemo } from 'react';
+import { CheckCircle2, Circle, Clock, type LucideIcon } from 'lucide-react';
+import { Badge } from '../../../../ui/badge';
+
+type TodoStatus = 'completed' | 'in_progress' | 'pending';
+type TodoPriority = 'high' | 'medium' | 'low';
+
+export type TodoItem = {
+ id?: string;
+ content: string;
+ status: string;
+ priority?: string;
+};
+
+type NormalizedTodoItem = {
+ id?: string;
+ content: string;
+ status: TodoStatus;
+ priority: TodoPriority;
+};
+
+type StatusConfig = {
+ icon: LucideIcon;
+ iconClassName: string;
+ badgeClassName: string;
+ textClassName: string;
+};
+
+// Centralized visual config keeps rendering logic compact and easier to scan.
+const STATUS_CONFIG: Record = {
+ completed: {
+ icon: CheckCircle2,
+ iconClassName: 'w-3.5 h-3.5 text-green-500 dark:text-green-400',
+ badgeClassName:
+ 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800',
+ textClassName: 'line-through text-gray-500 dark:text-gray-400',
+ },
+ in_progress: {
+ icon: Clock,
+ iconClassName: 'w-3.5 h-3.5 text-blue-500 dark:text-blue-400',
+ badgeClassName:
+ 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800',
+ textClassName: 'text-gray-900 dark:text-gray-100',
+ },
+ pending: {
+ icon: Circle,
+ iconClassName: 'w-3.5 h-3.5 text-gray-400 dark:text-gray-500',
+ badgeClassName:
+ 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700',
+ textClassName: 'text-gray-900 dark:text-gray-100',
+ },
+};
+
+const PRIORITY_BADGE_CLASS: Record = {
+ high: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800',
+ medium:
+ 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800',
+ low: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700',
+};
+
+// Incoming tool payloads can vary; normalize to supported UI states.
+const normalizeStatus = (status: string): TodoStatus => {
+ if (status === 'completed' || status === 'in_progress') {
+ return status;
+ }
+ return 'pending';
+};
+
+const normalizePriority = (priority?: string): TodoPriority => {
+ if (priority === 'high' || priority === 'medium') {
+ return priority;
+ }
+ return 'low';
+};
+
+const TodoRow = memo(
+ ({ todo }: { todo: NormalizedTodoItem }) => {
+ const statusConfig = STATUS_CONFIG[todo.status];
+ const StatusIcon = statusConfig.icon;
+
+ return (
+
+
+
+
+
+
+
+ {todo.content}
+
+
+
+ {todo.priority}
+
+
+ {todo.status.replace('_', ' ')}
+
+
+
+
+
+ );
+ }
+);
+
+const TodoList = memo(
+ ({
+ todos,
+ isResult = false,
+ }: {
+ todos: TodoItem[];
+ isResult?: boolean;
+ }) => {
+ // Memoize normalization to avoid recomputing list metadata on every render.
+ const normalizedTodos = useMemo(
+ () =>
+ todos.map((todo) => ({
+ id: todo.id,
+ content: todo.content,
+ status: normalizeStatus(todo.status),
+ priority: normalizePriority(todo.priority),
+ })),
+ [todos]
+ );
+
+ if (normalizedTodos.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {isResult && (
+
+ Todo List ({normalizedTodos.length}{' '}
+ {normalizedTodos.length === 1 ? 'item' : 'items'})
+
+ )}
+ {normalizedTodos.map((todo, index) => (
+
+ ))}
+
+ );
+ }
+);
+
+export default TodoList;
diff --git a/src/components/chat/tools/components/ContentRenderers/TodoListContent.tsx b/src/components/chat/tools/components/ContentRenderers/TodoListContent.tsx
index 5d318ba1..decbc9c3 100644
--- a/src/components/chat/tools/components/ContentRenderers/TodoListContent.tsx
+++ b/src/components/chat/tools/components/ContentRenderers/TodoListContent.tsx
@@ -1,23 +1,40 @@
-import React from 'react';
-import TodoList from '../../../../TodoList';
+import { memo, useMemo } from 'react';
+import TodoList, { type TodoItem } from './TodoList';
-interface TodoListContentProps {
- todos: Array<{
- id?: string;
- content: string;
- status: string;
- priority?: string;
- }>;
- isResult?: boolean;
-}
+const isTodoItem = (value: unknown): value is TodoItem => {
+ if (typeof value !== 'object' || value === null) {
+ return false;
+ }
+
+ const todo = value as Record;
+ return typeof todo.content === 'string' && typeof todo.status === 'string';
+};
/**
* Renders a todo list
* Used by: TodoWrite, TodoRead
*/
-export const TodoListContent: React.FC = ({
- todos,
- isResult = false
-}) => {
- return ;
-};
+export const TodoListContent = memo(
+ ({
+ todos,
+ isResult = false,
+ }: {
+ todos: unknown;
+ isResult?: boolean;
+ }) => {
+ const safeTodos = useMemo(() => {
+ if (!Array.isArray(todos)) {
+ return [];
+ }
+
+ // Tool payloads are runtime data; render only validated todo objects.
+ return todos.filter(isTodoItem);
+ }, [todos]);
+
+ if (safeTodos.length === 0) {
+ return null;
+ }
+
+ return ;
+ }
+);