From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 6 Jan 2026 04:08:11 +0000 Subject: Initial Control system --- .../frontend/src/components/mesh/SubtaskTree.tsx | 297 +++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 makima/frontend/src/components/mesh/SubtaskTree.tsx (limited to 'makima/frontend/src/components/mesh/SubtaskTree.tsx') diff --git a/makima/frontend/src/components/mesh/SubtaskTree.tsx b/makima/frontend/src/components/mesh/SubtaskTree.tsx new file mode 100644 index 0000000..176b7a7 --- /dev/null +++ b/makima/frontend/src/components/mesh/SubtaskTree.tsx @@ -0,0 +1,297 @@ +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} + +
+ ))} +
+
+ ); +} -- cgit v1.2.3