import { useState, useCallback, useMemo, useEffect } from "react"; import type { TaskWithSubtasks, TaskStatus, TaskSummary, CompletionAction, DaemonDirectory } from "../../lib/api"; import { retryCompletionAction, getDaemonDirectories, cloneWorktree } from "../../lib/api"; import { SubtaskTree, SubtaskProgressBar, calculateTreeStats } from "./SubtaskTree"; import { OverlayDiffViewer } from "./OverlayDiffViewer"; import { PRPreview } from "./PRPreview"; import { InlineSubtaskEditor } from "./InlineSubtaskEditor"; import { DirectoryInput } from "./DirectoryInput"; import { BranchTaskModal } from "./BranchTaskModal"; import { GitActionsPanel } from "./GitActionsPanel"; import { WorktreeFilesPanel } from "./WorktreeFilesPanel"; import { PatchesListPanel } from "./PatchesListPanel"; interface TaskDetailProps { task: TaskWithSubtasks; loading: boolean; onBack: () => void; onSave: (taskId: string, name: string, description: string, plan: string, targetRepoPath?: string, completionAction?: CompletionAction) => void; onDelete: (taskId: string) => void; onStart: (taskId: string) => void; onStop: (taskId: string) => void; onRestart: (taskId: string) => void; onContinue: (taskId: string) => void; onSelectSubtask: (taskId: string) => void; /** Toggle viewing a subtask's output (for running subtasks) */ onToggleSubtaskOutput?: (subtaskId: string, subtaskName: string) => void; /** Which subtask's output is currently being viewed */ viewingSubtaskId?: string | null; /** Navigate to view the contract */ onViewContract?: (contractId: string) => void; /** Branch the task to create a new task with same state */ onBranch?: (taskId: string, message: string, name?: string) => Promise; // Optional advanced features overlayDiff?: string; changedFiles?: string[]; onRequestDiff?: () => void; onCreatePR?: (title: string, body: string, draft: boolean) => Promise; onAutoMerge?: () => Promise; fetchSubtasks?: (taskId: string) => Promise; /** For supervisor tasks: all tasks in the contract (excluding the supervisor itself) */ contractTasks?: TaskSummary[]; /** Whether the contract is in local-only mode (no push/PR) */ isLocalOnly?: boolean; } function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", }); } function getStatusColor(status: TaskStatus): string { switch (status) { case "pending": return "text-[#9bc3ff]"; case "initializing": case "starting": return "text-cyan-400"; case "running": return "text-green-400"; case "paused": return "text-yellow-400"; case "blocked": return "text-orange-400"; case "done": return "text-emerald-400"; case "failed": return "text-red-400"; case "merged": return "text-purple-400"; default: return "text-[#9bc3ff]"; } } function getStatusBgColor(status: TaskStatus): string { switch (status) { case "pending": return "bg-[rgba(117,170,252,0.1)]"; case "initializing": case "starting": return "bg-cyan-400/10"; case "running": return "bg-green-400/10"; case "paused": return "bg-yellow-400/10"; case "blocked": return "bg-orange-400/10"; case "done": return "bg-emerald-400/10"; case "failed": return "bg-red-400/10"; case "merged": return "bg-purple-400/10"; default: return "bg-[rgba(117,170,252,0.1)]"; } } export function TaskDetail({ task, loading, onBack, onSave, onDelete, onStart, onStop, onRestart, onContinue, onSelectSubtask, onToggleSubtaskOutput, viewingSubtaskId, onViewContract, onBranch, overlayDiff, changedFiles, onRequestDiff, onCreatePR, onAutoMerge, fetchSubtasks, contractTasks, isLocalOnly = false, }: TaskDetailProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(task.name); const [editDescription, setEditDescription] = useState(task.description || ""); const [editPlan, setEditPlan] = useState(task.plan); const [editTargetRepoPath, setEditTargetRepoPath] = useState(task.targetRepoPath || ""); const [editCompletionAction, setEditCompletionAction] = useState( (task.completionAction as CompletionAction) || "none" ); const [showDiff, setShowDiff] = useState(false); const [showPRPreview, setShowPRPreview] = useState(false); const [useTreeView, setUseTreeView] = useState(false); // Track which subtask is expanded for inline editing const [expandedSubtaskId, setExpandedSubtaskId] = useState(null); // Track interrupt dropdown state const [showInterruptMenu, setShowInterruptMenu] = useState(false); // Track retry completion action state const [isRetryingCompletion, setIsRetryingCompletion] = useState(false); const [retryError, setRetryError] = useState(null); // Suggested directories from daemon const [suggestedDirectories, setSuggestedDirectories] = useState([]); // Track clone worktree state const [isCloning, setIsCloning] = useState(false); const [cloneError, setCloneError] = useState(null); const [cloneTargetDir, setCloneTargetDir] = useState(""); // Track branch modal state const [showBranchModal, setShowBranchModal] = useState(false); // Check if task is running const isTaskRunning = task.status === "running" || task.status === "initializing" || task.status === "starting"; // Check if task is in a terminal state (can be continued/reopened) const isTaskTerminal = task.status === "done" || task.status === "failed" || task.status === "merged"; // Check if this is a supervisor task const isSupervisor = task.isSupervisor === true; // Show continue for supervisors (always) or terminal states for other tasks const canContinue = isSupervisor || isTaskTerminal; // Show branch button when task has run at least once (not pending) const canBranch = onBranch && task.status !== "pending"; // Determine which tasks to show: for supervisors, show contractTasks; for regular tasks, show subtasks const displayTasks = useMemo(() => { if (isSupervisor && contractTasks) { return contractTasks; } return task.subtasks; }, [isSupervisor, contractTasks, task.subtasks]); // Calculate task statistics for progress bar const subtaskStats = useMemo( () => calculateTreeStats(displayTasks), [displayTasks] ); // Check if task can create PR const canCreatePR = useMemo(() => { return ( (task.status === "done" || task.status === "merged") && task.repositoryUrl && (onCreatePR || onAutoMerge) ); }, [task.status, task.repositoryUrl, onCreatePR, onAutoMerge]); // Check if task can retry completion action const canRetryCompletion = useMemo(() => { return ( (task.status === "done" || task.status === "failed" || task.status === "merged") && task.completionAction && task.completionAction !== "none" && task.targetRepoPath // Note: overlayPath may be null in server DB even if worktree exists on daemon // The daemon will scan for the worktree by task ID ); }, [task.status, task.completionAction, task.targetRepoPath]); // Handler for retrying completion action const handleRetryCompletion = useCallback(async () => { setIsRetryingCompletion(true); setRetryError(null); try { await retryCompletionAction(task.id); // Success - the result will be shown in task output } catch (e) { setRetryError(e instanceof Error ? e.message : "Failed to retry completion action"); } finally { setIsRetryingCompletion(false); } }, [task.id]); // Check if task can clone worktree const canCloneWorktree = useMemo(() => { return ( (task.status === "done" || task.status === "failed" || task.status === "merged") ); }, [task.status]); // Handler for cloning worktree const handleCloneWorktree = useCallback(async () => { if (!cloneTargetDir.trim()) { setCloneError("Please enter a target directory"); return; } setIsCloning(true); setCloneError(null); try { await cloneWorktree(task.id, cloneTargetDir); // Success - the result will be shown in task output setCloneTargetDir(""); // Clear input on success } catch (e) { setCloneError(e instanceof Error ? e.message : "Failed to clone worktree"); } finally { setIsCloning(false); } }, [task.id, cloneTargetDir]); // Fetch suggested directories when entering edit mode or when clone section is visible useEffect(() => { if (isEditing || canCloneWorktree) { getDaemonDirectories() .then((res) => setSuggestedDirectories(res.directories)) .catch(() => setSuggestedDirectories([])); } }, [isEditing, canCloneWorktree]); const handleSave = useCallback(() => { onSave( task.id, editName, editDescription, editPlan, editTargetRepoPath || undefined, editCompletionAction ); setIsEditing(false); }, [task.id, editName, editDescription, editPlan, editTargetRepoPath, editCompletionAction, onSave]); const handleCancel = useCallback(() => { setEditName(task.name); setEditDescription(task.description || ""); setEditPlan(task.plan); setEditTargetRepoPath(task.targetRepoPath || ""); setEditCompletionAction((task.completionAction as CompletionAction) || "none"); setIsEditing(false); }, [task]); // Toggle subtask expansion for inline editing const handleSubtaskToggle = useCallback((subtaskId: string) => { setExpandedSubtaskId((prev) => (prev === subtaskId ? null : subtaskId)); }, []); // Handle subtask click - toggle output view for any task status const handleSubtaskClick = useCallback( (subtask: TaskSummary) => { if (onToggleSubtaskOutput) { // Toggle viewing this subtask's output (works for any status) onToggleSubtaskOutput(subtask.id, subtask.name); } else { // Fallback to expand/collapse if output viewing not available handleSubtaskToggle(subtask.id); } }, [onToggleSubtaskOutput, handleSubtaskToggle] ); // Called when inline subtask editor saves changes const handleSubtaskUpdated = useCallback(() => { // Re-fetch the parent task to refresh subtask list // This will trigger from the parent component when task updates }, []); if (loading) { return (
Loading task...
); } return (
{/* Header */}
TASK//
{isEditing ? ( <> ) : ( <> {(task.status === "pending" || task.status === "failed") && ( )} {isTaskRunning && (
{showInterruptMenu && ( <> {/* Backdrop to close menu on click outside */}
setShowInterruptMenu(false)} />
)}
)} {canContinue && ( )} {canBranch && ( )} )}
{/* Content */}
{/* Task Info */}
{isEditing ? ( <> setEditName(e.target.value)} className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-lg px-3 py-2 outline-none focus:border-[#3f6fb3]" placeholder="Task name" />