"use client"; import { memo, useMemo } from "react"; import { IconChevronDown } from "@tabler/icons-react"; import type { TaskState, TaskSessionState } from "@/lib/types/http"; import { cn } from "@/lib/utils"; import { TaskItem } from "./task-item"; import { TaskItemWithContextMenu, type StepDef } from "./task-switcher-context-menu"; import { countGroupTasks, type GroupedSidebarList, type SidebarGroup, } from "@/lib/sidebar/apply-view"; import { type TaskMoveWorkflow } from "@/components/task/task-move-context-menu"; import { SortableTaskLevel, SortableTaskNode } from "./task-switcher-subtask-dnd"; 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; hasPendingPermission?: boolean; parentTaskTitle?: string; parentTaskId?: string; prInfo?: { number: number; state: string }; isPRReview?: boolean; isIssueWatch?: boolean; issueInfo?: { url: string; number: number }; agentErrorMessage?: string | null; }; type TaskSwitcherProps = { grouped: GroupedSidebarList; workflows?: TaskMoveWorkflow[]; 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; onLinkPullRequest?: (taskId: string) => void; onLinkIssue?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; onTogglePin?: (taskId: string) => void; onReorderGroup?: (groupTaskIds: string[]) => void; onReorderSubtasks?: (parentTaskId: string, orderedSubtaskIds: string[]) => void; pinnedTaskIds?: string[]; 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; depth?: number; subtaskToggle?: SubtaskToggleInfo; workflows?: TaskMoveWorkflow[]; 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; onLinkPullRequest?: (taskId: string) => void; onLinkIssue?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; onTogglePin?: (taskId: string) => void; isPinned?: boolean; deletingTaskId?: string | null; }; function TaskRow({ task, isSubTask, depth, subtaskToggle, workflows, stepsByWorkflowId, activeTaskId, selectedTaskId, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onLinkPullRequest, onLinkIssue, onMoveToStep, onTogglePin, isPinned, 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} isPinned={isPinned} /> ); } // Shared, per-render context threaded through the recursive task tree so each // node can look up its children, collapse state, and reorder callbacks without // drilling a dozen props through every level. type TaskTreeContext = { subTasksByParentId: Map; collapsedSubs: Set; onToggleSubtasks?: (parentTaskId: string) => void; pinnedSet: Set; rowProps: Omit; onReorderGroup?: (groupTaskIds: string[]) => void; onReorderSubtasks?: (parentTaskId: string, orderedSubtaskIds: string[]) => void; }; // One task row plus — when expanded — its nested subtree. Mutually recursive // with TaskTreeLevel, so it renders arbitrarily deep hierarchies. function TaskTreeNode({ task, depth, ctx, isDraggable, }: { task: TaskSwitcherItem; depth: number; ctx: TaskTreeContext; isDraggable: boolean; }) { const subs = ctx.subTasksByParentId.get(task.id); const hasSubs = !!subs?.length; const subsHidden = hasSubs && !!ctx.onToggleSubtasks && ctx.collapsedSubs.has(task.id); const toggleInfo: SubtaskToggleInfo | undefined = hasSubs && ctx.onToggleSubtasks ? { subtaskCount: countGroupTasks(subs!, ctx.subTasksByParentId), subtasksCollapsed: subsHidden, onToggleSubtasks: () => ctx.onToggleSubtasks!(task.id), } : undefined; const isRoot = depth === 0; const handle = ( ); const nested = !subsHidden && hasSubs ? ( ) : undefined; return ( ); } // One level of sibling tasks. `parentTaskId === null` is the group root (whose // reorder maps to onReorderGroup); deeper levels reorder via onReorderSubtasks // scoped to that parent's children. function TaskTreeLevel({ parentTaskId, tasks, depth, ctx, }: { parentTaskId: string | null; tasks: TaskSwitcherItem[]; depth: number; ctx: TaskTreeContext; }) { let onReorder: ((orderedTaskIds: string[]) => void) | undefined; if (parentTaskId === null) { onReorder = ctx.onReorderGroup; } else if (ctx.onReorderSubtasks) { const pid = parentTaskId; onReorder = (ids: string[]) => ctx.onReorderSubtasks!(pid, ids); } return ( ( )} /> ); } type GroupSectionProps = { group: SidebarGroup; subTasksByParentId: Map; workflows?: TaskMoveWorkflow[]; 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; onLinkPullRequest?: (taskId: string) => void; onLinkIssue?: (taskId: string) => void; onMoveToStep?: (taskId: string, workflowId: string, targetStepId: string) => void; onTogglePin?: (taskId: string) => void; onReorderGroup?: (groupTaskIds: string[]) => void; onReorderSubtasks?: (parentTaskId: string, orderedSubtaskIds: string[]) => void; pinnedSet: Set; deletingTaskId?: string | null; }; function GroupSection({ group, subTasksByParentId, workflows, stepsByWorkflowId, activeTaskId, selectedTaskId, isCollapsed, onToggleCollapsed, collapsedSubtaskParentIds, onToggleSubtasks, showHeader, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onLinkPullRequest, onLinkIssue, onMoveToStep, onTogglePin, onReorderGroup, onReorderSubtasks, pinnedSet, deletingTaskId, }: GroupSectionProps) { const totalCount = countGroupTasks(group.tasks, subTasksByParentId); const ctx: TaskTreeContext = { subTasksByParentId, collapsedSubs: new Set(collapsedSubtaskParentIds ?? []), onToggleSubtasks, pinnedSet, rowProps: { workflows, stepsByWorkflowId, activeTaskId, selectedTaskId, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onLinkPullRequest, onLinkIssue, onMoveToStep, onTogglePin, deletingTaskId, }, onReorderGroup, onReorderSubtasks, }; return (
{showHeader && ( )} {!isCollapsed && ( )}
); } export const TaskSwitcher = memo(function TaskSwitcher({ grouped, workflows, stepsByWorkflowId, activeTaskId, selectedTaskId, collapsedGroupKeys = [], onToggleGroup, collapsedSubtaskParentIds, onToggleSubtasks, onSelectTask, onRenameTask, onArchiveTask, onDeleteTask, onLinkPullRequest, onLinkIssue, onMoveToStep, onTogglePin, onReorderGroup, onReorderSubtasks, pinnedTaskIds, deletingTaskId, isLoading = false, totalTaskCount, }: TaskSwitcherProps) { const pinnedSet = useMemo(() => new Set(pinnedTaskIds ?? []), [pinnedTaskIds]); 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} onLinkPullRequest={onLinkPullRequest} onLinkIssue={onLinkIssue} onMoveToStep={onMoveToStep} onTogglePin={onTogglePin} onReorderGroup={onReorderGroup} onReorderSubtasks={onReorderSubtasks} pinnedSet={pinnedSet} deletingTaskId={deletingTaskId} /> ))}
); });