import { useState, useEffect } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { makeRequest } from "@/lib/api"; import type { TaskStatus, TaskAttempt, TaskAttemptActivity, TaskAttemptStatus, } from "shared/types"; interface Task { id: string; project_id: string; title: string; description: string | null; status: TaskStatus; created_at: string; updated_at: string; } interface ApiResponse { success: boolean; data: T | null; message: string | null; } interface TaskDetailsDialogProps { isOpen: boolean; onOpenChange: (open: boolean) => void; task: Task | null; projectId: string; onError: (error: string) => void; } const statusLabels: Record = { todo: "To Do", inprogress: "In Progress", inreview: "In Review", done: "Done", cancelled: "Cancelled", }; const getAttemptStatusDisplay = (status: TaskAttemptStatus): { label: string; className: string } => { switch (status) { case "setuprunning": return { label: "Setup Running", className: "bg-status-running text-status-running-foreground" }; case "setupcomplete": return { label: "Setup Complete", className: "bg-status-complete text-status-complete-foreground" }; case "setupfailed": return { label: "Setup Failed", className: "bg-status-failed text-status-failed-foreground" }; case "executorrunning": return { label: "Executor Running", className: "bg-status-running text-status-running-foreground" }; case "executorcomplete": return { label: "Executor Complete", className: "bg-status-complete text-status-complete-foreground" }; case "executorfailed": return { label: "Executor Failed", className: "bg-status-failed text-status-failed-foreground" }; default: return { label: "Unknown", className: "bg-status-init text-status-init-foreground" }; } }; export function TaskDetailsDialog({ isOpen, onOpenChange, task, projectId, onError, }: TaskDetailsDialogProps) { const [taskAttempts, setTaskAttempts] = useState([]); const [taskAttemptsLoading, setTaskAttemptsLoading] = useState(false); const [selectedAttempt, setSelectedAttempt] = useState( null ); const [attemptActivities, setAttemptActivities] = useState< TaskAttemptActivity[] >([]); const [activitiesLoading, setActivitiesLoading] = useState(false); const [selectedExecutor, setSelectedExecutor] = useState("claude"); const [creatingAttempt, setCreatingAttempt] = useState(false); const [stoppingAttempt, setStoppingAttempt] = useState(false); // Edit mode state const [isEditMode, setIsEditMode] = useState(false); const [editedTitle, setEditedTitle] = useState(""); const [editedDescription, setEditedDescription] = useState(""); const [editedStatus, setEditedStatus] = useState("todo"); const [savingTask, setSavingTask] = useState(false); // Check if the selected attempt is active (not in a final state) const isAttemptRunning = selectedAttempt && attemptActivities.length > 0 && (attemptActivities[0].status === "setuprunning" || attemptActivities[0].status === "setupcomplete" || attemptActivities[0].status === "executorrunning"); useEffect(() => { if (isOpen && task) { // Reset attempt-related state when switching tasks setSelectedAttempt(null); setAttemptActivities([]); setActivitiesLoading(false); fetchTaskAttempts(task.id); // Initialize edit state with current task values setEditedTitle(task.title); setEditedDescription(task.description || ""); setEditedStatus(task.status); setIsEditMode(false); } }, [isOpen, task]); const fetchTaskAttempts = async (taskId: string) => { try { setTaskAttemptsLoading(true); const response = await makeRequest( `/api/projects/${projectId}/tasks/${taskId}/attempts` ); if (response.ok) { const result: ApiResponse = await response.json(); if (result.success && result.data) { setTaskAttempts(result.data); // Automatically select the latest attempt if available if (result.data.length > 0) { const latestAttempt = result.data.reduce((latest, current) => new Date(current.created_at) > new Date(latest.created_at) ? current : latest ); setSelectedAttempt(latestAttempt); fetchAttemptActivities(latestAttempt.id); } } } else { onError("Failed to load task attempts"); } } catch (err) { onError("Failed to load task attempts"); } finally { setTaskAttemptsLoading(false); } }; const fetchAttemptActivities = async (attemptId: string) => { if (!task) return; try { setActivitiesLoading(true); const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities` ); if (response.ok) { const result: ApiResponse = await response.json(); if (result.success && result.data) { setAttemptActivities(result.data); } } else { onError("Failed to load attempt activities"); } } catch (err) { onError("Failed to load attempt activities"); } finally { setActivitiesLoading(false); } }; const handleAttemptClick = (attempt: TaskAttempt) => { setSelectedAttempt(attempt); fetchAttemptActivities(attempt.id); }; const saveTaskChanges = async () => { if (!task) return; try { setSavingTask(true); const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ title: editedTitle, description: editedDescription || null, status: editedStatus, }), } ); if (response.ok) { setIsEditMode(false); // Update the local task state would require parent component to refresh // For now, just exit edit mode } else { onError("Failed to save task changes"); } } catch (err) { onError("Failed to save task changes"); } finally { setSavingTask(false); } }; const cancelEdit = () => { if (task) { setEditedTitle(task.title); setEditedDescription(task.description || ""); setEditedStatus(task.status); } setIsEditMode(false); }; const createNewAttempt = async () => { if (!task) return; try { setCreatingAttempt(true); const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}`; const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ task_id: task.id, worktree_path: worktreePath, base_commit: null, merge_commit: null, executor: selectedExecutor, }), } ); if (response.ok) { // Refresh the attempts list await fetchTaskAttempts(task.id); } else { onError("Failed to create task attempt"); } } catch (err) { onError("Failed to create task attempt"); } finally { setCreatingAttempt(false); } }; const stopTaskAttempt = async () => { if (!task || !selectedAttempt) return; try { setStoppingAttempt(true); const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`, { method: "POST", headers: { "Content-Type": "application/json", }, } ); if (response.ok) { // Refresh the activities list to show the stopped status fetchAttemptActivities(selectedAttempt.id); } else { onError("Failed to stop task attempt"); } } catch (err) { onError("Failed to stop task attempt"); } finally { setStoppingAttempt(false); } }; return (
{isEditMode ? "Edit Task" : "Task Details"}
{isEditMode ? ( <> ) : ( )}
{/* Main Content */}
{/* Task Details */}
{isEditMode ? ( setEditedTitle(e.target.value)} className="mt-1" placeholder="Enter task title..." /> ) : (

{task?.title}

)}
{isEditMode ? (