From 87044a747b47bd83249d61a45842c7f7b2eae56d Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sun, 11 Jan 2026 05:52:14 +0000
Subject:  Contract system

---
 makima/frontend/src/components/mesh/TaskList.tsx | 215 +++++++++++++++++------
 1 file changed, 161 insertions(+), 54 deletions(-)

(limited to 'makima/frontend/src/components/mesh/TaskList.tsx')

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>
             ))}
-- 
cgit v1.2.3

