"use client"; import { memo } from "react"; import { IconChevronDown, IconCircleCheck, IconCircleDashed, IconDots, IconGitPullRequest, IconMessageQuestion, } from "@tabler/icons-react"; import { PRTaskIcon } from "@/components/github/pr-task-icon"; import { IssueTaskIcon } from "@/components/github/issue-task-icon"; import { useAppStore } from "@/components/state-provider"; import { cn } from "@/lib/utils"; import { DEBUG_UI } from "@/lib/config"; import type { TaskState, TaskSessionState } from "@/lib/types/http"; import { shouldUseQuestionTaskIcon } from "@/lib/ui/state-icons"; import type { SessionPollMode } from "@/lib/state/slices/session-runtime/types"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { RemoteCloudTooltip } from "./remote-cloud-tooltip"; import { classifyTask } from "./task-classify"; import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow"; type DiffStats = { additions: number; deletions: number; }; type TaskItemProps = { title: string; state?: TaskState; sessionState?: TaskSessionState; isArchived?: boolean; isSelected?: boolean; onClick?: () => void; diffStats?: DiffStats; isRemoteExecutor?: boolean; remoteExecutorType?: string; remoteExecutorName?: string; updatedAt?: string; menuOpen?: boolean; isDeleting?: boolean; taskId?: string; primarySessionId?: string | null; hasPendingClarification?: boolean; parentTaskTitle?: string; isSubTask?: boolean; /** Number of subtasks under this parent task. Only set for parent rows. */ subtaskCount?: number; /** Whether the subtasks of this parent are currently hidden. */ subtasksCollapsed?: boolean; /** Toggles subtask visibility when the chevron is clicked. */ onToggleSubtasks?: () => void; repositories?: string[]; prInfo?: { number: number; state: string }; issueInfo?: { url: string; number: number }; }; function formatRelativeTime(dateString: string): string { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffSecs < 60) 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(); } // Delegates to the shared classifier in task-switcher so the sidebar bucket // and the per-task running spinner always agree. A task whose workflow state // is REVIEW or COMPLETED must not render as "running" when its session // transiently cycles through STARTING/RUNNING (e.g. during an agent auto- // resume after a backend restart). function computeIsInProgress(state?: TaskState, sessionState?: TaskSessionState): boolean { return classifyTask(sessionState, state) === "in_progress"; } function handleTaskItemKeyDown(e: React.KeyboardEvent, onClick?: () => void): void { if (e.key !== "Enter" && e.key !== " ") return; e.preventDefault(); onClick?.(); } function TaskStateIcon({ sessionState, state, isInProgress, hasPendingClarification, }: { sessionState?: TaskSessionState; state?: TaskState; isInProgress: boolean; hasPendingClarification?: boolean; }) { if (shouldUseQuestionTaskIcon(state, hasPendingClarification)) { return ( ); } if (isInProgress) { return ( ); } if (classifyTask(sessionState, state) === "review") { return ( ); } return ( ); } const POLL_MODE_CONFIG: Record = { fast: { letter: "F", color: "text-emerald-500", label: "focused, 2s polling" }, slow: { letter: "S", color: "text-yellow-500", label: "subscribed, 30s polling" }, paused: { letter: "P", color: "text-muted-foreground/40", label: "no subscribers" }, }; function TaskItemStatsRow({ updatedAt, prInfo, primarySessionId, }: { updatedAt?: string; prInfo?: { number: number; state: string }; primarySessionId?: string | null; }) { const pollMode = useAppStore((s) => DEBUG_UI && primarySessionId ? (s.sessionPollMode.bySessionId[primarySessionId] ?? null) : null, ); if (!updatedAt && !prInfo && !pollMode) return null; const modeConfig = pollMode ? POLL_MODE_CONFIG[pollMode] : null; return ( {updatedAt && ( {formatRelativeTime(updatedAt)} )} {prInfo && #{prInfo.number}} {modeConfig && ( {modeConfig.letter} Git poll: {pollMode} ({modeConfig.label}) )} ); } function DiffStatsRight({ diffStats, menuOpen }: { diffStats: DiffStats; menuOpen: boolean }) { return (
+{diffStats.additions}{" "} -{diffStats.deletions}
); } /** Shows PR icon from store (real data) or from prInfo prop (prototype/mock). */ function TaskPRIcon({ taskId, prInfo, }: { taskId?: string; prInfo?: { number: number; state: string }; }) { const hasStorePR = useAppStore((s) => !!taskId && (s.taskPRs.byTaskId[taskId]?.length ?? 0) > 0); if (hasStorePR) return ; if (!prInfo) return null; const state = prInfo.state.toLowerCase(); let color = "text-muted-foreground"; if (state === "merged") color = "text-purple-500"; else if (state === "closed") color = "text-red-500"; return ( ); } function TaskItemContent({ title, taskId, isRemoteExecutor, remoteExecutorType, remoteExecutorName, primarySessionId, isArchived, repositories, updatedAt, prInfo, issueInfo, reserveMenuSpace, }: { title: string; taskId?: string; isRemoteExecutor?: boolean; remoteExecutorType?: string; remoteExecutorName?: string; primarySessionId?: string | null; isArchived?: boolean; repositories?: string[]; updatedAt?: string; prInfo?: { number: number; state: string }; issueInfo?: { url: string; number: number }; reserveMenuSpace: boolean; }) { return (
{title} {issueInfo && } {isRemoteExecutor && ( )} {isArchived && ( Archived )} {repositories && repositories.length > 1 && ( {repositories.join(" · ")} )}
); } export const TaskItem = memo(function TaskItem({ title, state, sessionState, isArchived, isSelected = false, onClick, diffStats, isRemoteExecutor, remoteExecutorType, remoteExecutorName, updatedAt, menuOpen = false, isDeleting, taskId, primarySessionId, hasPendingClarification, isSubTask, subtaskCount, subtasksCollapsed, onToggleSubtasks, repositories, prInfo, issueInfo, }: TaskItemProps) { const effectiveMenuOpen = menuOpen || isDeleting === true; const isInProgress = computeIsInProgress(state, sessionState); const hasDiffStats = !!diffStats && (diffStats.additions > 0 || diffStats.deletions > 0); const showSubtaskToggle = !!subtaskCount && subtaskCount > 0 && !!onToggleSubtasks; return (
handleTaskItemKeyDown(e, onClick)} className={cn( "group relative flex w-full items-start gap-2 py-2 pr-3 text-left text-sm outline-none cursor-pointer", "transition-colors duration-75 hover:bg-foreground/[0.05]", isSelected && "bg-primary/10", isSubTask ? "pl-8" : "pl-3", )} >
{isSubTask && ( ↳ )} {showSubtaskToggle && ( )} {hasDiffStats && }
); }); function SubtaskToggle({ taskId, count, collapsed, onToggle, }: { taskId?: string; count: number; collapsed: boolean; onToggle: () => void; }) { return ( ); } function TaskMenuButton({ visible, shiftLeft }: { visible: boolean; shiftLeft?: boolean }) { return (
); }