import dayjs from "dayjs"; import { Calendar, MessageSquare } from "lucide-react"; import { useMemo } from "react"; import { agentColor } from "../lib/agentIdentity"; import { AgentIdenticon } from "./AgentIdenticon"; import { LabelChip } from "./LabelChip"; import { Badge } from "./ui/badge"; interface TaskCardProps { task: any; labels?: { name: string; color: string; description: string }[]; onClick: () => void; onAgentClick?: (task: any) => void; isNew?: boolean; } function getPriorityBadge(priority?: string) { switch (priority) { case "urgent": case "high": case "High": return { label: "High", color: "bg-red-50 text-red-600 border-red-200/60 dark:bg-red-950/40 dark:text-red-400 dark:border-red-900/60", dot: "bg-red-500", }; case "medium": case "med": case "Med": return { label: "Med", color: "bg-amber-50 text-amber-600 border-amber-200/60 dark:bg-amber-950/40 dark:text-amber-400 dark:border-amber-900/60", dot: "bg-amber-500", }; case "low": case "Low": return { label: "Low", color: "bg-blue-50 text-blue-600 border-blue-200/60 dark:bg-blue-950/40 dark:text-blue-400 dark:border-blue-900/60", dot: "bg-blue-500", }; default: return { label: priority || "Low", color: "bg-slate-50 text-slate-600 border-slate-200/60 dark:bg-zinc-800 dark:text-slate-300 dark:border-zinc-700/60", dot: "bg-slate-400", }; } } export function TaskCard({ task, labels = [], onClick, onAgentClick, isNew }: TaskCardProps) { const isAssigned = !!task.assigned_to; const isWorking = task.status === "in_progress" && !task.glow_suppressed; const labelByName = new Map(labels.map((label) => [label.name, label])); const meta = useMemo(() => { if (!task.metadata) return {}; if (typeof task.metadata === "string") { try { return JSON.parse(task.metadata); } catch { return {}; } } return task.metadata; }, [task.metadata]); const priorityVal = task.priority || meta.priority || "low"; const priorityInfo = getPriorityBadge(priorityVal); const steps = Array.isArray(meta.steps) ? meta.steps : []; // Calculate DoD from description checkboxes const calculateDodProgress = (desc?: string | null) => { if (!desc) return null; const regex = /^\s*-\s*\[([ xX])\]\s+/gm; let total = 0; let checked = 0; let match; while ((match = regex.exec(desc)) !== null) { total++; if (match[1].toLowerCase() === "x") checked++; } return total > 0 ? Math.round((checked / total) * 100) : null; }; const desc = task.description || meta.businessContext || meta.technicalNotes || ""; const dodPct = calculateDodProgress(desc); let computedPct = 0; if (dodPct !== null) { computedPct = dodPct; } else if (steps.length > 0) { const completedSteps = steps.filter((s: any) => s.completed || s.status === "done" || s.verified).length; computedPct = Math.round((completedSteps / steps.length) * 100); } let progressPct = 0; if (task.status === "done") { progressPct = 100; } else if (task.status === "in_review") { progressPct = dodPct !== null ? dodPct : 100; } else if (task.status === "in_progress") { progressPct = dodPct !== null ? dodPct : meta.progress !== undefined ? meta.progress : computedPct; } return (
{ (window as any).__draggedTaskId = task.id; e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.setData("application/json", JSON.stringify({ id: task.id })); e.dataTransfer.effectAllowed = "move"; }} onDragEnd={() => { delete (window as any).__draggedTaskId; }} onClick={onClick} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onClick(); } }} role="button" tabIndex={0} className={` relative w-full text-left bg-surface-secondary rounded-xl p-3.5 outline-none transition-all duration-150 ease-out cursor-pointer shadow-xs hover:border-content-tertiary/40 hover:bg-surface-tertiary/50 group ${isWorking ? "border-2 border-accent shadow-xs shadow-accent/20" : "border border-border"} ${isNew ? "animate-card-highlight" : ""} `} style={ isWorking && task.agent_public_key ? { borderColor: `color-mix(in srgb, ${agentColor(task.agent_public_key)} 40%, transparent)`, boxShadow: `0 0 16px color-mix(in srgb, ${agentColor(task.agent_public_key)} 15%, transparent)`, } : undefined } > {/* Priority Pill & Module & Progress Badge */}
{priorityInfo.label}
{meta.module && ( {meta.module} )}
{task.status !== "backlog" && task.status !== "todo" && task.status !== "cancelled" && (
{progressPct}%
)}
{/* Task Title */}

{task.title}

{task.blocked && ( Blocked )}
{/* Task Description with Read more */} {desc && (
{desc} Read more
)} {/* Rich Metadata Badges */} {(meta.steps?.length > 0 || meta.qcChecks?.length > 0 || meta.affectedFiles?.length > 0 || meta.acceptanceTests?.length > 0) && (
{meta.steps?.length > 0 && ( 📝 {meta.steps.length} steps )} {meta.qcChecks?.length > 0 && ( ✅ {meta.qcChecks.length} QC )} {meta.affectedFiles?.length > 0 && ( 📁 {meta.affectedFiles.length} files )} {meta.acceptanceTests?.length > 0 && ( 🧪 {meta.acceptanceTests.length} tests )}
)} {/* Labels */} {task.labels?.length > 0 && (
{task.labels.map((name: string) => { const label = labelByName.get(name); return ; })}
)} {/* Dashed Separator */}
{/* Footer: Stacked Avatars + Working Dot + Stats Icons */}
{/* Left: Stacked Avatars / Assignee + Working Dot */}
{isWorking && ( )} {isAssigned ? ( ) : ( Unassigned )}
{/* Right: Info Metadata (Time, Comments, Subtasks) */}
{task.created_at ? `${dayjs().diff(dayjs(task.created_at), "day")}d` : "6d"} {task.notes?.length ?? 0} 📋 {meta.steps?.length ? `0/${meta.steps.length}` : task.subtask_count ? `0/${task.subtask_count}` : "0/1"}
); }