From 7accf1cd03b9258e01da2b6763d280515c225e6c Mon Sep 17 00:00:00 2001 From: Mufeed VH Date: Fri, 4 Jul 2025 20:38:29 +0530 Subject: [PATCH] feat(components): add comprehensive TodoReadWidget with advanced viewing capabilities - Add TodoReadWidget component with multiple view modes (list, board, timeline, stats) - Implement search and filtering functionality for todo items - Add export capabilities (JSON and Markdown formats) - Include rich UI with animations, progress tracking, and interactive elements - Integrate TodoReadWidget into StreamMessage component for todoread tool support - Add status indicators, dependency tracking, and completion rate calculations --- src/components/StreamMessage.tsx | 9 +- src/components/ToolWidgets.tsx | 512 +++++++++++++++++++++++++++++++ 2 files changed, 520 insertions(+), 1 deletion(-) diff --git a/src/components/StreamMessage.tsx b/src/components/StreamMessage.tsx index 2976750..decf67c 100644 --- a/src/components/StreamMessage.tsx +++ b/src/components/StreamMessage.tsx @@ -15,6 +15,7 @@ import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme"; import type { ClaudeStreamMessage } from "./AgentExecution"; import { TodoWidget, + TodoReadWidget, LSWidget, ReadWidget, ReadResultWidget, @@ -205,6 +206,12 @@ const StreamMessageComponent: React.FC = ({ message, classNa return ; } + // TodoRead tool + if (toolName === "todoread") { + renderedSomething = true; + return ; + } + // LS tool if (toolName === "ls" && input?.path) { renderedSomething = true; @@ -368,7 +375,7 @@ const StreamMessageComponent: React.FC = ({ message, classNa const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id); if (toolUse) { const toolName = toolUse.name?.toLowerCase(); - const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep','websearch','webfetch']; + const toolsWithWidgets = ['task','edit','multiedit','todowrite','todoread','ls','read','glob','bash','write','grep','websearch','webfetch']; if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { hasCorrespondingWidget = true; } diff --git a/src/components/ToolWidgets.tsx b/src/components/ToolWidgets.tsx index 589b222..5c2bdc8 100644 --- a/src/components/ToolWidgets.tsx +++ b/src/components/ToolWidgets.tsx @@ -41,6 +41,12 @@ import { FileCode, Folder, ChevronUp, + BarChart3, + Download, + LayoutGrid, + LayoutList, + Activity, + Hash, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; @@ -53,6 +59,9 @@ import { Card, CardContent } from "@/components/ui/card"; import { detectLinks, makeLinksClickable } from "@/lib/linkDetector"; import ReactMarkdown from "react-markdown"; import { open } from "@tauri-apps/plugin-shell"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Input } from "@/components/ui/input"; +import { motion, AnimatePresence } from "framer-motion"; /** * Widget for TodoWrite tool - displays a beautiful TODO list @@ -2472,3 +2481,506 @@ export const WebFetchWidget: React.FC<{ ); }; + +/** + * Widget for TodoRead tool - displays todos with advanced viewing capabilities + */ +export const TodoReadWidget: React.FC<{ todos?: any[]; result?: any }> = ({ todos: inputTodos, result }) => { + // Extract todos from result if not directly provided + let todos: any[] = inputTodos || []; + if (!todos.length && result) { + if (typeof result === 'object' && Array.isArray(result.todos)) { + todos = result.todos; + } else if (typeof result.content === 'string') { + try { + const parsed = JSON.parse(result.content); + if (Array.isArray(parsed)) todos = parsed; + else if (parsed.todos) todos = parsed.todos; + } catch (e) { + // Not JSON, ignore + } + } + } + + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [viewMode, setViewMode] = useState<"list" | "board" | "timeline" | "stats">("list"); + const [expandedTodos, setExpandedTodos] = useState>(new Set()); + + // Status icons and colors + const statusConfig = { + completed: { + icon: , + color: "text-green-500", + bgColor: "bg-green-500/10", + borderColor: "border-green-500/20", + label: "Completed" + }, + in_progress: { + icon: , + color: "text-blue-500", + bgColor: "bg-blue-500/10", + borderColor: "border-blue-500/20", + label: "In Progress" + }, + pending: { + icon: , + color: "text-muted-foreground", + bgColor: "bg-muted/50", + borderColor: "border-muted", + label: "Pending" + }, + cancelled: { + icon: , + color: "text-red-500", + bgColor: "bg-red-500/10", + borderColor: "border-red-500/20", + label: "Cancelled" + } + }; + + // Filter todos based on search and status + const filteredTodos = todos.filter(todo => { + const matchesSearch = !searchQuery || + todo.content.toLowerCase().includes(searchQuery.toLowerCase()) || + (todo.id && todo.id.toLowerCase().includes(searchQuery.toLowerCase())); + + const matchesStatus = statusFilter === "all" || todo.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + // Calculate statistics + const stats = { + total: todos.length, + completed: todos.filter(t => t.status === "completed").length, + inProgress: todos.filter(t => t.status === "in_progress").length, + pending: todos.filter(t => t.status === "pending").length, + cancelled: todos.filter(t => t.status === "cancelled").length, + completionRate: todos.length > 0 + ? Math.round((todos.filter(t => t.status === "completed").length / todos.length) * 100) + : 0 + }; + + // Group todos by status for board view + const todosByStatus = { + pending: filteredTodos.filter(t => t.status === "pending"), + in_progress: filteredTodos.filter(t => t.status === "in_progress"), + completed: filteredTodos.filter(t => t.status === "completed"), + cancelled: filteredTodos.filter(t => t.status === "cancelled") + }; + + // Toggle expanded state for a todo + const toggleExpanded = (todoId: string) => { + setExpandedTodos(prev => { + const next = new Set(prev); + if (next.has(todoId)) { + next.delete(todoId); + } else { + next.add(todoId); + } + return next; + }); + }; + + // Export todos as JSON + const exportAsJson = () => { + const dataStr = JSON.stringify(todos, null, 2); + const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); + const exportFileDefaultName = 'todos.json'; + const linkElement = document.createElement('a'); + linkElement.setAttribute('href', dataUri); + linkElement.setAttribute('download', exportFileDefaultName); + linkElement.click(); + }; + + // Export todos as Markdown + const exportAsMarkdown = () => { + let markdown = "# Todo List\n\n"; + markdown += `**Total**: ${stats.total} | **Completed**: ${stats.completed} | **In Progress**: ${stats.inProgress} | **Pending**: ${stats.pending}\n\n`; + + const statusGroups = ["pending", "in_progress", "completed", "cancelled"]; + statusGroups.forEach(status => { + const todosInStatus = todos.filter(t => t.status === status); + if (todosInStatus.length > 0) { + markdown += `## ${statusConfig[status as keyof typeof statusConfig]?.label || status}\n\n`; + todosInStatus.forEach(todo => { + const checkbox = todo.status === "completed" ? "[x]" : "[ ]"; + markdown += `- ${checkbox} ${todo.content}${todo.id ? ` (${todo.id})` : ""}\n`; + if (todo.dependencies?.length > 0) { + markdown += ` - Dependencies: ${todo.dependencies.join(", ")}\n`; + } + }); + markdown += "\n"; + } + }); + + const dataUri = 'data:text/markdown;charset=utf-8,'+ encodeURIComponent(markdown); + const linkElement = document.createElement('a'); + linkElement.setAttribute('href', dataUri); + linkElement.setAttribute('download', 'todos.md'); + linkElement.click(); + }; + + // Render todo card + const TodoCard = ({ todo, isExpanded }: { todo: any; isExpanded: boolean }) => { + const config = statusConfig[todo.status as keyof typeof statusConfig] || statusConfig.pending; + + return ( + todo.id && toggleExpanded(todo.id)} + > +
+
+ {config.icon} +
+
+

+ {todo.content} +

+ + {/* Todo metadata */} +
+ {todo.id && ( +
+ + {todo.id} +
+ )} + {todo.dependencies?.length > 0 && ( +
+ + {todo.dependencies.length} deps +
+ )} +
+ + {/* Expanded details */} + + {isExpanded && todo.dependencies?.length > 0 && ( + +
+ Dependencies: +
+ {todo.dependencies.map((dep: string) => ( + + {dep} + + ))} +
+
+
+ )} +
+
+
+
+ ); + }; + + // Render statistics view + const StatsView = () => ( +
+ {/* Overall Progress */} + +
+

Overall Progress

+ {stats.completionRate}% +
+
+ +
+
+ + {/* Status Breakdown */} +
+ {Object.entries(statusConfig).map(([status, config]) => { + const count = stats[status as keyof typeof stats] || 0; + const percentage = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0; + + return ( + +
+
{config.icon}
+
+

{config.label}

+

{count}

+

{percentage}%

+
+
+
+ ); + })} +
+ + {/* Activity Chart */} + +
+ +

Activity Overview

+
+
+ {Object.entries(statusConfig).map(([status, config]) => { + const count = stats[status as keyof typeof stats] || 0; + const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0; + + return ( +
+ {config.label} +
+ +
+ {count} +
+ ); + })} +
+
+
+ ); + + // Render board view + const BoardView = () => ( +
+ {Object.entries(todosByStatus).map(([status, todos]) => { + const config = statusConfig[status as keyof typeof statusConfig]; + + return ( +
+
+
{config.icon}
+

{config.label}

+ + {todos.length} + +
+
+ {todos.map(todo => ( + + ))} + {todos.length === 0 && ( +

+ No todos +

+ )} +
+
+ ); + })} +
+ ); + + // Render timeline view + const TimelineView = () => { + // Group todos by their dependencies to create a timeline + const rootTodos = todos.filter(t => !t.dependencies || t.dependencies.length === 0); + const rendered = new Set(); + + const renderTodoWithDependents = (todo: any, level = 0) => { + if (rendered.has(todo.id)) return null; + rendered.add(todo.id); + + const dependents = todos.filter(t => + t.dependencies?.includes(todo.id) && !rendered.has(t.id) + ); + + return ( +
+ {level > 0 && ( +
+ )} +
0 && "ml-12")}> +
+
+ {dependents.length > 0 && ( +
+ )} +
+
+ +
+
+ {dependents.map(dep => renderTodoWithDependents(dep, level + 1))} +
+ ); + }; + + return ( +
+ {rootTodos.map(todo => renderTodoWithDependents(todo))} + {todos.filter(t => !rendered.has(t.id)).map(todo => renderTodoWithDependents(todo))} +
+ ); + }; + + return ( +
+ {/* Header */} +
+
+ +
+

Todo Overview

+

+ {stats.total} total • {stats.completed} completed • {stats.completionRate}% done +

+
+
+ + {/* Export Options */} +
+ + +
+
+ + {/* Search and Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9 h-9" + /> +
+ +
+
+ {["all", "pending", "in_progress", "completed", "cancelled"].map(status => ( + + ))} +
+
+
+ + {/* View Mode Tabs */} + setViewMode(v as typeof viewMode)}> + + + + List + + + + Board + + + + Timeline + + + + Stats + + + + +
+ + {filteredTodos.map(todo => ( + + ))} + + {filteredTodos.length === 0 && ( +
+ {searchQuery || statusFilter !== "all" + ? "No todos match your filters" + : "No todos available"} +
+ )} +
+
+ + + + + + + + + + + + +
+
+ ); +};