summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh')
-rw-r--r--makima/frontend/src/components/mesh/TaskDetail.tsx12
-rw-r--r--makima/frontend/src/components/mesh/TaskList.tsx215
-rw-r--r--makima/frontend/src/components/mesh/TaskOutput.tsx96
-rw-r--r--makima/frontend/src/components/mesh/TaskTree.tsx390
4 files changed, 659 insertions, 54 deletions
diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx
index be4fb80..967b1d1 100644
--- a/makima/frontend/src/components/mesh/TaskDetail.tsx
+++ b/makima/frontend/src/components/mesh/TaskDetail.tsx
@@ -23,6 +23,8 @@ interface TaskDetailProps {
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;
// Optional advanced features
overlayDiff?: string;
changedFiles?: string[];
@@ -105,6 +107,7 @@ export function TaskDetail({
onCreateSubtask,
onToggleSubtaskOutput,
viewingSubtaskId,
+ onViewContract,
overlayDiff,
changedFiles,
onRequestDiff,
@@ -417,6 +420,15 @@ export function TaskDetail({
>
{task.status}
</span>
+ {/* Contract badge - clickable to view contract */}
+ {task.contractId && onViewContract && (
+ <button
+ onClick={() => onViewContract(task.contractId!)}
+ className="px-2 py-0.5 font-mono text-xs text-blue-400 bg-blue-400/10 border border-blue-400/20 hover:bg-blue-400/20 transition-colors"
+ >
+ Contract
+ </button>
+ )}
{/* Orchestrator badge for depth 0 tasks with subtasks */}
{task.depth === 0 && task.subtasks.length > 0 && (
<span className="px-2 py-0.5 font-mono text-xs text-purple-400 bg-purple-400/10 border border-purple-400/20">
diff --git a/makima/frontend/src/components/mesh/TaskList.tsx b/makima/frontend/src/components/mesh/TaskList.tsx
index a37e564..d013782 100644
--- a/makima/frontend/src/components/mesh/TaskList.tsx
+++ b/makima/frontend/src/components/mesh/TaskList.tsx
@@ -1,4 +1,5 @@
-import type { TaskSummary, TaskStatus } from "../../lib/api";
+import { useMemo } from "react";
+import type { TaskSummary, TaskStatus, ContractPhase } from "../../lib/api";
interface TaskListProps {
tasks: TaskSummary[];
@@ -8,6 +9,13 @@ interface TaskListProps {
onCreate: () => void;
}
+interface GroupedTasks {
+ contractId: string | null;
+ contractName: string | null;
+ contractPhase: ContractPhase | null;
+ tasks: TaskSummary[];
+}
+
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
@@ -61,6 +69,17 @@ function getStatusBgColor(status: TaskStatus): string {
}
}
+function getPhaseColor(phase: ContractPhase | null): string {
+ switch (phase) {
+ case "research": return "text-blue-400";
+ case "specify": return "text-cyan-400";
+ case "plan": return "text-yellow-400";
+ case "execute": return "text-green-400";
+ case "review": return "text-purple-400";
+ default: return "text-[#8b949e]";
+ }
+}
+
export function TaskList({
tasks,
loading,
@@ -68,6 +87,53 @@ export function TaskList({
onDelete,
onCreate,
}: TaskListProps) {
+ // Group tasks by contract
+ const groupedTasks = useMemo(() => {
+ // Separate root tasks (no parent) from subtasks
+ const rootTasks = tasks.filter((t) => !t.parentTaskId);
+
+ // Group by contractId
+ const groups = new Map<string | null, GroupedTasks>();
+
+ for (const task of rootTasks) {
+ const key = task.contractId;
+ if (!groups.has(key)) {
+ groups.set(key, {
+ contractId: task.contractId,
+ contractName: task.contractName,
+ contractPhase: task.contractPhase,
+ tasks: [],
+ });
+ }
+ groups.get(key)!.tasks.push(task);
+ }
+
+ // Sort tasks within each group: supervisors first, then by status (running first), then by date
+ for (const group of groups.values()) {
+ group.tasks.sort((a, b) => {
+ // Supervisors always first
+ if (a.isSupervisor && !b.isSupervisor) return -1;
+ if (!a.isSupervisor && b.isSupervisor) return 1;
+ // Running tasks next
+ if (a.status === "running" && b.status !== "running") return -1;
+ if (a.status !== "running" && b.status === "running") return 1;
+ // Then by date (newest first)
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
+ });
+ }
+
+ // Sort: contracts first (alphabetically by name), then orphan tasks
+ const sorted = Array.from(groups.values()).sort((a, b) => {
+ // Orphan tasks (no contract) go last
+ if (!a.contractId && b.contractId) return 1;
+ if (a.contractId && !b.contractId) return -1;
+ // Sort by contract name
+ return (a.contractName || "").localeCompare(b.contractName || "");
+ });
+
+ return sorted;
+ }, [tasks]);
+
if (loading) {
return (
<div className="panel h-full flex items-center justify-center">
@@ -76,8 +142,7 @@ export function TaskList({
);
}
- // Separate root tasks (no parent) from subtasks
- const rootTasks = tasks.filter((t) => !t.parentTaskId);
+ const totalTasks = groupedTasks.reduce((sum, g) => sum + g.tasks.length, 0);
return (
<div className="panel h-full flex flex-col">
@@ -94,65 +159,107 @@ export function TaskList({
</div>
<div className="flex-1 overflow-y-auto">
- {rootTasks.length === 0 ? (
+ {totalTasks === 0 ? (
<div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
No tasks yet. Create one to start orchestrating Claude Code instances.
</div>
) : (
- <div className="divide-y divide-[rgba(117,170,252,0.15)]">
- {rootTasks.map((task) => (
- <div
- key={task.id}
- className="p-4 hover:bg-[rgba(117,170,252,0.05)] transition-colors"
- >
- <div className="flex items-start justify-between gap-4">
- <button
- onClick={() => onSelect(task.id)}
- className="flex-1 text-left"
- >
- <div className="flex items-center gap-2 mb-1">
- <h3 className="font-mono text-sm text-[#dbe7ff]">
- {task.name}
- </h3>
- <span
- className={`px-2 py-0.5 font-mono text-[10px] uppercase ${getStatusColor(
- task.status
- )} ${getStatusBgColor(task.status)} border border-current/20`}
- >
- {task.status}
+ <div>
+ {groupedTasks.map((group) => (
+ <div key={group.contractId || "orphan"}>
+ {/* Contract header */}
+ <div className="sticky top-0 bg-[#0d1117] border-b border-[rgba(117,170,252,0.25)] px-4 py-2 flex items-center gap-2">
+ {group.contractId ? (
+ <>
+ <span className="font-mono text-xs text-[#dbe7ff] font-medium">
+ {group.contractName}
</span>
- {task.depth === 0 && task.subtaskCount > 0 && (
- <span className="px-2 py-0.5 font-mono text-[10px] text-purple-400 bg-purple-400/10 border border-purple-400/20">
- Orchestrator
+ {group.contractPhase && (
+ <span className={`font-mono text-[10px] ${getPhaseColor(group.contractPhase)}`}>
+ [{group.contractPhase}]
</span>
)}
- {task.priority > 0 && (
- <span className="px-2 py-0.5 font-mono text-[10px] text-orange-400 bg-orange-400/10 border border-orange-400/20">
- P{task.priority}
- </span>
- )}
- </div>
- {task.progressSummary && (
- <p className="font-mono text-xs text-[#9bc3ff] mb-2 line-clamp-2">
- {task.progressSummary}
- </p>
- )}
- <div className="flex gap-4 font-mono text-[10px] text-[#75aafc]">
- {task.subtaskCount > 0 && (
- <span>{task.subtaskCount} subtasks</span>
- )}
- <span>{formatDate(task.createdAt)}</span>
+ <span className="font-mono text-[10px] text-[#8b949e]">
+ ({group.tasks.length})
+ </span>
+ </>
+ ) : (
+ <span className="font-mono text-xs text-[#8b949e] italic">
+ Unassigned Tasks ({group.tasks.length})
+ </span>
+ )}
+ </div>
+
+ {/* Tasks in this group */}
+ <div className="divide-y divide-[rgba(117,170,252,0.15)]">
+ {group.tasks.map((task) => (
+ <div
+ key={task.id}
+ className={`p-4 hover:bg-[rgba(117,170,252,0.05)] transition-colors ${
+ task.isSupervisor
+ ? "bg-[rgba(117,170,252,0.08)] border-l-2 border-[#75aafc]"
+ : ""
+ }`}
+ >
+ <div className="flex items-start justify-between gap-4">
+ <button
+ onClick={() => onSelect(task.id)}
+ className="flex-1 text-left"
+ >
+ <div className="flex items-center gap-2 mb-1">
+ <h3 className="font-mono text-sm text-[#dbe7ff]">
+ {task.name}
+ </h3>
+ <span
+ className={`px-2 py-0.5 font-mono text-[10px] uppercase ${getStatusColor(
+ task.status
+ )} ${getStatusBgColor(task.status)} border border-current/20`}
+ >
+ {task.status}
+ </span>
+ {task.isSupervisor && (
+ <span className="px-2 py-0.5 font-mono text-[10px] text-[#75aafc] bg-[#0f3c78] border border-[#3f6fb3] uppercase">
+ Supervisor
+ </span>
+ )}
+ {!task.isSupervisor && task.depth === 0 && task.subtaskCount > 0 && (
+ <span className="px-2 py-0.5 font-mono text-[10px] text-purple-400 bg-purple-400/10 border border-purple-400/20">
+ Orchestrator
+ </span>
+ )}
+ {task.priority > 0 && (
+ <span className="px-2 py-0.5 font-mono text-[10px] text-orange-400 bg-orange-400/10 border border-orange-400/20">
+ P{task.priority}
+ </span>
+ )}
+ </div>
+ {task.progressSummary && (
+ <p className="font-mono text-xs text-[#9bc3ff] mb-2 line-clamp-2">
+ {task.progressSummary}
+ </p>
+ )}
+ <div className="flex gap-4 font-mono text-[10px] text-[#75aafc]">
+ {task.subtaskCount > 0 && (
+ <span>{task.subtaskCount} subtasks</span>
+ )}
+ <span>{formatDate(task.createdAt)}</span>
+ </div>
+ </button>
+ {/* Supervisor tasks cannot be deleted directly - they are deleted with the contract */}
+ {!task.isSupervisor && (
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ onDelete(task.id);
+ }}
+ className="px-2 py-1 font-mono text-[10px] text-red-400 hover:bg-red-400/10 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
+ >
+ Delete
+ </button>
+ )}
+ </div>
</div>
- </button>
- <button
- onClick={(e) => {
- e.stopPropagation();
- onDelete(task.id);
- }}
- className="px-2 py-1 font-mono text-[10px] text-red-400 hover:bg-red-400/10 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
- >
- Delete
- </button>
+ ))}
</div>
</div>
))}
diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx
index 10de225..cb0eba3 100644
--- a/makima/frontend/src/components/mesh/TaskOutput.tsx
+++ b/makima/frontend/src/components/mesh/TaskOutput.tsx
@@ -275,7 +275,103 @@ function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) {
</div>
);
+ case "auth_required":
+ return <AuthRequiredEntry entry={entry} />;
+
default:
return null;
}
}
+
+function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) {
+ const [authCode, setAuthCode] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const loginUrl = entry.toolInput?.loginUrl as string | undefined;
+ const hostname = entry.toolInput?.hostname as string | undefined;
+ // Get taskId from entry or fallback to toolInput (for robustness)
+ const taskId = entry.taskId || (entry.toolInput?.taskId as string | undefined);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!authCode.trim() || !taskId) return;
+
+ setSubmitting(true);
+ setError(null);
+
+ try {
+ // Send the auth code to the task via the message endpoint
+ await sendTaskMessage(taskId, `AUTH_CODE:${authCode.trim()}`);
+ setSubmitted(true);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to submit code");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (submitted) {
+ return (
+ <div className="bg-green-900/30 border border-green-500/50 rounded p-3 my-2">
+ <div className="flex items-center gap-2 text-green-400 font-semibold">
+ <span>✓</span>
+ <span>Authentication code submitted</span>
+ </div>
+ <p className="text-green-200/80 text-sm mt-1">
+ Waiting for authentication to complete...
+ </p>
+ </div>
+ );
+ }
+
+ return (
+ <div className="bg-amber-900/30 border border-amber-500/50 rounded p-3 my-2">
+ <div className="flex items-center gap-2 text-amber-400 font-semibold mb-2">
+ <span>🔐</span>
+ <span>Authentication Required{hostname ? ` (${hostname})` : ""}</span>
+ </div>
+ <p className="text-amber-200/80 text-sm mb-3">
+ The daemon's OAuth token has expired. Click the button to login, then paste the code below:
+ </p>
+
+ <div className="flex flex-col gap-3">
+ {loginUrl ? (
+ <a
+ href={loginUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="inline-block bg-amber-500 hover:bg-amber-400 text-black font-medium px-4 py-2 rounded transition-colors text-center"
+ >
+ 1. Login to Claude
+ </a>
+ ) : (
+ <p className="text-red-400 text-sm">Login URL not available</p>
+ )}
+
+ <form onSubmit={handleSubmit} className="flex gap-2">
+ <input
+ type="text"
+ value={authCode}
+ onChange={(e) => setAuthCode(e.target.value)}
+ placeholder="2. Paste authentication code here"
+ className="flex-1 bg-[#0a1525] border border-amber-500/30 rounded px-3 py-2 text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400"
+ disabled={submitting}
+ />
+ <button
+ type="submit"
+ disabled={submitting || !authCode.trim()}
+ className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-medium px-4 py-2 rounded transition-colors"
+ >
+ {submitting ? "..." : "Submit"}
+ </button>
+ </form>
+
+ {error && (
+ <p className="text-red-400 text-sm">{error}</p>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/mesh/TaskTree.tsx b/makima/frontend/src/components/mesh/TaskTree.tsx
new file mode 100644
index 0000000..46ae78d
--- /dev/null
+++ b/makima/frontend/src/components/mesh/TaskTree.tsx
@@ -0,0 +1,390 @@
+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>
+ )}
+ </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>
+ );
+}