import { memo, useCallback, useState } from "react"; import { Play, Pause, AlertCircle, Loader2, MoreHorizontal, Trash2, Folder, ArrowRight } from "lucide-react"; import type { RegisteredProject, ProjectHealth, ProjectStatus } from "@fusion/core"; import type { NodeInfo } from "../api"; export interface ProjectCardProps { project: RegisteredProject; health: ProjectHealth | null; onSelect: (project: RegisteredProject) => void; onPause: (project: RegisteredProject) => void; onResume: (project: RegisteredProject) => void; onRemove: (project: RegisteredProject) => void; node?: NodeInfo; 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(--color-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}`; } 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; if ( prevHealth.activeTaskCount !== nextHealth.activeTaskCount || prevHealth.inFlightAgentCount !== nextHealth.inFlightAgentCount || prevHealth.totalTasksCompleted !== nextHealth.totalTasksCompleted || prevHealth.totalTasksFailed !== nextHealth.totalTasksFailed || prevHealth.status !== nextHealth.status ) { return false; } const prevNode = previous.node; const nextNode = next.node; if (!prevNode && !nextNode) return true; if (!prevNode || !nextNode) return false; return ( prevNode.id === nextNode.id && prevNode.name === nextNode.name && prevNode.status === nextNode.status && prevNode.type === nextNode.type ); } function ProjectCardInner({ project, health, onSelect, onPause, onResume, onRemove, node, isLoading = false, }: ProjectCardProps) { const [removeArmed, setRemoveArmed] = useState(false); 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(); if (!removeArmed) { setRemoveArmed(true); return; } onRemove(project); setRemoveArmed(false); }, [removeArmed, 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}

{node && ( on: {node.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);