import { useState, useCallback } from "react"; import type { TaskSummary, TaskStatus } from "../../lib/api"; interface SubtaskTreeProps { subtasks: TaskSummary[]; onSelect: (taskId: string) => void; depth?: number; loading?: boolean; fetchSubtasks?: (taskId: string) => Promise; } interface TreeNodeProps { task: TaskSummary; onSelect: (taskId: string) => void; depth: number; fetchSubtasks?: (taskId: string) => Promise; } function getStatusColor(status: TaskStatus): string { switch (status) { case "pending": return "text-[#9bc3ff]"; 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 getStatusIcon(status: TaskStatus): string { switch (status) { case "pending": return "○"; case "running": return "◉"; case "paused": return "◎"; case "blocked": return "◈"; case "done": return "●"; case "failed": return "✕"; case "merged": return "◆"; default: return "○"; } } function TreeNode({ task, onSelect, depth, fetchSubtasks }: TreeNodeProps) { const [expanded, setExpanded] = useState(false); const [children, setChildren] = useState(null); const [loadingChildren, setLoadingChildren] = useState(false); const hasSubtasks = task.subtaskCount > 0; const handleToggle = useCallback(async () => { if (!hasSubtasks) return; if (expanded) { setExpanded(false); } else { if (!children && fetchSubtasks) { setLoadingChildren(true); try { const subtasks = await fetchSubtasks(task.id); setChildren(subtasks); } catch (err) { console.error("Failed to fetch subtasks:", err); } finally { setLoadingChildren(false); } } setExpanded(true); } }, [expanded, children, hasSubtasks, task.id, fetchSubtasks]); const indent = depth * 16; return (
{/* Expand/Collapse button */} {/* Status icon */} {getStatusIcon(task.status)} {/* Task name - clickable */} {/* Subtask count badge */} {hasSubtasks && ( {task.subtaskCount} sub )} {/* Priority indicator */} {task.priority > 0 && ( P{task.priority} )}
{/* Children */} {expanded && children && children.length > 0 && (
{children.map((child) => ( ))}
)}
); } export function SubtaskTree({ subtasks, onSelect, depth = 0, loading = false, fetchSubtasks, }: SubtaskTreeProps) { if (loading) { return (
Loading subtasks...
); } if (subtasks.length === 0) { return (
No subtasks
); } return (
{subtasks.map((task) => ( ))}
); } // Aggregated status summary for a task tree export interface TaskTreeStats { total: number; pending: number; running: number; paused: number; blocked: number; done: number; failed: number; merged: number; } export function calculateTreeStats(subtasks: TaskSummary[]): TaskTreeStats { const stats: TaskTreeStats = { total: subtasks.length, pending: 0, running: 0, paused: 0, blocked: 0, done: 0, failed: 0, merged: 0, }; for (const task of subtasks) { switch (task.status) { case "pending": stats.pending++; break; case "running": stats.running++; break; case "paused": stats.paused++; break; case "blocked": stats.blocked++; break; case "done": stats.done++; break; case "failed": stats.failed++; break; case "merged": stats.merged++; break; } } return stats; } // Visual summary bar export function SubtaskProgressBar({ stats }: { stats: TaskTreeStats }) { if (stats.total === 0) return null; const segments = [ { count: stats.merged, color: "bg-purple-400", label: "Merged" }, { count: stats.done, color: "bg-emerald-400", label: "Done" }, { count: stats.running, color: "bg-green-400", label: "Running" }, { count: stats.paused, color: "bg-yellow-400", label: "Paused" }, { count: stats.blocked, color: "bg-orange-400", label: "Blocked" }, { count: stats.pending, color: "bg-[#75aafc]", label: "Pending" }, { count: stats.failed, color: "bg-red-400", label: "Failed" }, ].filter((s) => s.count > 0); return (
{/* Progress bar */}
{segments.map((segment, i) => (
))}
{/* Legend */}
{segments.map((segment, i) => (
{segment.label}: {segment.count}
))}
); }