import { memo } from "react"; import { useTranslation } from "react-i18next"; import type { Task, TaskDetail, MergeResult, GithubIssueAction, ColumnId } from "@fusion/core"; import { isNearDuplicateCanonicalInactive } from "../../../core/src/near-duplicate-canonical"; import { ClipboardList, GitBranch } from "lucide-react"; import { TaskCard } from "./TaskCard"; import type { ToastType } from "../hooks/useToast"; import type { RevertTaskOptions, RevertTaskResult } from "../api"; import type { BlockerFanoutEntry } from "../hooks/useBlockerFanout"; import type { TaskContextMenuColumnMetadata } from "./TaskContextMenu"; interface WorktreeGroupProps { label: string; activeTasks: Task[]; queuedTasks: Task[]; allTasks?: Task[]; projectId?: string; onOpenDetail: (task: Task | TaskDetail) => void; onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void; workflowId?: string | null; onOpenRefine?: (task: Task | TaskDetail) => void; onMoveTask?: (id: string, column: ColumnId, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise; addToast: (message: string, type?: ToastType) => void; globalPaused?: boolean; onUpdateTask?: ( id: string, updates: { title?: string; description?: string; dependencies?: string[] } ) => Promise; onPauseTask?: (id: string) => Promise; onRetryTask?: (id: string) => Promise; onUnpauseTask?: (id: string) => Promise; onResetTask?: (id: string) => Promise; onDuplicateTask?: (id: string) => Promise; onMergeTask?: (id: string) => Promise; onArchiveTask?: (id: string, options?: { removeLineageReferences?: boolean }) => Promise; onUnarchiveTask?: (id: string) => Promise; /* FNXC:TaskRevert 2026-07-05-00:00 (FN-7525): threaded alongside onArchiveTask/onUnarchiveTask. */ onRevertTask?: (id: string, body?: RevertTaskOptions) => Promise; onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; removeLineageReferences?: boolean; githubIssueAction?: GithubIssueAction; }) => Promise; onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes" | "retries" | "workflow") => void; /** Project-level stuck task timeout in milliseconds (undefined = disabled) */ taskStuckTimeoutMs?: number; /** Called when user clicks a mission badge on a task card */ onOpenMission?: (missionId: string) => void; /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ lastFetchTimeMs?: number; /** Per-task card-placed custom field definitions (U13/KTD-14). */ taskCardFieldDefs?: ReadonlyMap; /** Trusted aggregate-board workflow badges keyed by task id; omitted in per-workflow and non-board surfaces. */ taskWorkflowBadges?: ReadonlyMap; /** Precomputed blocker fanout keyed by blocker task ID. */ blockerFanoutMap?: ReadonlyMap; /** Whether GitHub CLI auth is available for creating PRs from task cards. */ prAuthAvailable?: boolean; /** Whether project-level auto-merge is enabled, which hides manual Create PR card actions. */ autoMergeEnabled?: boolean; /** Project merge strategy for Task Detail-equivalent card context actions. */ mergeStrategy?: string; /** Ordered workflow columns for deriving context-menu move targets in workflow mode. */ workflowContextMenuColumns?: readonly TaskContextMenuColumnMetadata[]; /** Per-task workflow columns for aggregate Board cards whose tasks come from different workflows. */ taskContextMenuColumnsByTaskId?: ReadonlyMap; } function WorktreeGroupComponent({ label, activeTasks, queuedTasks, allTasks, projectId, onOpenDetail, onPlanningMode, workflowId, onOpenRefine, onMoveTask, addToast, globalPaused, onUpdateTask, onPauseTask, onRetryTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onArchiveTask, onUnarchiveTask, onRevertTask, onDeleteTask, onOpenDetailWithTab, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, taskCardFieldDefs, taskWorkflowBadges, blockerFanoutMap, prAuthAvailable, autoMergeEnabled, mergeStrategy = "direct", workflowContextMenuColumns, taskContextMenuColumnsByTaskId, }: WorktreeGroupProps) { const { t } = useTranslation("app"); const upNextLabel = t("worktree.upNext", "Up Next"); const unassignedLabel = t("worktree.unassigned", "Unassigned"); const resolveNearDuplicateCanonicalInactive = (task: Task): boolean | undefined => { const nearDuplicateOf = task.sourceMetadata?.nearDuplicateOf; if (typeof nearDuplicateOf !== "string" || !allTasks) return undefined; return isNearDuplicateCanonicalInactive(allTasks.find((candidate) => candidate.id === nearDuplicateOf)); }; const getTaskContextMenuColumns = (task: Task) => taskContextMenuColumnsByTaskId?.get(task.id) ?? workflowContextMenuColumns; const getTaskColumnFlags = (task: Task) => getTaskContextMenuColumns(task)?.find((candidate) => candidate.id === task.column)?.flags; const getTaskPlanningWorkflowId = (task: Task) => (task as Task & { workflowId?: string | null }).workflowId ?? taskWorkflowBadges?.get(task.id)?.workflowId ?? workflowId ?? null; return (
{label === upNextLabel || label === unassignedLabel ? : } {label}
{activeTasks.map((task) => ( ))} {queuedTasks.map((task) => ( ))}
); } export const WorktreeGroup = memo(WorktreeGroupComponent); WorktreeGroup.displayName = "WorktreeGroup";