summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/mesh/TaskList.tsx
blob: d013782ad9370cb167b2db02a06b0f3512957a76 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11

                                                                            








                                 






                                      




















































                                                       










                                                             






                          














































                                                                                                    







                                                                                
                                                                              















                                                                                                                                                                                            
                             



                                                                                        








                                                                                                                                      
                             


                                                                                                        

                               














































































                                                                                                                                                                                     
                          
                     








                      
import { useMemo } from "react";
import type { TaskSummary, TaskStatus, ContractPhase } from "../../lib/api";

interface TaskListProps {
  tasks: TaskSummary[];
  loading: boolean;
  onSelect: (id: string) => void;
  onDelete: (id: string) => void;
  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", {
    month: "short",
    day: "numeric",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
}

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 getStatusBgColor(status: TaskStatus): string {
  switch (status) {
    case "pending":
      return "bg-[rgba(117,170,252,0.1)]";
    case "running":
      return "bg-green-400/10";
    case "paused":
      return "bg-yellow-400/10";
    case "blocked":
      return "bg-orange-400/10";
    case "done":
      return "bg-emerald-400/10";
    case "failed":
      return "bg-red-400/10";
    case "merged":
      return "bg-purple-400/10";
    default:
      return "bg-[rgba(117,170,252,0.1)]";
  }
}

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,
  onSelect,
  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">
        <div className="font-mono text-[#9bc3ff] text-sm">Loading tasks...</div>
      </div>
    );
  }

  const totalTasks = groupedTasks.reduce((sum, g) => sum + g.tasks.length, 0);

  return (
    <div className="panel h-full flex flex-col">
      <div className="flex items-center justify-between p-4 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)]">
        <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
          MESH//TASKS
        </div>
        <button
          onClick={onCreate}
          className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors uppercase"
        >
          + New Task
        </button>
      </div>

      <div className="flex-1 overflow-y-auto">
        {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>
            {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>
                      {group.contractPhase && (
                        <span className={`font-mono text-[10px] ${getPhaseColor(group.contractPhase)}`}>
                          [{group.contractPhase}]
                        </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>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}