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;
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,
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
const rootTasks = tasks.filter((t) => !t.parentTaskId);
// 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 one to start orchestrating Claude Code instances.'
: `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>
</>
) : (
<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>
);
}