summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/TaskList.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/mesh/TaskList.tsx
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/components/mesh/TaskList.tsx')
-rw-r--r--makima/frontend/src/components/mesh/TaskList.tsx215
1 files changed, 161 insertions, 54 deletions
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>
))}