diff --git a/apps/web/src/components/configuration-sidebar/index.tsx b/apps/web/src/components/configuration-sidebar/index.tsx index bb9321b3..5057cd63 100644 --- a/apps/web/src/components/configuration-sidebar/index.tsx +++ b/apps/web/src/components/configuration-sidebar/index.tsx @@ -8,10 +8,7 @@ import { useConfigStore } from "@/hooks/use-config-store"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { Skeleton } from "@/components/ui/skeleton"; -import type { - ConfigurableFieldUIMetadata, - ConfigurableFieldUIType, -} from "@open-swe/shared/configurable-metadata"; +import type { ConfigurableFieldUIMetadata } from "@open-swe/shared/configurable-metadata"; import { Button } from "@/components/ui/button"; import { PanelRightOpen } from "lucide-react"; import { GraphConfigurationMetadata } from "@open-swe/shared/open-swe/types"; diff --git a/apps/web/src/components/tasks/index.tsx b/apps/web/src/components/tasks/index.tsx new file mode 100644 index 00000000..eac1f7dc --- /dev/null +++ b/apps/web/src/components/tasks/index.tsx @@ -0,0 +1,625 @@ +"use client"; +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + CheckCircle2, + Circle, + Play, + ChevronDown, + ChevronRight, + ChevronLeft, + Edit2, + Save, + Plus, + Trash2, + Clock, + Filter, + PanelLeftClose, + User, + Bot, + RotateCcw, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ProgressBar } from "./progress-bar"; +import { PlanItem, TaskPlan } from "@open-swe/shared/open-swe/types"; + +interface TasksSidebarProps { + isOpen: boolean; + onClose: () => void; + taskPlan: TaskPlan; + className?: string; + onTaskChange?: (taskId: string) => void; + onRevisionChange?: (taskId: string, revisionIndex: number) => void; + onEditPlanItem?: ( + taskId: string, + planItemIndex: number, + newPlan: string, + ) => void; + onAddPlanItem?: (taskId: string, plan: string) => void; + onDeletePlanItem?: (taskId: string, planItemIndex: number) => void; +} + +interface TaskPlanViewProps { + taskPlan: TaskPlan; + onTaskChange?: (taskId: string) => void; + onRevisionChange?: (taskId: string, revisionIndex: number) => void; + onEditPlanItem?: ( + taskId: string, + planItemIndex: number, + newPlan: string, + ) => void; + onAddPlanItem?: (taskId: string, plan: string) => void; + onDeletePlanItem?: (taskId: string, planItemIndex: number) => void; +} + +type FilterType = "all" | "completed" | "current" | "pending"; + +// Tasks Sidebar Component +export function TasksSidebar({ + isOpen, + onClose, + taskPlan, + onTaskChange, + onRevisionChange, + onEditPlanItem, + onAddPlanItem, + onDeletePlanItem, +}: TasksSidebarProps) { + const [currentTaskIndex, setCurrentTaskIndex] = useState( + taskPlan.activeTaskIndex, + ); + const [currentRevisionIndex, setCurrentRevisionIndex] = useState(0); + const [expandedSummaries, setExpandedSummaries] = useState>( + new Set(), + ); + const [editingPlanItem, setEditingPlanItem] = useState(null); + const [editingText, setEditingText] = useState(""); + const [newPlanItemText, setNewPlanItemText] = useState(""); + const [showAddPlanItem, setShowAddPlanItem] = useState(false); + const [filter, setFilter] = useState("all"); + + const currentTask = taskPlan.tasks[currentTaskIndex]; + const isLatestTask = currentTaskIndex === taskPlan.activeTaskIndex; + const isLatestRevision = + currentRevisionIndex === currentTask?.activeRevisionIndex; + + useEffect(() => { + if (currentTask?.planRevisions) { + setCurrentRevisionIndex(currentTask.activeRevisionIndex); + setExpandedSummaries(new Set()); + setEditingPlanItem(null); + } + }, [currentTask]); + + if (!currentTask) return null; + + const currentRevision = currentTask.planRevisions[currentRevisionIndex]; + const planItems = currentRevision?.plans || []; + const sortedItems = [...planItems].sort((a, b) => a.index - b.index); + + const currentPlanItemIndex = sortedItems + .filter((item) => !item.completed) + .reduce( + (min, item) => (item.index < min ? item.index : min), + Number.POSITIVE_INFINITY, + ); + + const filteredItems = sortedItems.filter((item) => { + if (filter === "all") return true; + if (filter === "completed") return item.completed; + if (filter === "current") return item.index === currentPlanItemIndex; + if (filter === "pending") + return !item.completed && item.index !== currentPlanItemIndex; + return true; + }); + + const isPlanItemEditable = (item: PlanItem) => { + return ( + isLatestTask && + isLatestRevision && + !item.completed && + item.index !== currentPlanItemIndex + ); + }; + + const getItemState = ( + item: PlanItem, + ): "completed" | "current" | "remaining" => { + if (item.completed) return "completed"; + if (item.index === currentPlanItemIndex) return "current"; + return "remaining"; + }; + + const getStateIcon = (state: string) => { + switch (state) { + case "completed": + return ; + case "current": + return ; + default: + return ; + } + }; + + const startEditing = (item: PlanItem) => { + setEditingPlanItem(item.index); + setEditingText(item.plan); + }; + + const saveEdit = () => { + if (editingPlanItem !== null && editingText.trim()) { + onEditPlanItem?.(currentTask.id, editingPlanItem, editingText.trim()); + setEditingPlanItem(null); + setEditingText(""); + } + }; + + const addNewPlanItem = () => { + if (newPlanItemText.trim()) { + onAddPlanItem?.(currentTask.id, newPlanItemText.trim()); + setNewPlanItemText(""); + setShowAddPlanItem(false); + } + }; + + const goToPreviousRevision = () => { + if (currentRevisionIndex > 0) { + const newIndex = currentRevisionIndex - 1; + setCurrentRevisionIndex(newIndex); + onRevisionChange?.(currentTask.id, newIndex); + setExpandedSummaries(new Set()); + setEditingPlanItem(null); + } + }; + + const goToNextRevision = () => { + if (currentRevisionIndex < currentTask.planRevisions.length - 1) { + const newIndex = currentRevisionIndex + 1; + setCurrentRevisionIndex(newIndex); + onRevisionChange?.(currentTask.id, newIndex); + setExpandedSummaries(new Set()); + setEditingPlanItem(null); + } + }; + + const goToLatestRevision = () => { + const latestIndex = currentTask.activeRevisionIndex; + setCurrentRevisionIndex(latestIndex); + onRevisionChange?.(currentTask.id, latestIndex); + setExpandedSummaries(new Set()); + setEditingPlanItem(null); + }; + + const formatDate = (timestamp: number) => { + return new Date(timestamp).toLocaleString(); + }; + + return ( +
+
+ {/* Header */} +
+
+

Tasks

+ +
+ + {/* Task selector */} + {taskPlan.tasks.length > 1 && ( +
+ + +
+ )} + + {/* Plan revision navigation */} +
+ +
+
+
+ {currentRevision?.createdBy === "agent" ? ( + + ) : ( + + )} + +
+ + Rev {currentRevision?.revisionIndex + 1} of{" "} + {currentTask.planRevisions.length} + + {!isLatestRevision && ( + + Historical + + )} +
+ + {currentTask.planRevisions.length > 1 && ( +
+ + + + + {!isLatestRevision && ( + + )} +
+ )} +
+ + {/* Revision info */} + {currentRevision && ( +
+ Created by {currentRevision.createdBy} on{" "} + {formatDate(currentRevision.createdAt)} +
+ )} +
+ + {/* Filter controls */} +
+
+ + +
+ + {isLatestTask && isLatestRevision && ( + + Editable + + )} +
+
+ + {/* Content */} +
+
+ {filteredItems.length === 0 ? ( +
+ No plan items match the current filter +
+ ) : ( + filteredItems.map((item) => { + const state = getItemState(item); + const isExpanded = expandedSummaries.has(item.index); + const isEditing = editingPlanItem === item.index; + const editable = isPlanItemEditable(item); + + return ( +
+
+
+ {getStateIcon(state)} +
+ +
+ {isEditing ? ( +
+