From c098444fb209f3b3f24bf6093313cc73e4ee1288 Mon Sep 17 00:00:00 2001 From: Adam Cohen Hillel Date: Thu, 17 Apr 2025 02:02:03 +0100 Subject: [PATCH] todo panel --- .../app/dashboard/agents/[threadId]/page.tsx | 10 + frontend/src/components/thread/todo-panel.tsx | 278 ++++++++++++++++++ .../thread/tool-call-side-panel.tsx | 12 + 3 files changed, 300 insertions(+) create mode 100644 frontend/src/components/thread/todo-panel.tsx diff --git a/frontend/src/app/dashboard/agents/[threadId]/page.tsx b/frontend/src/app/dashboard/agents/[threadId]/page.tsx index 1a26747a..a58042f9 100644 --- a/frontend/src/app/dashboard/agents/[threadId]/page.tsx +++ b/frontend/src/app/dashboard/agents/[threadId]/page.tsx @@ -17,6 +17,7 @@ import { FileViewerModal } from '@/components/thread/file-viewer-modal'; import { SiteHeader } from "@/components/thread/thread-site-header" import { ToolCallSidePanel, SidePanelContent, ToolCallData } from "@/components/thread/tool-call-side-panel"; import { useSidebar } from "@/components/ui/sidebar"; +import { TodoPanel } from '@/components/thread/todo-panel'; // Define a type for the params to make React.use() work properly type ThreadParams = { @@ -1392,6 +1393,15 @@ export default function ThreadPage({ params }: { params: Promise }
+ {/* Show Todo panel above chat input when side panel is closed */} + {!isSidePanelOpen && sandboxId && ( + + )} + ([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [lastRefreshed, setLastRefreshed] = useState(new Date()); + const [isCollapsed, setIsCollapsed] = useState(true); + + const fetchTodoContent = async () => { + if (!sandboxId) return; + + setIsLoading(true); + setError(null); + + try { + const content = await getSandboxFileContent(sandboxId, '/workspace/todo.md'); + + if (typeof content === 'string') { + parseTasks(content); + } else if (content instanceof Blob) { + // Handle blob content (convert to text) + const text = await content.text(); + parseTasks(text); + } else { + throw new Error('Unexpected content format'); + } + } catch (err) { + console.error('Failed to load todo.md:', err); + // Don't show error toast when file doesn't exist yet, as this is expected initially + if (err instanceof Error && !err.message.includes('404')) { + setError('Tasks will show up shortly'); + // toast.error('Failed to load todo file'); + } else { + setTasks([]); + } + } finally { + setIsLoading(false); + } + }; + + // Extract just the task items from todo.md content + const parseTasks = (content: string) => { + if (!content) { + setTasks([]); + return; + } + + const lines = content.split('\n'); + const extractedTasks: TodoTask[] = []; + + lines.forEach(line => { + const trimmedLine = line.trim(); + + // Only look for task items with checkbox + const taskMatch = trimmedLine.match(/^\s*[-*]\s+\[([ x])\]\s+(.+)$/); + if (taskMatch) { + extractedTasks.push({ + text: taskMatch[2].trim(), + completed: taskMatch[1] === 'x' + }); + } + }); + + setTasks(extractedTasks); + }; + + // Fetch the todo.md file when component mounts or sandboxId changes + useEffect(() => { + if (sandboxId) { + fetchTodoContent(); + } + }, [sandboxId, lastRefreshed]); + + // Set up periodic refresh (every 10 seconds) + useEffect(() => { + const intervalId = setInterval(() => { + fetchTodoContent(); + }, 10000); + + return () => clearInterval(intervalId); + }, [sandboxId]); + + const handleRefresh = () => { + setLastRefreshed(new Date()); + }; + + const toggleCollapse = () => { + setIsCollapsed(!isCollapsed); + }; + + // Only get incomplete tasks + const incompleteTasks = tasks.filter(task => !task.completed); + const completedTasks = tasks.filter(task => task.completed); + + // Get first incomplete task + const firstIncompleteTask = incompleteTasks.length > 0 ? incompleteTasks[0] : null; + + // Styling based on whether the side panel is open + const containerClasses = isSidePanelOpen + ? 'border-t p-2 bg-sidebar' // At bottom of side panel + : 'border rounded-md shadow-sm mb-2 bg-card'; // Above chat input + + const heightClasses = isSidePanelOpen + ? isCollapsed ? 'h-[70px]' : 'h-[200px]' // Fixed height in side panel + : isCollapsed ? 'max-h-[70px]' : 'max-h-[200px]'; // Max height above chat input + + return ( +
+ {isLoading ? ( +
+ + +
+ ) : error ? ( +
+

{error}

+
+ ) : tasks.length === 0 ? ( +
+ Tasks will show up shortly +
+ ) : ( + <> + {/* Display tasks based on collapsed state */} + {isCollapsed ? ( +
+ {firstIncompleteTask ? ( +
+
+ + {/* Pulse animation inside circle */} + +
+ {firstIncompleteTask.text} +
+ + +
+
+ ) : ( +
+ + All tasks completed! +
+ + +
+
+ )} +
+ ) : ( + <> +
+
+ + +
+
+ +
+ {/* Incomplete tasks first */} + {incompleteTasks.map((task, index) => ( +
+
+ +
+ {task.text} +
+ ))} + + {/* Show completed tasks after incomplete ones */} + {completedTasks.length > 0 && ( + <> + {incompleteTasks.length > 0 &&
} + + {completedTasks.map((task, index) => ( +
+
+ +
+ {task.text} +
+ ))} + + )} +
+
+ + )} + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/tool-call-side-panel.tsx b/frontend/src/components/thread/tool-call-side-panel.tsx index 14b4e033..4b8c151b 100644 --- a/frontend/src/components/thread/tool-call-side-panel.tsx +++ b/frontend/src/components/thread/tool-call-side-panel.tsx @@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button"; import { X, Package, Info, Terminal, CheckCircle, SkipBack, SkipForward, MonitorPlay, FileSymlink, FileDiff, FileEdit, Search, Globe, ExternalLink, Database, Code, ListFilter } from "lucide-react"; import { Slider } from "@/components/ui/slider"; import { Project } from "@/lib/api"; +import { TodoPanel } from "./todo-panel"; // Define the structure for LIVE tool call data (from streaming) export interface ToolCallData { @@ -1186,6 +1187,9 @@ export function ToolCallSidePanel({ // Get VNC preview URL from project if available const vncPreviewUrl = project?.sandbox?.vnc_preview; + // Get the sandbox ID from project for todo.md fetching + const sandboxId = project?.sandbox?.id || null; + return (
)}
+ + {/* Todo Panel at the bottom of side panel */} + {sandboxId && ( + + )} )}