import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { UnreadCommentsBadge } from '@renderer/components/team/UnreadCommentsBadge'; import { Button } from '@renderer/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount'; import { useStore } from '@renderer/store'; import { REVIEW_STATE_DISPLAY, buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildTaskChangePresenceKey, buildTaskChangeRequestOptions, isTaskSummaryCacheableForOptions, } from '@renderer/utils/taskChangeRequest'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { ArrowLeftFromLine, ArrowRightFromLine, CheckCircle2, FileCode, FilePenLine, GitPullRequestArrow, HelpCircle, Play, RotateCcw, Trash2, XCircle, } from 'lucide-react'; import type { KanbanColumnId, KanbanTaskState, ResolvedTeamMember, TeamTask } from '@shared/types'; interface KanbanTaskCardProps { task: TeamTask; teamName: string; columnId: KanbanColumnId; kanbanTaskState?: KanbanTaskState; hasReviewers: boolean; compact?: boolean; taskMap: Map; members: ResolvedTeamMember[]; onRequestReview: (taskId: string) => void; onApprove: (taskId: string) => void; onRequestChanges: (taskId: string) => void; onMoveBackToDone: (taskId: string) => void; onStartTask: (taskId: string) => void; onCompleteTask: (taskId: string) => void; onCancelTask: (taskId: string) => void; onScrollToTask?: (taskId: string) => void; onTaskClick?: (task: TeamTask) => void; onViewChanges?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void; } interface DependencyBadgeProps { taskId: string; taskMap: Map; onScrollToTask?: (taskId: string) => void; } const DependencyBadge = ({ taskId, taskMap, onScrollToTask, }: DependencyBadgeProps): React.JSX.Element => { const depTask = taskMap.get(taskId); const isCompleted = depTask?.status === 'completed'; return ( ); }; const TruncatedTitle = ({ text, className, }: { text: string; className?: string; }): React.JSX.Element => { const ref = useRef(null); const [isTruncated, setIsTruncated] = useState(false); const checkTruncation = useCallback(() => { const el = ref.current; if (el) { setIsTruncated(el.scrollWidth > el.clientWidth); } }, []); return (
{text}
{text}
); }; const CancelTaskButton = ({ taskId, onConfirm, }: { taskId: string; onConfirm: (taskId: string) => void; }): React.JSX.Element => { const [open, setOpen] = useState(false); return ( Cancel e.stopPropagation()} >

Move this task back to TODO and notify the team?

); }; interface TaskActionIconButtonProps { label: string; icon: React.ReactNode; onClick: (e: React.MouseEvent) => void; className: string; variant?: 'outline' | 'ghost' | 'destructive'; } const TaskActionIconButton = ({ label, icon, onClick, className, variant = 'outline', }: TaskActionIconButtonProps): React.JSX.Element => ( {label} ); export const KanbanTaskCard = ({ task, teamName, columnId, kanbanTaskState: _kanbanTaskState, hasReviewers, compact, taskMap, members, onRequestReview, onApprove, onRequestChanges, onMoveBackToDone, onStartTask, onCompleteTask, onCancelTask, onScrollToTask, onTaskClick, onViewChanges, onDeleteTask, }: KanbanTaskCardProps): React.JSX.Element => { const colorMap = useMemo(() => buildMemberColorMap(members), [members]); const unreadCount = useUnreadCommentCount(teamName, task.id, task.comments); const blockedByIds = task.blockedBy?.filter((id) => id.length > 0) ?? []; const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? []; const hasBlockedBy = blockedByIds.length > 0; const hasBlocks = blocksIds.length > 0; // Lazy-check if task has file changes (only for done/review/approved columns) const showChangesColumn = (columnId === 'done' || columnId === 'review' || columnId === 'approved') && !!onViewChanges; const taskChangeRequestOptions = useMemo(() => buildTaskChangeRequestOptions(task), [task]); const useTerminalSummaryCache = useMemo( () => isTaskSummaryCacheableForOptions(taskChangeRequestOptions), [taskChangeRequestOptions] ); const cacheKey = useMemo( () => buildTaskChangePresenceKey(teamName, task.id, taskChangeRequestOptions), [teamName, task.id, taskChangeRequestOptions] ); const taskHasChanges = useStore((s) => s.taskHasChanges[cacheKey]); const checkTaskHasChanges = useStore((s) => s.checkTaskHasChanges); useEffect(() => { if (showChangesColumn && useTerminalSummaryCache && taskHasChanges !== true) { void checkTaskHasChanges(teamName, task.id, taskChangeRequestOptions); } }, [ showChangesColumn, useTerminalSummaryCache, task.id, teamName, taskHasChanges, checkTaskHasChanges, taskChangeRequestOptions, ]); const isReviewManual = columnId === 'review' && !hasReviewers; const metaActions = ( <> {showChangesColumn && taskHasChanges === true ? ( } variant="ghost" className="text-sky-400 hover:bg-sky-500/10 hover:text-sky-300" onClick={(e) => { e.stopPropagation(); onViewChanges(task.id); }} /> ) : null} {onDeleteTask ? ( } variant="ghost" className="text-red-400 hover:bg-red-500/10 hover:text-red-300" onClick={(e) => { e.stopPropagation(); onDeleteTask(task.id); }} /> ) : null} ); return (
onTaskClick?.(task)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onTaskClick?.(task); } }} > {formatTaskDisplayLabel(task)}
{task.owner ? : null} {!compact && }
{task.needsClarification ? ( {task.needsClarification === 'user' ? 'Awaiting user' : 'Awaiting lead'} ) : null} {task.reviewState === 'needsFix' ? ( {REVIEW_STATE_DISPLAY.needsFix.label} ) : null} {compact && }
{hasBlockedBy ? (
Blocked by {blockedByIds.map((id) => ( ))}
) : null} {hasBlocks ? (
Blocks {blocksIds.map((id) => ( ))}
) : null}
{columnId === 'todo' ? ( <> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onStartTask(task.id); }} /> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onCompleteTask(task.id); }} /> ) : null} {columnId === 'in_progress' ? ( <> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onCompleteTask(task.id); }} /> ) : null} {columnId === 'done' ? ( <> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onApprove(task.id); }} /> } className="border-violet-500/40 text-violet-400 hover:bg-violet-500/10 hover:text-violet-300" onClick={(e) => { e.stopPropagation(); onRequestReview(task.id); }} /> ) : null} {columnId === 'review' ? (
{isReviewManual ? (
Manual review
) : null}
} className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onApprove(task.id); }} /> } variant="destructive" className="bg-red-500/90 text-white hover:bg-red-500" onClick={(e) => { e.stopPropagation(); onRequestChanges(task.id); }} />
{isReviewManual ? (
{metaActions}
) : null}
) : null} {columnId === 'approved' ? ( } className="border-amber-500/40 text-amber-400 hover:bg-amber-500/10 hover:text-amber-300" onClick={(e) => { e.stopPropagation(); onMoveBackToDone(task.id); }} /> ) : null}
{!isReviewManual ? (
{metaActions}
) : null}
); };