"use client"; import { cloneElement, isValidElement, memo, useState } from "react"; import { IconChevronDown, IconArrowRight, IconPencil, IconCopy, IconArchive, IconTrash, IconLoader, } from "@tabler/icons-react"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@kandev/ui/context-menu"; import type { TaskState, TaskSessionState } from "@/lib/types/http"; import { cn } from "@/lib/utils"; import { TaskItem } from "./task-item"; import type { GroupedSidebarList, SidebarGroup } from "@/lib/sidebar/apply-view"; export type TaskSwitcherItem = { id: string; title: string; state?: TaskState; sessionState?: TaskSessionState; description?: string; workflowId?: string; workflowName?: string; workflowStepId?: string; workflowStepTitle?: string; repositoryPath?: string; repositories?: string[]; diffStats?: { additions: number; deletions: number }; isRemoteExecutor?: boolean; remoteExecutorType?: string; remoteExecutorName?: string; updatedAt?: string; createdAt?: string; isArchived?: boolean; primarySessionId?: string | null; hasPendingClarification?: boolean; parentTaskTitle?: string; parentTaskId?: string; prInfo?: { number: number; state: string }; isPRReview?: boolean; isIssueWatch?: boolean; issueInfo?: { url: string; number: number }; }; type StepDef = { id: string; title: string; color?: string }; type TaskSwitcherProps = { grouped: GroupedSidebarList; stepsByWorkflowId?: Record; activeTaskId: string | null; selectedTaskId: string | null; collapsedGroupKeys?: string[]; onToggleGroup?: (groupKey: string) => void; collapsedSubtaskParentIds?: string[]; onToggleSubtasks?: (parentTaskId: string) => void; onSelectTask: (taskId: string) => void; onRenameTask?: (taskId: string, currentTitle: string) => void; onArchiveTask?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; deletingTaskId?: string | null; isLoading?: boolean; totalTaskCount?: number; }; type SubtaskToggleInfo = { subtaskCount: number; subtasksCollapsed: boolean; onToggleSubtasks: () => void; }; function TaskSwitcherSkeleton() { return (
); } function GroupHeader({ label, groupKey, count, isCollapsed, onToggle, }: { label: string; groupKey: string; count: number; isCollapsed: boolean; onToggle: () => void; }) { return ( ); } type TaskRowProps = { task: TaskSwitcherItem; isSubTask?: boolean; subtaskToggle?: SubtaskToggleInfo; stepsByWorkflowId?: Record; activeTaskId: string | null; selectedTaskId: string | null; onSelectTask: (taskId: string) => void; onRenameTask?: (taskId: string, currentTitle: string) => void; onArchiveTask?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; deletingTaskId?: string | null; }; function TaskRow({ task, isSubTask, subtaskToggle, stepsByWorkflowId, activeTaskId, selectedTaskId, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onMoveToStep, deletingTaskId, }: TaskRowProps) { const isSelected = task.id === selectedTaskId || task.id === activeTaskId; const taskSteps = task.workflowId ? stepsByWorkflowId?.[task.workflowId] : undefined; return ( onSelectTask(task.id)} isDeleting={deletingTaskId === task.id} /> ); } function GroupSection({ group, subTasksByParentId, stepsByWorkflowId, activeTaskId, selectedTaskId, isCollapsed, onToggleCollapsed, collapsedSubtaskParentIds, onToggleSubtasks, showHeader, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onMoveToStep, deletingTaskId, }: { group: SidebarGroup; subTasksByParentId: Map; stepsByWorkflowId?: Record; activeTaskId: string | null; selectedTaskId: string | null; isCollapsed: boolean; onToggleCollapsed: () => void; collapsedSubtaskParentIds?: string[]; onToggleSubtasks?: (parentTaskId: string) => void; showHeader: boolean; onSelectTask: (taskId: string) => void; onRenameTask?: (taskId: string, currentTitle: string) => void; onArchiveTask?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; deletingTaskId?: string | null; }) { const totalCount = group.tasks.reduce( (sum, t) => sum + 1 + (subTasksByParentId.get(t.id)?.length ?? 0), 0, ); const collapsedSubs = new Set(collapsedSubtaskParentIds ?? []); const rowProps = { stepsByWorkflowId, activeTaskId, selectedTaskId, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onMoveToStep, deletingTaskId, }; return (
{showHeader && ( )} {!isCollapsed && group.tasks.map((task) => { const subs = subTasksByParentId.get(task.id); const hasSubs = !!subs?.length; const subsHidden = hasSubs && !!onToggleSubtasks && collapsedSubs.has(task.id); const toggleInfo: SubtaskToggleInfo | undefined = hasSubs && onToggleSubtasks ? { subtaskCount: subs!.length, subtasksCollapsed: subsHidden, onToggleSubtasks: () => onToggleSubtasks(task.id), } : undefined; return (
{!subsHidden && subs?.map((sub) => )}
); })}
); } function TaskItemWithContextMenu({ task, steps, children, onRenameTask, onArchiveTask, onDeleteTask, onMoveToStep, isDeleting, }: { task: TaskSwitcherItem; steps?: StepDef[]; children: React.ReactElement<{ menuOpen?: boolean }>; onRenameTask?: (taskId: string, currentTitle: string) => void; onArchiveTask?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; isDeleting?: boolean; }) { const [contextOpen, setContextOpen] = useState(false); const [menuKey, setMenuKey] = useState(0); const menuOpen = contextOpen || isDeleting === true; return (
{cloneWithMenuOpen(children, menuOpen)}
{onRenameTask && ( onRenameTask(task.id, task.title)}> Rename )} Duplicate {onArchiveTask && ( onArchiveTask(task.id)}> Archive )} {onMoveToStep && task.workflowId && steps && steps.length > 0 && ( { setContextOpen(false); setMenuKey((k) => k + 1); onMoveToStep(task.id, task.workflowId!, stepId); }} /> )} {onDeleteTask && ( <> onDeleteTask(task.id)} > {isDeleting ? ( ) : ( )} Delete )}
); } function MoveToStepSubmenu({ steps, currentStepId, disabled, onSelect, }: { steps: StepDef[]; currentStepId?: string; disabled?: boolean; onSelect: (stepId: string) => void; }) { return ( <> Move to {steps.map((step) => ( { e.preventDefault(); onSelect(step.id); }} > {step.title} {step.id === currentStepId && ( Current )} ))} ); } function cloneWithMenuOpen( children: React.ReactElement<{ menuOpen?: boolean }>, menuOpen: boolean, ): React.ReactNode { if (isValidElement(children)) return cloneElement(children, { menuOpen }); return children; } export const TaskSwitcher = memo(function TaskSwitcher({ grouped, stepsByWorkflowId, activeTaskId, selectedTaskId, collapsedGroupKeys = [], onToggleGroup, collapsedSubtaskParentIds, onToggleSubtasks, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onMoveToStep, deletingTaskId, isLoading = false, totalTaskCount, }: TaskSwitcherProps) { if (isLoading) return ; const totalTasks = totalTaskCount ?? grouped.groups.reduce((sum, g) => sum + g.tasks.length, 0); if (totalTasks === 0) { return
No tasks yet.
; } const collapsedSet = new Set(collapsedGroupKeys); const showHeaders = grouped.groups.length > 1 || (grouped.groups.length === 1 && grouped.groups[0].key !== "__all__"); return (
{grouped.groups.map((group) => ( onToggleGroup?.(group.key)} collapsedSubtaskParentIds={collapsedSubtaskParentIds} onToggleSubtasks={onToggleSubtasks} showHeader={showHeaders} onSelectTask={onSelectTask} onRenameTask={onRenameTask} onArchiveTask={onArchiveTask} onDeleteTask={onDeleteTask} onMoveToStep={onMoveToStep} deletingTaskId={deletingTaskId} /> ))}
); });