"use client"; import { useState, useEffect, 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 { 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, Undo2, GitMerge, ChevronDown, ChevronUp, Brain, } from "lucide-react"; import { CountUpTimer } from "@/components/ui/count-up-timer"; import { getElectronAPI } from "@/lib/electron"; 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; onRevert?: () => void; onMerge?: () => void; hasContext?: boolean; isCurrentAutoTask?: boolean; shortcutKey?: string; /** Context content for extracting progress info */ contextContent?: string; /** Feature summary from agent completion */ summary?: string; } export const KanbanCard = memo(function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, onResume, onForceStop, onManualVerify, onMoveBackToInProgress, onFollowUp, onCommit, onRevert, onMerge, hasContext, isCurrentAutoTask, shortcutKey, contextContent, summary, }: KanbanCardProps) { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false); const [isRevertDialogOpen, setIsRevertDialogOpen] = useState(false); const [agentInfo, setAgentInfo] = useState(null); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); const { kanbanCardDetailLevel } = useAppStore(); // Check if feature has worktree const hasWorktree = !!feature.branchName; // Helper functions to check what should be shown based on detail level const showSteps = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed"; const showAgentInfo = kanbanCardDetailLevel === "detailed"; // Load context file for in_progress, waiting_approval, and verified features useEffect(() => { const loadContext = async () => { // Use provided context or load from file if (contextContent) { const info = parseAgentContext(contextContent); setAgentInfo(info); return; } // Only load for non-backlog features 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; // Use features API to get agent output 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 { // Fallback to direct file read for backward compatibility 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 { // Context file might not exist console.debug("[KanbanCard] No context file for feature:", feature.id); } }; loadContext(); // Reload context periodically while feature is running 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 = () => { setIsDeleteDialogOpen(false); onDelete(); }; const handleCancelDelete = () => { setIsDeleteDialogOpen(false); }; // Dragging logic: // - Backlog items can always be dragged // - skipTests items can be dragged even when in_progress or verified (unless currently running) // - waiting_approval items can always be dragged (to allow manual verification via drag) // - verified items can always be dragged (to allow moving back to waiting_approval or backlog) // - Non-skipTests (TDD) items in progress cannot be dragged (they are running) const isDraggable = feature.status === "backlog" || feature.status === "waiting_approval" || feature.status === "verified" || (feature.skipTests && !isCurrentAutoTask); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: feature.id, disabled: !isDraggable, }); const style = { transform: CSS.Transform.toString(transform), transition, }; return ( {/* Skip Tests indicator badge */} {feature.skipTests && !feature.error && (
Manual
)} {/* Error indicator badge */} {feature.error && (
Errored
)} {/* Just Finished indicator badge - shows when agent just completed work */} {feature.justFinished && feature.status === "waiting_approval" && !feature.error && (
Done
)} {/* Branch badge - show when feature has a worktree */} {hasWorktree && !isCurrentAutoTask && (
{feature.branchName?.replace("feature/", "")}

{feature.branchName}

)} {isCurrentAutoTask && (
{formatModelName(feature.model ?? DEFAULT_MODEL)} {feature.startedAt && ( )}
)} {!isCurrentAutoTask && (
{ e.stopPropagation(); onEdit(); }} data-testid={`edit-feature-${feature.id}`} > Edit {onViewOutput && feature.status !== "backlog" && ( { e.stopPropagation(); onViewOutput(); }} data-testid={`view-logs-${feature.id}`} > Logs )} { e.stopPropagation(); handleDeleteClick(e as unknown as React.MouseEvent); }} data-testid={`delete-feature-${feature.id}`} > Delete
)}
{isDraggable && (
)}
{feature.description || feature.summary || feature.id} {/* Show More/Less toggle - only show when description is likely truncated */} {(feature.description || feature.summary || "").length > 100 && ( )} {feature.category}
{/* Steps Preview - Show in Standard and Detailed modes */} {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 steps

)}
)} {/* Model/Preset Info for Backlog Cards - Show in Detailed mode */} {showAgentInfo && feature.status === "backlog" && (
{formatModelName(feature.model ?? DEFAULT_MODEL)}
{feature.thinkingLevel && feature.thinkingLevel !== "none" && (
{formatThinkingLevel(feature.thinkingLevel)}
)}
)} {/* Agent Info Panel - shows for in_progress, waiting_approval, verified */} {/* Detailed mode: Show all agent info */} {showAgentInfo && feature.status !== "backlog" && agentInfo && (
{/* Model & Phase */}
{formatModelName(feature.model ?? DEFAULT_MODEL)}
{agentInfo.currentPhase && (
{agentInfo.currentPhase}
)}
{/* Task List Progress (if todos found) */} {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 - prioritize feature.summary from UpdateFeatureStatus */} {(feature.status === "waiting_approval" || feature.status === "verified") && ( <> {(feature.summary || summary || agentInfo.summary) && (
Summary

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

)} {/* Show tool count even without 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 && ( <> {onViewOutput && ( )} {onForceStop && ( )} )} {!isCurrentAutoTask && feature.status === "in_progress" && ( <> {/* skipTests features show manual verify button */} {feature.skipTests && onManualVerify ? ( ) : hasContext && onResume ? ( ) : onVerify ? ( ) : null} {onViewOutput && !feature.skipTests && ( )} )} {!isCurrentAutoTask && feature.status === "verified" && ( <> {/* Logs button if context exists */} {hasContext && onViewOutput && ( )} )} {!isCurrentAutoTask && feature.status === "waiting_approval" && ( <> {/* Revert button - only show when worktree exists (icon only to save space) */} {hasWorktree && onRevert && (

Revert changes

)} {/* Follow-up prompt button */} {onFollowUp && ( )} {/* Merge button - only show when worktree exists */} {hasWorktree && onMerge && ( )} {/* Commit and verify button - show when no worktree */} {!hasWorktree && onCommit && ( )} )}
{/* Delete Confirmation Dialog */} Delete Feature Are you sure you want to delete this feature? This action cannot be undone. Delete {/* 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"}
{/* Revert Confirmation Dialog */} Revert Changes This will discard all changes made by the agent and move the feature back to the backlog. {feature.branchName && ( Branch {feature.branchName} will be deleted. )} This action cannot be undone.
); });