diff --git a/frontend/src/components/tasks/TaskDetailsDialog.tsx b/frontend/src/components/tasks/TaskDetailsDialog.tsx index 10f8d29e..c6b35148 100644 --- a/frontend/src/components/tasks/TaskDetailsDialog.tsx +++ b/frontend/src/components/tasks/TaskDetailsDialog.tsx @@ -1,124 +1,210 @@ -import { useState, useEffect } from 'react' -import { Card, CardContent } from '@/components/ui/card' +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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { makeAuthenticatedRequest } from '@/lib/auth' -import type { TaskStatus, TaskAttempt, TaskAttemptActivity, ExecutorConfig } from 'shared/types' + 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 { makeAuthenticatedRequest } from "@/lib/auth"; +import type { + TaskStatus, + TaskAttempt, + TaskAttemptActivity, + ExecutorConfig, +} from "shared/types"; interface Task { - id: string - project_id: string - title: string - description: string | null - status: TaskStatus - created_at: string - updated_at: string + 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 + 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 + 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' -} + todo: "To Do", + inprogress: "In Progress", + inreview: "In Review", + done: "Done", + cancelled: "Cancelled", +}; -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([]) - const [activitiesLoading, setActivitiesLoading] = useState(false) - const [selectedExecutor, setSelectedExecutor] = useState({ type: "echo" }) - const [creatingAttempt, setCreatingAttempt] = useState(false) +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({ + type: "echo", + }); + const [creatingAttempt, setCreatingAttempt] = 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); useEffect(() => { if (isOpen && task) { - fetchTaskAttempts(task.id) + fetchTaskAttempts(task.id); + // Initialize edit state with current task values + setEditedTitle(task.title); + setEditedDescription(task.description || ""); + setEditedStatus(task.status); + setIsEditMode(false); } - }, [isOpen, task]) + }, [isOpen, task]); const fetchTaskAttempts = async (taskId: string) => { try { - setTaskAttemptsLoading(true) - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}/attempts`) - + setTaskAttemptsLoading(true); + const response = await makeAuthenticatedRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts` + ); + if (response.ok) { - const result: ApiResponse = await response.json() + const result: ApiResponse = await response.json(); if (result.success && result.data) { - setTaskAttempts(result.data) + setTaskAttempts(result.data); } } else { - onError('Failed to load task attempts') + onError("Failed to load task attempts"); } } catch (err) { - onError('Failed to load task attempts') + onError("Failed to load task attempts"); } finally { - setTaskAttemptsLoading(false) + setTaskAttemptsLoading(false); } - } + }; const fetchAttemptActivities = async (attemptId: string) => { - if (!task) return - + if (!task) return; + try { - setActivitiesLoading(true) - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities`) - + setActivitiesLoading(true); + const response = await makeAuthenticatedRequest( + `/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities` + ); + if (response.ok) { - const result: ApiResponse = await response.json() + const result: ApiResponse = + await response.json(); if (result.success && result.data) { - setAttemptActivities(result.data) + setAttemptActivities(result.data); } } else { - onError('Failed to load attempt activities') + onError("Failed to load attempt activities"); } } catch (err) { - onError('Failed to load attempt activities') + onError("Failed to load attempt activities"); } finally { - setActivitiesLoading(false) + setActivitiesLoading(false); } - } + }; const handleAttemptClick = (attempt: TaskAttempt) => { - setSelectedAttempt(attempt) - fetchAttemptActivities(attempt.id) - } + setSelectedAttempt(attempt); + fetchAttemptActivities(attempt.id); + }; + + const saveTaskChanges = async () => { + if (!task) return; + + try { + setSavingTask(true); + const response = await makeAuthenticatedRequest( + `/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 - + if (!task) return; + try { - setCreatingAttempt(true) - const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}` - + setCreatingAttempt(true); + const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}`; + const response = await makeAuthenticatedRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ task_id: task.id, @@ -128,175 +214,389 @@ export function TaskDetailsDialog({ isOpen, onOpenChange, task, projectId, onErr executor_config: selectedExecutor, }), } - ) - + ); + if (response.ok) { // Refresh the attempts list - await fetchTaskAttempts(task.id) + await fetchTaskAttempts(task.id); } else { - onError('Failed to create task attempt') + onError("Failed to create task attempt"); } } catch (err) { - onError('Failed to create task attempt') + onError("Failed to create task attempt"); } finally { - setCreatingAttempt(false) + setCreatingAttempt(false); } - } + }; return ( - - + + - Task Details: {task?.title} - -
- {/* Task Info */} -
-

Task Information

-
-
- -

{task?.title}

-
-
- -

- {task ? statusLabels[task.status] : ''} -

-
-
- {task?.description && ( -
- -

{task.description}

-
- )} -
- - {/* Task Attempts */} -
-
-

Task Attempts

-
-
- - -
- + + ) : ( + -
-
- {taskAttemptsLoading ? ( -
Loading attempts...
- ) : taskAttempts.length === 0 ? ( -
- No attempts found for this task -
- ) : ( -
- {taskAttempts.map((attempt) => ( - handleAttemptClick(attempt)} - > - -
-
-
-

Worktree: {attempt.worktree_path}

-

- Created: {new Date(attempt.created_at).toLocaleDateString()} -

-
-
-
-
- -

- {attempt.base_commit || 'None'} -

-
-
- -

- {attempt.merge_commit || 'None'} -

-
-
-
-
-
- ))} -
- )} -
- - {/* Activity History */} - {selectedAttempt && ( -
-

- Activity History for Attempt: {selectedAttempt.worktree_path} -

- {activitiesLoading ? ( -
Loading activities...
- ) : attemptActivities.length === 0 ? ( -
- No activities found for this attempt -
- ) : ( -
- {attemptActivities.map((activity) => ( - - -
-
-
- - {activity.status === 'init' ? 'Init' : - activity.status === 'inprogress' ? 'In Progress' : - 'Paused'} - -
- {activity.note && ( -

{activity.note}

- )} -
-

- {new Date(activity.created_at).toLocaleString()} -

-
-
-
- ))} -
)}
- )} +
+ + +
+ {/* Main Content */} +
+ {/* Task Details */} + + +
+
+ + {isEditMode ? ( + setEditedTitle(e.target.value)} + className="mt-1" + placeholder="Enter task title..." + /> + ) : ( +

+ {task?.title} +

+ )} +
+ +
+ + {isEditMode ? ( +