mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 20:42:11 +00:00
feat: Implement tasks component (#93)
* feat: Implement tasks component * hookup * improve task UI rendering + responsiveness * progress bar small sizes responsiveness' && gp * transition new TaskSidebar, fix add new Task logic * cr --------- Co-authored-by: starmorph <dylan@starmorph.com>
This commit is contained in:
parent
1736c18a19
commit
b3bcc46c59
6 changed files with 1058 additions and 7 deletions
|
|
@ -8,10 +8,7 @@ import { useConfigStore } from "@/hooks/use-config-store";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type {
|
import type { ConfigurableFieldUIMetadata } from "@open-swe/shared/configurable-metadata";
|
||||||
ConfigurableFieldUIMetadata,
|
|
||||||
ConfigurableFieldUIType,
|
|
||||||
} from "@open-swe/shared/configurable-metadata";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PanelRightOpen } from "lucide-react";
|
import { PanelRightOpen } from "lucide-react";
|
||||||
import { GraphConfigurationMetadata } from "@open-swe/shared/open-swe/types";
|
import { GraphConfigurationMetadata } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
|
||||||
625
apps/web/src/components/tasks/index.tsx
Normal file
625
apps/web/src/components/tasks/index.tsx
Normal file
|
|
@ -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<Set<number>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [editingPlanItem, setEditingPlanItem] = useState<number | null>(null);
|
||||||
|
const [editingText, setEditingText] = useState("");
|
||||||
|
const [newPlanItemText, setNewPlanItemText] = useState("");
|
||||||
|
const [showAddPlanItem, setShowAddPlanItem] = useState(false);
|
||||||
|
const [filter, setFilter] = useState<FilterType>("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 <CheckCircle2 className="h-4 w-4 text-green-600" />;
|
||||||
|
case "current":
|
||||||
|
return <Play className="h-4 w-4 text-blue-500" />;
|
||||||
|
default:
|
||||||
|
return <Circle className="h-4 w-4 text-gray-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 right-0 z-10 h-screen border-l border-gray-200 bg-white shadow-lg transition-all duration-300",
|
||||||
|
isOpen ? "w-80 md:w-xl" : "w-0 overflow-hidden border-l-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full flex-col transition-opacity duration-300",
|
||||||
|
isOpen ? "opacity-100 delay-150" : "opacity-0",
|
||||||
|
)}
|
||||||
|
style={{ minWidth: isOpen ? "320px" : "0" }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="border-b border-gray-200 p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">Tasks</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<PanelLeftClose className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task selector */}
|
||||||
|
{taskPlan.tasks.length > 1 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-700">
|
||||||
|
Task
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="w-full rounded border border-gray-200 px-2 py-1 text-sm"
|
||||||
|
value={currentTask.id}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newTaskIndex = taskPlan.tasks.findIndex(
|
||||||
|
(t) => t.id === e.target.value,
|
||||||
|
);
|
||||||
|
if (newTaskIndex !== -1) {
|
||||||
|
setCurrentTaskIndex(newTaskIndex);
|
||||||
|
onTaskChange?.(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{taskPlan.tasks.map((task, index) => (
|
||||||
|
<option
|
||||||
|
key={task.id}
|
||||||
|
value={task.id}
|
||||||
|
>
|
||||||
|
Task #{task.taskIndex + 1}: {task.request.substring(0, 50)}
|
||||||
|
{index === taskPlan.activeTaskIndex && " (Active)"}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Plan revision navigation */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-700">
|
||||||
|
Plan Revision
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex flex-1 items-center gap-1 rounded border border-gray-200 bg-gray-50 px-2 py-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{currentRevision?.createdBy === "agent" ? (
|
||||||
|
<Bot className="h-3 w-3 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<User className="h-3 w-3 text-green-500" />
|
||||||
|
)}
|
||||||
|
<Clock className="h-3 w-3 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs">
|
||||||
|
Rev {currentRevision?.revisionIndex + 1} of{" "}
|
||||||
|
{currentTask.planRevisions.length}
|
||||||
|
</span>
|
||||||
|
{!isLatestRevision && (
|
||||||
|
<span className="ml-1 rounded bg-orange-50 px-1 py-0.5 text-xs text-orange-600">
|
||||||
|
Historical
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentTask.planRevisions.length > 1 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToPreviousRevision}
|
||||||
|
disabled={currentRevisionIndex === 0}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
title="Previous revision"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToNextRevision}
|
||||||
|
disabled={
|
||||||
|
currentRevisionIndex ===
|
||||||
|
currentTask.planRevisions.length - 1
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
title="Next revision"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!isLatestRevision && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToLatestRevision}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
title="Latest revision"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Revision info */}
|
||||||
|
{currentRevision && (
|
||||||
|
<div className="mt-1 text-xs text-gray-500">
|
||||||
|
Created by {currentRevision.createdBy} on{" "}
|
||||||
|
{formatDate(currentRevision.createdAt)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter controls */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1 rounded border border-gray-200 bg-gray-50 px-2 py-1">
|
||||||
|
<Filter className="h-3 w-3 text-gray-500" />
|
||||||
|
<select
|
||||||
|
className="border-none bg-transparent text-xs outline-none"
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value as FilterType)}
|
||||||
|
>
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="current">Current</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLatestTask && isLatestRevision && (
|
||||||
|
<span className="rounded bg-green-50 px-2 py-1 text-xs text-green-600">
|
||||||
|
Editable
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filteredItems.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-sm text-gray-500">
|
||||||
|
No plan items match the current filter
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredItems.map((item) => {
|
||||||
|
const state = getItemState(item);
|
||||||
|
const isExpanded = expandedSummaries.has(item.index);
|
||||||
|
const isEditing = editingPlanItem === item.index;
|
||||||
|
const editable = isPlanItemEditable(item);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.index}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border p-3",
|
||||||
|
state === "current" && "border-blue-200 bg-blue-50",
|
||||||
|
state === "completed" && "border-green-200 bg-green-50",
|
||||||
|
state === "remaining" && "border-gray-200 bg-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="mt-0.5 flex-shrink-0">
|
||||||
|
{getStateIcon(state)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Textarea
|
||||||
|
value={editingText}
|
||||||
|
onChange={(e) => setEditingText(e.target.value)}
|
||||||
|
className="min-h-[60px] text-sm"
|
||||||
|
placeholder="Enter plan item description..."
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={saveEdit}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
>
|
||||||
|
<Save className="mr-1 h-3 w-3" />
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingPlanItem(null);
|
||||||
|
setEditingText("");
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="mb-1 flex items-start justify-between gap-2">
|
||||||
|
<p className="text-sm leading-relaxed text-gray-900">
|
||||||
|
{item.plan}
|
||||||
|
</p>
|
||||||
|
{editable && (
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => startEditing(item)}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<Edit2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
onDeletePlanItem?.(
|
||||||
|
currentTask.id,
|
||||||
|
item.index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0 text-red-500"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
Plan Item #{item.index + 1}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2 py-1 text-xs",
|
||||||
|
state === "completed" &&
|
||||||
|
"bg-green-100 text-green-700",
|
||||||
|
state === "current" &&
|
||||||
|
"bg-blue-100 text-blue-700",
|
||||||
|
state === "remaining" &&
|
||||||
|
"bg-gray-100 text-gray-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{state === "completed"
|
||||||
|
? "Completed"
|
||||||
|
: state === "current"
|
||||||
|
? "In Progress"
|
||||||
|
: "Pending"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.completed && item.summary && (
|
||||||
|
<Collapsible
|
||||||
|
open={isExpanded}
|
||||||
|
onOpenChange={() => {
|
||||||
|
const newExpanded = new Set(
|
||||||
|
expandedSummaries,
|
||||||
|
);
|
||||||
|
if (newExpanded.has(item.index)) {
|
||||||
|
newExpanded.delete(item.index);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(item.index);
|
||||||
|
}
|
||||||
|
setExpandedSummaries(newExpanded);
|
||||||
|
}}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 p-0 text-xs text-green-700"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="mr-1 h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="mr-1 h-3 w-3" />
|
||||||
|
)}
|
||||||
|
View summary
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="mt-2">
|
||||||
|
<div className="rounded border border-green-200 bg-green-50 p-2 text-xs text-green-800">
|
||||||
|
{item.summary}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add New Plan Item - Only for latest task and revision */}
|
||||||
|
{isLatestTask &&
|
||||||
|
isLatestRevision &&
|
||||||
|
!planItems.every((item) => item.completed) && (
|
||||||
|
<div>
|
||||||
|
{showAddPlanItem ? (
|
||||||
|
<div className="rounded-lg border border-dashed border-gray-300 p-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Textarea
|
||||||
|
value={newPlanItemText}
|
||||||
|
onChange={(e) => setNewPlanItemText(e.target.value)}
|
||||||
|
placeholder="Enter new plan item description..."
|
||||||
|
className="min-h-[60px] text-sm"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={addNewPlanItem}
|
||||||
|
disabled={!newPlanItemText.trim()}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
Add Plan Item
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddPlanItem(false);
|
||||||
|
setNewPlanItemText("");
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-10 w-full border-dashed border-gray-300 text-sm"
|
||||||
|
onClick={() => setShowAddPlanItem(true)}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add New Plan Item
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main TaskPlan View Component
|
||||||
|
export function TaskPlanView({
|
||||||
|
taskPlan,
|
||||||
|
onTaskChange,
|
||||||
|
onRevisionChange,
|
||||||
|
onEditPlanItem,
|
||||||
|
onAddPlanItem,
|
||||||
|
onDeletePlanItem,
|
||||||
|
}: TaskPlanViewProps) {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
if (taskPlan.tasks.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded border border-gray-200 bg-gray-50 p-2">
|
||||||
|
<div className="text-center text-xs text-gray-500">
|
||||||
|
No tasks available
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Compact Progress Bar */}
|
||||||
|
<ProgressBar
|
||||||
|
taskPlan={taskPlan}
|
||||||
|
onOpenSidebar={() => setIsSidebarOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tasks Sidebar */}
|
||||||
|
<TasksSidebar
|
||||||
|
isOpen={isSidebarOpen}
|
||||||
|
onClose={() => setIsSidebarOpen(false)}
|
||||||
|
taskPlan={taskPlan}
|
||||||
|
onTaskChange={onTaskChange}
|
||||||
|
onRevisionChange={onRevisionChange}
|
||||||
|
onEditPlanItem={onEditPlanItem}
|
||||||
|
onAddPlanItem={onAddPlanItem}
|
||||||
|
onDeletePlanItem={onDeletePlanItem}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
275
apps/web/src/components/tasks/progress-bar.tsx
Normal file
275
apps/web/src/components/tasks/progress-bar.tsx
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
"use client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { CheckCircle2, Play, List, HelpCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { PlanItem, TaskPlan } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
|
interface ProgressBarProps {
|
||||||
|
taskPlan?: TaskPlan;
|
||||||
|
className?: string;
|
||||||
|
onOpenSidebar?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProgressBar({
|
||||||
|
taskPlan,
|
||||||
|
className,
|
||||||
|
onOpenSidebar,
|
||||||
|
}: ProgressBarProps) {
|
||||||
|
const [showLegend, setShowLegend] = useState(false);
|
||||||
|
|
||||||
|
if (!taskPlan || !taskPlan.tasks.length) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-2 w-full rounded-md border border-gray-200 bg-gray-50 px-3 py-2 sm:mt-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-gray-500">No active plan</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTask = taskPlan.tasks[taskPlan.activeTaskIndex];
|
||||||
|
if (!currentTask || !currentTask.planRevisions.length) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-2 w-full rounded-md border border-gray-200 bg-gray-50 px-3 py-2 sm:mt-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-gray-500">No active plan</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeRevision =
|
||||||
|
currentTask.planRevisions[currentTask.activeRevisionIndex];
|
||||||
|
const planItems = [...activeRevision.plans].sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
// Find the current task (lowest index among uncompleted tasks)
|
||||||
|
const currentTaskIndex = planItems
|
||||||
|
.filter((item) => !item.completed)
|
||||||
|
.reduce(
|
||||||
|
(min, item) => (item.index < min ? item.index : min),
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getItemState = (
|
||||||
|
item: PlanItem,
|
||||||
|
): "completed" | "current" | "remaining" => {
|
||||||
|
if (item.completed) return "completed";
|
||||||
|
if (item.index === currentTaskIndex) return "current";
|
||||||
|
return "remaining";
|
||||||
|
};
|
||||||
|
|
||||||
|
const completedCount = planItems.filter((item) => item.completed).length;
|
||||||
|
const progressPercentage =
|
||||||
|
planItems.length > 0 ? (completedCount / planItems.length) * 100 : 0;
|
||||||
|
|
||||||
|
const getSegmentColor = (state: string) => {
|
||||||
|
switch (state) {
|
||||||
|
case "completed":
|
||||||
|
return "bg-green-400";
|
||||||
|
case "current":
|
||||||
|
return "bg-blue-400";
|
||||||
|
default:
|
||||||
|
return "bg-gray-200";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentPlanItem = planItems.find(
|
||||||
|
(item) => item.index === currentTaskIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-md border border-gray-200 bg-white shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Compact header */}
|
||||||
|
<div className="overflow-hidden px-2 py-2 sm:px-3">
|
||||||
|
<div className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-gray-700 sm:text-sm">
|
||||||
|
Plan Progress
|
||||||
|
</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-4 w-4 p-0 sm:h-5 sm:w-5"
|
||||||
|
onClick={() => setShowLegend(!showLegend)}
|
||||||
|
>
|
||||||
|
<HelpCircle className="h-3 w-3 text-gray-500" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="top"
|
||||||
|
className="max-w-xs p-2 text-xs"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium">Plan Progress</p>
|
||||||
|
<p>
|
||||||
|
Shows the current progress of the AI agent's plan
|
||||||
|
execution.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500 sm:text-sm">
|
||||||
|
{Math.round(progressPercentage)}%
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onOpenSidebar}
|
||||||
|
className="h-7 border-blue-200 px-2 text-xs hover:bg-blue-50 sm:h-8 sm:px-3 sm:text-sm"
|
||||||
|
>
|
||||||
|
<List className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
<span className="hidden sm:inline">Tasks</span>
|
||||||
|
<span className="sm:hidden">View</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend - conditionally shown */}
|
||||||
|
{showLegend && (
|
||||||
|
<div className="mb-2 flex flex-wrap items-center gap-2 rounded border border-gray-200 bg-gray-50 px-2 py-1 text-xs sm:gap-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-400"></div>
|
||||||
|
<span>Completed</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-2 animate-pulse rounded-full bg-blue-400"></div>
|
||||||
|
<span>Current</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-gray-200"></div>
|
||||||
|
<span>Pending</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="ml-auto h-4 touch-manipulation p-0 text-xs"
|
||||||
|
onClick={() => setShowLegend(false)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Progress Stats */}
|
||||||
|
<div className="mb-2 flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<span className="text-xs text-gray-600 sm:text-sm">
|
||||||
|
{completedCount} of {planItems.length} tasks completed
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 sm:text-sm">
|
||||||
|
Task #{currentTask.taskIndex + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div
|
||||||
|
className="flex h-3 cursor-pointer touch-manipulation gap-[1px] overflow-hidden rounded-sm bg-gray-100 transition-all hover:h-3.5 sm:h-2 sm:hover:h-2.5"
|
||||||
|
onClick={onOpenSidebar}
|
||||||
|
aria-label="Click to view all tasks"
|
||||||
|
title="Click to view all tasks"
|
||||||
|
>
|
||||||
|
{planItems.map((item) => {
|
||||||
|
const state = getItemState(item);
|
||||||
|
const segmentWidth = `${100 / planItems.length}%`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider key={item.index}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"transition-all hover:opacity-80",
|
||||||
|
getSegmentColor(state),
|
||||||
|
state === "current" && "animate-pulse",
|
||||||
|
)}
|
||||||
|
style={{ width: segmentWidth }}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="bottom"
|
||||||
|
className="max-w-xs p-2 text-xs sm:max-w-sm"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex flex-col gap-0.5 sm:flex-row sm:items-center sm:gap-1">
|
||||||
|
<span className="font-medium">
|
||||||
|
Task #{item.index}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{state === "completed"
|
||||||
|
? "Completed"
|
||||||
|
: state === "current"
|
||||||
|
? "Current"
|
||||||
|
: "Pending"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs break-words">{item.plan}</p>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Task Info */}
|
||||||
|
{currentPlanItem && (
|
||||||
|
<div className="flex items-start gap-2 rounded border border-blue-100 bg-blue-50 px-2 py-1.5 sm:px-3">
|
||||||
|
<Play className="mt-0.5 h-3 w-3 flex-shrink-0 text-blue-500 sm:h-4 sm:w-4" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-xs font-medium text-blue-700 sm:text-sm">
|
||||||
|
Currently working on:
|
||||||
|
</div>
|
||||||
|
<p className="line-clamp-2 text-xs break-words text-gray-700 sm:line-clamp-1 sm:truncate sm:text-sm">
|
||||||
|
{currentPlanItem.plan}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* All tasks completed */}
|
||||||
|
{completedCount === planItems.length && planItems.length > 0 && (
|
||||||
|
<div className="flex items-start gap-2 rounded border border-green-100 bg-green-50 px-2 py-1.5 sm:px-3">
|
||||||
|
<CheckCircle2 className="mt-0.5 h-3 w-3 flex-shrink-0 text-green-600 sm:h-4 sm:w-4" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-green-700 sm:text-sm">
|
||||||
|
All tasks completed
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-700 sm:text-sm">
|
||||||
|
The agent has finished all planned tasks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
apps/web/src/components/tasks/use-task-plan.tsx
Normal file
130
apps/web/src/components/tasks/use-task-plan.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { useStreamContext } from "@/providers/Stream";
|
||||||
|
import { TaskPlan } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useTaskPlan() {
|
||||||
|
const { values } = useStreamContext();
|
||||||
|
const [taskPlan, setTaskPlan] = useState<TaskPlan>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPlanStr = JSON.stringify(taskPlan, null, 2);
|
||||||
|
const newPlanStr = JSON.stringify(values?.plan, null, 2);
|
||||||
|
if (currentPlanStr !== newPlanStr) {
|
||||||
|
setTaskPlan(values?.plan);
|
||||||
|
}
|
||||||
|
}, [values?.plan]);
|
||||||
|
|
||||||
|
const handleTaskChange = (taskId: string) => {
|
||||||
|
console.log(`Switched to task: ${taskId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevisionChange = (taskId: string, revisionIndex: number) => {
|
||||||
|
console.log(`Task ${taskId} switched to revision ${revisionIndex}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditPlanItem = (
|
||||||
|
taskId: string,
|
||||||
|
planItemIndex: number,
|
||||||
|
newPlan: string,
|
||||||
|
) => {
|
||||||
|
setTaskPlan((prevTaskPlan) => {
|
||||||
|
const prevTaskPlan_ = prevTaskPlan ?? {
|
||||||
|
tasks: [],
|
||||||
|
activeTaskIndex: 0,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...prevTaskPlan_,
|
||||||
|
tasks: prevTaskPlan_.tasks.map((task) => {
|
||||||
|
if (task.id === taskId) {
|
||||||
|
const updatedRevisions = [...task.planRevisions];
|
||||||
|
const activeRevision = updatedRevisions[task.activeRevisionIndex];
|
||||||
|
updatedRevisions[task.activeRevisionIndex] = {
|
||||||
|
...activeRevision,
|
||||||
|
plans: activeRevision.plans.map((item) =>
|
||||||
|
item.index === planItemIndex
|
||||||
|
? { ...item, plan: newPlan }
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return { ...task, planRevisions: updatedRevisions };
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`Edited plan item ${planItemIndex} in task ${taskId}: ${newPlan}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPlanItem = (taskId: string, plan: string) => {
|
||||||
|
setTaskPlan((prevTaskPlan) => {
|
||||||
|
const prevTaskPlan_ = prevTaskPlan ?? {
|
||||||
|
tasks: [],
|
||||||
|
activeTaskIndex: 0,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...prevTaskPlan_,
|
||||||
|
tasks: prevTaskPlan_.tasks.map((task) => {
|
||||||
|
if (task.id === taskId) {
|
||||||
|
const updatedRevisions = [...task.planRevisions];
|
||||||
|
const activeRevision = updatedRevisions[task.activeRevisionIndex];
|
||||||
|
const maxIndex = Math.max(
|
||||||
|
...activeRevision.plans.map((item) => item.index),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const newPlanItem = {
|
||||||
|
index: maxIndex + 1,
|
||||||
|
plan,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
updatedRevisions[task.activeRevisionIndex] = {
|
||||||
|
...activeRevision,
|
||||||
|
plans: [...activeRevision.plans, newPlanItem],
|
||||||
|
};
|
||||||
|
return { ...task, planRevisions: updatedRevisions };
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(`Added new plan item to task ${taskId}: ${plan}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePlanItem = (taskId: string, planItemIndex: number) => {
|
||||||
|
setTaskPlan((prevTaskPlan) => {
|
||||||
|
const prevTaskPlan_ = prevTaskPlan ?? {
|
||||||
|
tasks: [],
|
||||||
|
activeTaskIndex: 0,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...prevTaskPlan_,
|
||||||
|
tasks: prevTaskPlan_.tasks.map((task) => {
|
||||||
|
if (task.id === taskId) {
|
||||||
|
const updatedRevisions = [...task.planRevisions];
|
||||||
|
const activeRevision = updatedRevisions[task.activeRevisionIndex];
|
||||||
|
updatedRevisions[task.activeRevisionIndex] = {
|
||||||
|
...activeRevision,
|
||||||
|
plans: activeRevision.plans.filter(
|
||||||
|
(item) => item.index !== planItemIndex,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return { ...task, planRevisions: updatedRevisions };
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(`Deleted plan item ${planItemIndex} from task ${taskId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
taskPlan,
|
||||||
|
handleTaskChange,
|
||||||
|
handleRevisionChange,
|
||||||
|
handleEditPlanItem,
|
||||||
|
handleAddPlanItem,
|
||||||
|
handleDeletePlanItem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,8 @@ import {
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../ui/tooltip";
|
} from "../ui/tooltip";
|
||||||
import { BaseMessage } from "@langchain/core/messages";
|
import { BaseMessage } from "@langchain/core/messages";
|
||||||
|
import { TaskPlanView } from "../tasks";
|
||||||
|
import { useTaskPlan } from "../tasks/use-task-plan";
|
||||||
|
|
||||||
function StickyToBottomContent(props: {
|
function StickyToBottomContent(props: {
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
|
|
@ -100,6 +102,14 @@ export function Thread() {
|
||||||
const [artifactOpen, closeArtifact] = useArtifactOpen();
|
const [artifactOpen, closeArtifact] = useArtifactOpen();
|
||||||
const { selectedRepository } = useGitHubApp();
|
const { selectedRepository } = useGitHubApp();
|
||||||
const { getConfigs } = useConfigStore();
|
const { getConfigs } = useConfigStore();
|
||||||
|
const {
|
||||||
|
taskPlan,
|
||||||
|
handleTaskChange,
|
||||||
|
handleRevisionChange,
|
||||||
|
handleEditPlanItem,
|
||||||
|
handleAddPlanItem,
|
||||||
|
handleDeletePlanItem,
|
||||||
|
} = useTaskPlan();
|
||||||
|
|
||||||
const [threadId, _setThreadId] = useQueryState("threadId");
|
const [threadId, _setThreadId] = useQueryState("threadId");
|
||||||
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
||||||
|
|
@ -384,8 +394,8 @@ export function Thread() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{chatStarted && (
|
{chatStarted && (
|
||||||
<div className="relative z-10 flex items-center justify-between gap-3 p-2">
|
<div className="relative z-10 grid grid-cols-10 items-start gap-3 p-2">
|
||||||
<div className="relative flex items-center justify-start gap-2">
|
<div className="relative col-span-2 flex items-center justify-start gap-2">
|
||||||
<div className="absolute left-0 z-10">
|
<div className="absolute left-0 z-10">
|
||||||
{(!chatHistoryOpen || !isLargeScreen) && (
|
{(!chatHistoryOpen || !isLargeScreen) && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -423,7 +433,20 @@ export function Thread() {
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="col-span-6 mx-auto flex w-sm justify-center md:w-md lg:w-lg xl:w-xl">
|
||||||
|
{taskPlan && (
|
||||||
|
<TaskPlanView
|
||||||
|
taskPlan={taskPlan}
|
||||||
|
onTaskChange={handleTaskChange}
|
||||||
|
onRevisionChange={handleRevisionChange}
|
||||||
|
onEditPlanItem={handleEditPlanItem}
|
||||||
|
onAddPlanItem={handleAddPlanItem}
|
||||||
|
onDeletePlanItem={handleDeletePlanItem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-2 flex items-center justify-end gap-2 text-gray-700">
|
||||||
<GitHubOAuthButton />
|
<GitHubOAuthButton />
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
tooltip="Configuration"
|
tooltip="Configuration"
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
const [threads, setThreads] = useState<ThreadWithTasks[]>([]);
|
const [threads, setThreads] = useState<ThreadWithTasks[]>([]);
|
||||||
const [threadsLoading, setThreadsLoading] = useState(false);
|
const [threadsLoading, setThreadsLoading] = useState(false);
|
||||||
|
const [busyThreadIds, setBusyThreadIds] = useState<string[]>([]);
|
||||||
|
|
||||||
// Real-time thread updater for all properties (replaces polling)
|
// Real-time thread updater for all properties (replaces polling)
|
||||||
const updateThreadFromStream = useCallback(
|
const updateThreadFromStream = useCallback(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue