import type { CSSProperties, ComponentProps, HTMLAttributes } from "react"; import type { GraphPosition } from "./types.js"; import { useNodeDrag } from "./hooks/useNodeDrag.js"; import { TaskCard } from "@fusion/dashboard/app/components/TaskCard"; import { isTaskStuck } from "@fusion/dashboard/app/utils/taskStuck"; import "./GraphTaskNode.css"; import "./GraphHighlight.css"; import "./styles/drag.css"; type TaskCardComponentProps = ComponentProps; type TaskCardBridgeProps = Pick< TaskCardComponentProps, | "task" | "projectId" | "onOpenDetail" | "addToast" | "globalPaused" | "onUpdateTask" | "onArchiveTask" | "onUnarchiveTask" | "onDeleteTask" | "onRetryTask" | "onOpenDetailWithTab" | "taskStuckTimeoutMs" | "onOpenMission" | "onMoveTask" | "lastFetchTimeMs" | "workflowStepNameLookup" >; export interface GraphTaskNodeProps extends TaskCardBridgeProps, Pick, "onMouseEnter" | "onMouseLeave" | "onClick"> { style?: CSSProperties; position: GraphPosition; scale: number; isSelected?: boolean; isHighlighted?: boolean; isDimmed?: boolean; onNodePositionChange: (taskId: string, position: GraphPosition) => void; onNodeDragStateChange?: (isDragging: boolean) => void; onNodeDragEnd?: () => void; } const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]); function getStatusLabel(status?: string): string { if (!status) { return "Executing"; } return status.charAt(0).toUpperCase() + status.slice(1); } export function GraphTaskNode({ style, position, scale, isSelected = false, isHighlighted = false, isDimmed = false, onMouseEnter, onMouseLeave, onClick, onNodePositionChange, onNodeDragStateChange, onNodeDragEnd, ...taskCardProps }: GraphTaskNodeProps) { const { task, globalPaused, taskStuckTimeoutMs, lastFetchTimeMs, onOpenDetail } = taskCardProps; const isFailed = task.status === "failed"; const isPaused = task.paused === true; const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs); const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval"; const isActive = !globalPaused && !isFailed && !isPaused && !isStuck && !isAwaitingApproval && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string)); const hasValidCurrentStep = typeof task.currentStep === "number" && task.currentStep >= 0 && Array.isArray(task.steps) && task.currentStep < task.steps.length; const isInReview = task.column === "in-review"; const drag = useNodeDrag({ taskId: task.id, position, scale, canDrag: isSelected, onPositionChange: onNodePositionChange, onDragStateChange: onNodeDragStateChange, onDragEnd: onNodeDragEnd, onDoubleTap: () => onOpenDetail(task), }); return (
{ if (event.defaultPrevented) { return; } onOpenDetail(task); }} onClickCapture={drag.onClickCapture} onPointerDown={drag.onPointerDown} onPointerMove={drag.onPointerMove} onPointerUp={drag.onPointerUp} onPointerCancel={drag.onPointerCancel} > {isActive ? (
{getStatusLabel(task.status)}
) : null} {}} disableDrag={true} />
); }