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

                                                                                            





                                 
                                  


                       



                                      
                                        


                       

                                                                




















































                                                       










                                                             




                          
            

                   



                                                                           
                                      


                                                                                                
 










                                                                                 


                                                          
                                       





                                            
                                              





























                                                                                                    
                            
 







                                                                                
                                                                              
 






                                                                   





                                                                                                                         























                                                                                                                                                                                              


                                              
                             
                                                                                        
                                   

                                                                                      

                








                                                                                                                                      
                             


                                                                                                        

                               




                                                                             










                                                                                                                            


























































                                                                                                                                                
























                                                                                                                                                                                                                   
                            
                          
                     








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

interface TaskListProps {
  tasks: TaskSummary[];
  loading: boolean;
  onSelect: (id: string) => void;
  onDelete: (id: string) => void;
  onDismiss: (id: string) => void;
  onCreate: () => void;
}

interface GroupedTasks {
  contractId: string | null;
  contractName: string | null;
  contractPhase: ContractPhase | null;
  contractStatus: ContractStatus | null;
  tasks: TaskSummary[];
}

type StatusFilter = 'all' | 'active' | 'completed' | 'archived';

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,
  onDismiss,
  onCreate,
}: TaskListProps) {
  // Filter state - default to 'active' to show only active contracts
  const [statusFilter, setStatusFilter] = useState<StatusFilter>('active');

  // Group tasks by contract and filter by status
  const groupedTasks = useMemo(() => {
    // Separate root tasks (no parent) from subtasks
    // Show supervisor tasks AND standalone tasks (tasks without a contract)
    const rootTasks = tasks.filter((t) => !t.parentTaskId && (t.isSupervisor || !t.contractId));

    // Filter tasks based on contract status
    const filteredTasks = statusFilter === 'all'
      ? rootTasks
      : rootTasks.filter((task) => {
          // Tasks without a contract go in 'all' only, or treat them as 'active'
          if (!task.contractId) {
            return statusFilter === 'active';
          }
          return task.contractStatus === statusFilter;
        });

    // Group by contractId
    const groups = new Map<string | null, GroupedTasks>();

    for (const task of filteredTasks) {
      const key = task.contractId;
      if (!groups.has(key)) {
        groups.set(key, {
          contractId: task.contractId,
          contractName: task.contractName,
          contractPhase: task.contractPhase,
          contractStatus: task.contractStatus,
          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, statusFilter]);

  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);

  const filterOptions: { value: StatusFilter; label: string }[] = [
    { value: 'all', label: 'All' },
    { value: 'active', label: 'Active' },
    { value: 'completed', label: 'Completed' },
    { value: 'archived', label: 'Archive' },
  ];

  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>
        <div className="flex items-center gap-2">
          {/* Status filter toggle */}
          <div className="flex border border-[rgba(117,170,252,0.25)]">
            {filterOptions.map((option) => (
              <button
                key={option.value}
                onClick={() => setStatusFilter(option.value)}
                className={`px-2 py-1 font-mono text-[10px] uppercase transition-colors ${
                  statusFilter === option.value
                    ? 'bg-[rgba(117,170,252,0.2)] text-[#dbe7ff] border-r border-[rgba(117,170,252,0.25)] last:border-r-0'
                    : 'text-[#8b949e] hover:text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)] border-r border-[rgba(117,170,252,0.25)] last:border-r-0'
                }`}
              >
                {option.label}
              </button>
            ))}
          </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>

      <div className="flex-1 overflow-y-auto">
        {totalTasks === 0 ? (
          <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
            {statusFilter === 'all'
              ? 'No tasks yet. Create a contract or a standalone task to get started.'
              : `No ${statusFilter} tasks found.`}
          </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>
                    </>
                  ) : (
                    <>
                      <svg className="w-4 h-4 text-[#9bc3ff]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
                      </svg>
                      <span className="font-mono text-xs text-[#9bc3ff]">
                        Standalone Tasks
                      </span>
                      <span className="font-mono text-[10px] text-[#8b949e]">
                        ({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 */}
                        <div className="flex gap-2">
                          {/* Show dismiss button for completed standalone tasks (tasks without a contract) */}
                          {!task.contractId && (task.status === "done" || task.status === "failed" || task.status === "merged") && (
                            <button
                              onClick={(e) => {
                                e.stopPropagation();
                                onDismiss(task.id);
                              }}
                              className="px-2 py-1 font-mono text-[10px] text-[#8b949e] hover:bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
                            >
                              Dismiss
                            </button>
                          )}
                          {!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>
    </div>
  );
}