import { memo, useCallback } from "react"; import { Play, Pause, AlertCircle, Loader2, MoreHorizontal, Trash2, Folder, ArrowRight } from "lucide-react"; import type { RegisteredProject, ProjectHealth, ProjectStatus } from "@fusion/core"; export interface ProjectCardProps { project: RegisteredProject; health: ProjectHealth | null; onSelect: (project: RegisteredProject) => void; onPause: (project: RegisteredProject) => void; onResume: (project: RegisteredProject) => void; onRemove: (project: RegisteredProject) => void; isLoading?: boolean; } const STATUS_CONFIG: Record = { active: { label: "Active", color: "var(--success)", icon: Play }, paused: { label: "Paused", color: "var(--warning)", icon: Pause }, errored: { label: "Error", color: "var(--error)", icon: AlertCircle }, initializing: { label: "Initializing", color: "var(--info)", icon: Loader2 }, }; function formatRelativeTime(timestamp: string | undefined): string { if (!timestamp) return "Never"; const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } function truncatePath(path: string, maxLength: number = 40): string { if (path.length <= maxLength) return path; const start = path.slice(0, Math.floor(maxLength / 2) - 2); const end = path.slice(-Math.floor(maxLength / 2) + 2); return `${start}...${end}`; } /** * Compare two sets of ProjectCardProps for memo equality. * Checks project properties and health metrics to determine if re-render is needed. */ function areProjectCardPropsEqual(previous: ProjectCardProps, next: ProjectCardProps): boolean { if (previous.project.id !== next.project.id) return false; if (previous.project.status !== next.project.status) return false; if (previous.project.name !== next.project.name) return false; if (previous.project.path !== next.project.path) return false; if (previous.project.lastActivityAt !== next.project.lastActivityAt) return false; if (previous.isLoading !== next.isLoading) return false; // Compare health const prevHealth = previous.health; const nextHealth = next.health; if (!prevHealth && !nextHealth) return true; if (!prevHealth || !nextHealth) return false; return ( prevHealth.activeTaskCount === nextHealth.activeTaskCount && prevHealth.inFlightAgentCount === nextHealth.inFlightAgentCount && prevHealth.totalTasksCompleted === nextHealth.totalTasksCompleted && prevHealth.totalTasksFailed === nextHealth.totalTasksFailed && prevHealth.status === nextHealth.status ); } /** * Individual project card component showing project status, health metrics, and actions. * * Displays: * - Project name and truncated path * - Status badge (active, paused, errored, initializing) * - Health metrics: active tasks, running agents, completed tasks * - Last activity timestamp * - Action buttons: Open, Pause/Resume, Remove * * @example * ```tsx * setCurrentProject(p)} * onPause={(p) => pauseProject(p.id)} * onResume={(p) => resumeProject(p.id)} * onRemove={(p) => unregisterProject(p.id)} * /> * ``` */ function ProjectCardInner({ project, health, onSelect, onPause, onResume, onRemove, isLoading = false, }: ProjectCardProps) { const statusConfig = STATUS_CONFIG[project.status]; const StatusIcon = statusConfig.icon; const handleSelect = useCallback(() => { onSelect(project); }, [onSelect, project]); const handlePause = useCallback((e: React.MouseEvent) => { e.stopPropagation(); onPause(project); }, [onPause, project]); const handleResume = useCallback((e: React.MouseEvent) => { e.stopPropagation(); onResume(project); }, [onResume, project]); const handleRemove = useCallback((e: React.MouseEvent) => { e.stopPropagation(); onRemove(project); }, [onRemove, project]); const isPaused = project.status === "paused"; const isErrored = project.status === "errored"; const isInitializing = project.status === "initializing"; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleSelect(); } }} >

{project.name}

{truncatePath(project.path)}
{statusConfig.label}
{health && ( <>
{health.activeTaskCount} Active Tasks
{health.inFlightAgentCount} Agents
{health.totalTasksCompleted} Completed
)} {!health && (
No health data available
)}
Last activity: {formatRelativeTime(project.lastActivityAt || health?.lastActivityAt)}
{isPaused ? ( ) : ( )}
); } export const ProjectCard = memo(ProjectCardInner, areProjectCardPropsEqual);