import { useState, useEffect, useMemo, memo } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { HotkeyButton } from "@/components/ui/hotkey-button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Feature, useAppStore, ThinkingLevel } from "@/store/app-store"; import { GripVertical, Edit, CheckCircle2, Circle, Loader2, Trash2, Eye, PlayCircle, RotateCcw, StopCircle, Hand, MessageSquare, GitCommit, Cpu, Wrench, ListTodo, Sparkles, Expand, FileText, MoreVertical, AlertCircle, GitBranch, GitPullRequest, ExternalLink, ChevronDown, ChevronUp, Brain, Wand2, Archive, Lock, } from "lucide-react"; import { CountUpTimer } from "@/components/ui/count-up-timer"; import { getElectronAPI } from "@/lib/electron"; import { getBlockingDependencies } from "@/lib/dependency-resolver"; import { parseAgentContext, AgentTaskInfo, formatModelName, DEFAULT_MODEL, } from "@/lib/agent-context-parser"; import { Markdown } from "@/components/ui/markdown"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; /** * Formats thinking level for compact display */ function formatThinkingLevel(level: ThinkingLevel | undefined): string { if (!level || level === "none") return ""; const labels: Record = { none: "", low: "Low", medium: "Med", // high: "High", // ultrathink: "Ultra", }; return labels[level]; } interface KanbanCardProps { feature: Feature; onEdit: () => void; onDelete: () => void; onViewOutput?: () => void; onVerify?: () => void; onResume?: () => void; onForceStop?: () => void; onManualVerify?: () => void; onMoveBackToInProgress?: () => void; onFollowUp?: () => void; onCommit?: () => void; onImplement?: () => void; onComplete?: () => void; onViewPlan?: () => void; onApprovePlan?: () => void; hasContext?: boolean; isCurrentAutoTask?: boolean; shortcutKey?: string; contextContent?: string; summary?: string; opacity?: number; glassmorphism?: boolean; cardBorderEnabled?: boolean; cardBorderOpacity?: number; } export const KanbanCard = memo(function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, onResume, onForceStop, onManualVerify, onMoveBackToInProgress, onFollowUp, onCommit, onImplement, onComplete, onViewPlan, onApprovePlan, hasContext, isCurrentAutoTask, shortcutKey, contextContent, summary, opacity = 100, glassmorphism = true, cardBorderEnabled = true, cardBorderOpacity = 100, }: KanbanCardProps) { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false); const [agentInfo, setAgentInfo] = useState(null); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); const [currentTime, setCurrentTime] = useState(() => Date.now()); const { kanbanCardDetailLevel, enableDependencyBlocking, features, useWorktrees, } = useAppStore(); // Calculate blocking dependencies (if feature is in backlog and has incomplete dependencies) const blockingDependencies = useMemo(() => { if (!enableDependencyBlocking || feature.status !== "backlog") { return []; } return getBlockingDependencies(feature, features); }, [enableDependencyBlocking, feature, features]); const showSteps = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed"; const showAgentInfo = kanbanCardDetailLevel === "detailed"; const isJustFinished = useMemo(() => { if ( !feature.justFinishedAt || feature.status !== "waiting_approval" || feature.error ) { return false; } const finishedTime = new Date(feature.justFinishedAt).getTime(); const twoMinutes = 2 * 60 * 1000; return currentTime - finishedTime < twoMinutes; }, [feature.justFinishedAt, feature.status, feature.error, currentTime]); useEffect(() => { if (!feature.justFinishedAt || feature.status !== "waiting_approval") { return; } const finishedTime = new Date(feature.justFinishedAt).getTime(); const twoMinutes = 2 * 60 * 1000; const timeRemaining = twoMinutes - (currentTime - finishedTime); if (timeRemaining <= 0) { return; } const interval = setInterval(() => { setCurrentTime(Date.now()); }, 1000); return () => clearInterval(interval); }, [feature.justFinishedAt, feature.status, currentTime]); useEffect(() => { const loadContext = async () => { if (contextContent) { const info = parseAgentContext(contextContent); setAgentInfo(info); return; } if (feature.status === "backlog") { setAgentInfo(null); return; } try { const api = getElectronAPI(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const currentProject = (window as any).__currentProject; if (!currentProject?.path) return; if (api.features) { const result = await api.features.getAgentOutput( currentProject.path, feature.id ); if (result.success && result.content) { const info = parseAgentContext(result.content); setAgentInfo(info); } } else { const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`; const result = await api.readFile(contextPath); if (result.success && result.content) { const info = parseAgentContext(result.content); setAgentInfo(info); } } } catch { console.debug("[KanbanCard] No context file for feature:", feature.id); } }; loadContext(); if (isCurrentAutoTask) { const interval = setInterval(loadContext, 3000); return () => clearInterval(interval); } }, [feature.id, feature.status, contextContent, isCurrentAutoTask]); const handleDeleteClick = (e: React.MouseEvent) => { e.stopPropagation(); setIsDeleteDialogOpen(true); }; const handleConfirmDelete = () => { onDelete(); }; const isDraggable = feature.status === "backlog" || feature.status === "waiting_approval" || feature.status === "verified" || (feature.status === "in_progress" && !isCurrentAutoTask); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: feature.id, disabled: !isDraggable, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : undefined, }; const borderStyle: React.CSSProperties = { ...style }; if (!cardBorderEnabled) { (borderStyle as Record).borderWidth = "0px"; (borderStyle as Record).borderColor = "transparent"; } else if (cardBorderOpacity !== 100) { (borderStyle as Record).borderWidth = "1px"; (borderStyle as Record).borderColor = `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`; } const cardElement = ( {/* Background overlay with opacity */} {!isDragging && (
)} {/* Compact Badge Row */} {(feature.error || (blockingDependencies.length > 0 && !feature.error && !feature.skipTests && feature.status === "backlog") || isJustFinished) && (
{/* Error badge */} {feature.error && (

{feature.error}

)} {/* Blocked badge */} {blockingDependencies.length > 0 && !feature.error && !feature.skipTests && feature.status === "backlog" && (

Blocked by {blockingDependencies.length} incomplete{" "} {blockingDependencies.length === 1 ? "dependency" : "dependencies"}

{blockingDependencies .map((depId) => { const dep = features.find((f) => f.id === depId); return dep?.description || depId; }) .join(", ")}

)} {/* Just Finished badge */} {isJustFinished && (
)}
)} {/* Category row */}
{feature.category}
{/* Priority and Manual Verification badges - top left, aligned with delete button */} {(feature.priority || (feature.skipTests && !feature.error && feature.status === "backlog")) && (
{/* Priority badge */} {feature.priority && (
{feature.priority === 1 ? "H" : feature.priority === 2 ? "M" : "L"}

{feature.priority === 1 ? "High Priority" : feature.priority === 2 ? "Medium Priority" : "Low Priority"}

)} {/* Manual verification badge */} {feature.skipTests && !feature.error && feature.status === "backlog" && (

Manual verification required

)}
)} {isCurrentAutoTask && (
{feature.startedAt && ( )}
{ e.stopPropagation(); onEdit(); }} data-testid={`edit-running-${feature.id}`} className="text-xs" > Edit {/* Model info in dropdown */}
{formatModelName(feature.model ?? DEFAULT_MODEL)}
)} {!isCurrentAutoTask && feature.status === "backlog" && (
)} {!isCurrentAutoTask && (feature.status === "waiting_approval" || feature.status === "verified") && ( <>
{onViewOutput && ( )}
)} {!isCurrentAutoTask && feature.status === "in_progress" && ( <>
{ e.stopPropagation(); onEdit(); }} data-testid={`edit-feature-${feature.id}`} className="text-xs" > Edit {onViewOutput && ( { e.stopPropagation(); onViewOutput(); }} data-testid={`view-logs-${feature.id}`} className="text-xs" > View Logs )} {/* Model info in dropdown */}
{formatModelName(feature.model ?? DEFAULT_MODEL)}
)}
{isDraggable && (
)}
{feature.titleGenerating ? (
Generating title...
) : feature.title ? ( {feature.title} ) : null} {feature.description || feature.summary || feature.id} {(feature.description || feature.summary || "").length > 100 && ( )}
{/* Target Branch Display */} {useWorktrees && feature.branchName && (
{feature.branchName}
)} {/* PR URL Display */} {typeof feature.prUrl === "string" && /^https?:\/\//i.test(feature.prUrl) && (() => { const prNumber = feature.prUrl.split("/").pop(); return ( ); })()} {/* Steps Preview */} {showSteps && feature.steps && feature.steps.length > 0 && (
{feature.steps.slice(0, 3).map((step, index) => (
{feature.status === "verified" ? ( ) : ( )} {step}
))} {feature.steps.length > 3 && (

+{feature.steps.length - 3} more

)}
)} {/* Model/Preset Info for Backlog Cards */} {showAgentInfo && feature.status === "backlog" && (
{formatModelName(feature.model ?? DEFAULT_MODEL)}
{feature.thinkingLevel && feature.thinkingLevel !== "none" && (
{formatThinkingLevel(feature.thinkingLevel)}
)}
)} {/* Agent Info Panel */} {showAgentInfo && feature.status !== "backlog" && agentInfo && (
{/* Model & Phase */}
{formatModelName(feature.model ?? DEFAULT_MODEL)}
{agentInfo.currentPhase && (
{agentInfo.currentPhase}
)}
{/* Task List Progress */} {agentInfo.todos.length > 0 && (
{ agentInfo.todos.filter((t) => t.status === "completed") .length } /{agentInfo.todos.length} tasks
{agentInfo.todos.slice(0, 3).map((todo, idx) => (
{todo.status === "completed" ? ( ) : todo.status === "in_progress" ? ( ) : ( )} {todo.content}
))} {agentInfo.todos.length > 3 && (

+{agentInfo.todos.length - 3} more

)}
)} {/* Summary for waiting_approval and verified */} {(feature.status === "waiting_approval" || feature.status === "verified") && ( <> {(feature.summary || summary || agentInfo.summary) && (
Summary

{feature.summary || summary || agentInfo.summary}

)} {!feature.summary && !summary && !agentInfo.summary && agentInfo.toolCallCount > 0 && (
{agentInfo.toolCallCount} tool calls {agentInfo.todos.length > 0 && ( { agentInfo.todos.filter( (t) => t.status === "completed" ).length }{" "} tasks done )}
)} )}
)} {/* Actions */}
{isCurrentAutoTask && ( <> {/* Approve Plan button - PRIORITY: shows even when agent is "running" (paused for approval) */} {feature.planSpec?.status === "generated" && onApprovePlan && ( )} {onViewOutput && ( )} {onForceStop && ( )} )} {!isCurrentAutoTask && feature.status === "in_progress" && ( <> {/* Approve Plan button - shows when plan is generated and waiting for approval */} {feature.planSpec?.status === "generated" && onApprovePlan && ( )} {feature.skipTests && onManualVerify ? ( ) : hasContext && onResume ? ( ) : onVerify ? ( ) : null} {onViewOutput && !feature.skipTests && ( )} )} {!isCurrentAutoTask && feature.status === "verified" && ( <> {/* Logs button */} {onViewOutput && ( )} {/* Complete button */} {onComplete && ( )} )} {!isCurrentAutoTask && feature.status === "waiting_approval" && ( <> {/* Refine prompt button */} {onFollowUp && ( )} {/* Show Verify button if PR was created (changes are committed), otherwise show Commit button */} {feature.prUrl && onManualVerify ? ( ) : onCommit ? ( ) : null} )} {!isCurrentAutoTask && feature.status === "backlog" && ( <> {feature.planSpec?.content && onViewPlan && ( )} {onImplement && ( )} )}
{/* Delete Confirmation Dialog */} {/* Summary Modal */} Implementation Summary {(() => { const displayText = feature.description || feature.summary || "No description"; return displayText.length > 100 ? `${displayText.slice(0, 100)}...` : displayText; })()}
{feature.summary || summary || agentInfo?.summary || "No summary available"}
); // Wrap with animated border when in progress if (isCurrentAutoTask) { return
{cardElement}
; } return cardElement; });