import { useState, useCallback } from "react";
import type { TaskSummary, TaskStatus } from "../../lib/api";
interface TaskTreeProps {
tasks: TaskSummary[];
supervisorTaskId: string | null;
onSelect: (taskId: string) => void;
onStartSupervisor?: () => void;
loading?: boolean;
fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>;
}
interface TreeNodeProps {
task: TaskSummary;
isSupervisorTask: boolean;
onSelect: (taskId: string) => void;
depth: number;
fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>;
}
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, isSupervisorTask, onSelect, depth, fetchSubtasks }: TreeNodeProps) {
const [expanded, setExpanded] = useState(isSupervisorTask); // Supervisor expanded by default
const [children, setChildren] = useState<TaskSummary[] | null>(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 (
<div className="select-none">
<div
className={`flex items-center gap-2 py-2 px-2 hover:bg-[rgba(117,170,252,0.05)] transition-colors cursor-pointer group ${
isSupervisorTask ? "bg-[rgba(117,170,252,0.08)] border-l-2 border-[#75aafc]" : ""
}`}
style={{ paddingLeft: `${indent + 8}px` }}
>
{/* Expand/Collapse button */}
<button
onClick={handleToggle}
className={`w-4 h-4 flex items-center justify-center font-mono text-[10px] ${
hasSubtasks
? "text-[#75aafc] hover:text-[#9bc3ff]"
: "text-transparent cursor-default"
}`}
disabled={!hasSubtasks}
>
{loadingChildren ? (
<span className="animate-spin">...</span>
) : hasSubtasks ? (
expanded ? "▼" : "▶"
) : (
""
)}
</button>
{/* Supervisor badge or status icon */}
{isSupervisorTask ? (
<span className="px-1.5 py-0.5 font-mono text-[9px] bg-[#0f3c78] text-[#75aafc] border border-[#3f6fb3] uppercase">
Supervisor
</span>
) : (
<span
className={`font-mono text-xs ${getStatusColor(task.status)}`}
title={task.status}
>
{getStatusIcon(task.status)}
</span>
)}
{/* Task name - clickable */}
<button
onClick={() => onSelect(task.id)}
className={`flex-1 text-left font-mono text-sm transition-colors truncate ${
isSupervisorTask
? "text-[#9bc3ff] hover:text-white font-medium"
: "text-[#dbe7ff] hover:text-white"
}`}
>
{task.name}
</button>
{/* Status for supervisor */}
{isSupervisorTask && (
<span
className={`font-mono text-[10px] ${getStatusColor(task.status)}`}
>
{task.status}
</span>
)}
{/* Subtask count badge */}
{hasSubtasks && !isSupervisorTask && (
<span className="font-mono text-[9px] text-[#555] group-hover:text-[#75aafc]">
{task.subtaskCount} sub
</span>
)}
{/* Priority indicator */}
{task.priority > 0 && (
<span className="font-mono text-[9px] text-orange-400">
P{task.priority}
</span>
)}
{/* Open in full page button */}
<a
href={`/tasks/${task.id}`}
onClick={(e) => e.stopPropagation()}
className="font-mono text-[10px] text-[#555] opacity-0 group-hover:opacity-100 hover:text-[#75aafc] transition-all"
title="Open task page"
>
↗
</a>
</div>
{/* Children */}
{expanded && children && children.length > 0 && (
<div className="border-l border-[rgba(117,170,252,0.15)]" style={{ marginLeft: `${indent + 16}px` }}>
{children.map((child) => (
<TreeNode
key={child.id}
task={child}
isSupervisorTask={false}
onSelect={onSelect}
depth={depth + 1}
fetchSubtasks={fetchSubtasks}
/>
))}
</div>
)}
</div>
);
}
// Stats summary component
export interface TaskTreeStats {
total: number;
pending: number;
running: number;
paused: number;
blocked: number;
done: number;
failed: number;
merged: number;
}
export function calculateTreeStats(tasks: TaskSummary[]): TaskTreeStats {
const stats: TaskTreeStats = {
total: tasks.length,
pending: 0,
running: 0,
paused: 0,
blocked: 0,
done: 0,
failed: 0,
merged: 0,
};
for (const task of tasks) {
// Skip supervisor task in stats
if (task.isSupervisor) continue;
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;
}
}
// Adjust total to exclude supervisor
stats.total = tasks.filter(t => !t.isSupervisor).length;
return stats;
}
// Progress bar for task tree
export function TaskTreeProgressBar({ stats }: { stats: TaskTreeStats }) {
if (stats.total === 0) return null;
const completedPercent = ((stats.done + stats.merged) / stats.total) * 100;
const runningPercent = (stats.running / stats.total) * 100;
const failedPercent = (stats.failed / stats.total) * 100;
return (
<div className="space-y-2">
{/* Progress bar */}
<div className="h-2 bg-[rgba(117,170,252,0.1)] rounded overflow-hidden flex">
<div
className="bg-emerald-400 transition-all"
style={{ width: `${completedPercent}%` }}
title={`Completed: ${stats.done + stats.merged}`}
/>
<div
className="bg-green-400 transition-all"
style={{ width: `${runningPercent}%` }}
title={`Running: ${stats.running}`}
/>
<div
className="bg-red-400 transition-all"
style={{ width: `${failedPercent}%` }}
title={`Failed: ${stats.failed}`}
/>
</div>
{/* Summary */}
<div className="flex items-center justify-between font-mono text-[10px]">
<span className="text-[#555]">
{stats.done + stats.merged} / {stats.total} completed
</span>
{stats.running > 0 && (
<span className="text-green-400">{stats.running} running</span>
)}
{stats.failed > 0 && (
<span className="text-red-400">{stats.failed} failed</span>
)}
</div>
</div>
);
}
export function TaskTree({
tasks,
supervisorTaskId,
onSelect,
onStartSupervisor,
loading = false,
fetchSubtasks,
}: TaskTreeProps) {
if (loading) {
return (
<div className="p-4 text-center font-mono text-xs text-[#555]">
Loading tasks...
</div>
);
}
// Separate supervisor from other tasks
const supervisorTask = tasks.find(t => t.id === supervisorTaskId || t.isSupervisor);
const workerTasks = tasks.filter(t => t.id !== supervisorTaskId && !t.isSupervisor && !t.parentTaskId);
// Calculate stats for worker tasks
const stats = calculateTreeStats(tasks);
return (
<div className="space-y-4">
{/* Supervisor Section */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="font-mono text-xs text-[#75aafc] uppercase">
Contract Supervisor
</h3>
{supervisorTask && supervisorTask.status === "pending" && onStartSupervisor && (
<button
onClick={onStartSupervisor}
className="px-2 py-1 font-mono text-[10px] text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors"
>
Start Supervisor
</button>
)}
</div>
{supervisorTask ? (
<TreeNode
task={supervisorTask}
isSupervisorTask={true}
onSelect={onSelect}
depth={0}
fetchSubtasks={fetchSubtasks}
/>
) : (
<div className="p-3 border border-dashed border-[rgba(117,170,252,0.2)] text-center">
<p className="font-mono text-xs text-[#555]">
No supervisor task found
</p>
</div>
)}
</div>
{/* Progress Section */}
{stats.total > 0 && (
<div className="space-y-2">
<h3 className="font-mono text-xs text-[#75aafc] uppercase">
Task Progress
</h3>
<TaskTreeProgressBar stats={stats} />
</div>
)}
{/* Worker Tasks Section */}
{workerTasks.length > 0 && (
<div className="space-y-2">
<h3 className="font-mono text-xs text-[#75aafc] uppercase">
Worker Tasks ({workerTasks.length})
</h3>
<div className="divide-y divide-[rgba(117,170,252,0.1)]">
{workerTasks.map((task) => (
<TreeNode
key={task.id}
task={task}
isSupervisorTask={false}
onSelect={onSelect}
depth={0}
fetchSubtasks={fetchSubtasks}
/>
))}
</div>
</div>
)}
{/* Empty State */}
{workerTasks.length === 0 && !supervisorTask && (
<div className="p-4 text-center font-mono text-xs text-[#555]">
No tasks in this contract
</div>
)}
</div>
);
}