import { useEffect, useMemo } from 'react'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { CollapsibleTeamSection } from '@renderer/components/team/CollapsibleTeamSection'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberLogsTab } from '@renderer/components/team/members/MemberLogsTab'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { markAsRead } from '@renderer/services/commentReadStorage'; import { useStore } from '@renderer/store'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { buildMemberColorMap, KANBAN_COLUMN_DISPLAY, TASK_STATUS_LABELS, TASK_STATUS_STYLES, } from '@renderer/utils/memberHelpers'; import { formatDistanceToNow } from 'date-fns'; import { ArrowLeftFromLine, ArrowRightFromLine, Clock, FileCode, Link2, Loader2, PenLine, } from 'lucide-react'; import { TaskCommentsSection } from './TaskCommentsSection'; import type { KanbanTaskState, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types'; interface TaskDetailDialogProps { open: boolean; task: TeamTaskWithKanban | null; teamName: string; kanbanTaskState?: KanbanTaskState; taskMap: Map; members: ResolvedTeamMember[]; onClose: () => void; onScrollToTask?: (taskId: string) => void; onOwnerChange?: (taskId: string, owner: string | null) => void; onViewChanges?: (taskId: string, filePath?: string) => void; /** Extra content rendered in the dialog header (e.g. "Open team" button). */ headerExtra?: React.ReactNode; } export const TaskDetailDialog = ({ open, task, teamName, kanbanTaskState, taskMap, members, onClose, onScrollToTask, onOwnerChange, onViewChanges, headerExtra, }: TaskDetailDialogProps): React.JSX.Element => { const colorMap = useMemo(() => buildMemberColorMap(members), [members]); const currentTask = task ? (taskMap.get(task.id) ?? task) : null; useEffect(() => { if (!open || !currentTask) return; const comments = currentTask.comments ?? []; if (comments.length === 0) return; const latest = Math.max(...comments.map((c) => new Date(c.createdAt).getTime())); if (latest > 0) markAsRead(teamName, currentTask.id, latest); }, [open, teamName, currentTask]); // Lazy-load task changes when dialog is open and task is completed const isTaskCompleted = currentTask?.status === 'completed'; const activeChangeSet = useStore((s) => s.activeChangeSet); const changeSetLoading = useStore((s) => s.changeSetLoading); const fetchTaskChanges = useStore((s) => s.fetchTaskChanges); const taskChangesFiles = useMemo(() => { if (!activeChangeSet || !currentTask) return null; if ('taskId' in activeChangeSet && activeChangeSet.taskId === currentTask.id) { return activeChangeSet.files; } return null; }, [activeChangeSet, currentTask]); useEffect(() => { if (!open || !currentTask || !isTaskCompleted || !onViewChanges) return; // Only fetch if we don't already have data for this task if (taskChangesFiles !== null) return; void fetchTaskChanges(teamName, currentTask.id); }, [ open, currentTask, isTaskCompleted, teamName, fetchTaskChanges, taskChangesFiles, onViewChanges, ]); const handleDependencyClick = (taskId: string): void => { onClose(); onScrollToTask?.(taskId); }; if (!currentTask) { return ( !v && onClose()}> Task not found ); } const kanbanColumn = kanbanTaskState?.column ?? currentTask.kanbanColumn; const status = currentTask.status; const statusStyle = kanbanColumn && KANBAN_COLUMN_DISPLAY[kanbanColumn] ? { bg: KANBAN_COLUMN_DISPLAY[kanbanColumn].bg, text: KANBAN_COLUMN_DISPLAY[kanbanColumn].text, } : TASK_STATUS_STYLES[status]; const statusLabel = kanbanColumn && KANBAN_COLUMN_DISPLAY[kanbanColumn] ? KANBAN_COLUMN_DISPLAY[kanbanColumn].label : TASK_STATUS_LABELS[status]; const blockedByIds = currentTask.blockedBy?.filter((id) => id.length > 0) ?? []; const blocksIds = currentTask.blocks?.filter((id) => id.length > 0) ?? []; const relatedIds = (currentTask.related ?? []).filter( (id) => id.length > 0 && id !== currentTask.id ); const relatedByIds = Array.from(taskMap.values()) .filter( (t) => t.id !== currentTask.id && Array.isArray(t.related) && t.related.includes(currentTask.id) ) .map((t) => t.id); const isTodo = status === 'pending' && !kanbanColumn; const canReassign = isTodo && onOwnerChange; return ( !v && onClose()}>
#{currentTask.id} {statusLabel} {headerExtra ?
{headerExtra}
: null}
{currentTask.subject} {currentTask.activeForm ? ( {currentTask.activeForm} ) : null}
{/* Metadata */}
{canReassign ? ( ) : currentTask.owner ? ( ) : ( )}
{currentTask.createdBy ? (
{currentTask.createdBy}
) : null} {currentTask.createdAt ? (() => { const date = new Date(currentTask.createdAt); return isNaN(date.getTime()) ? null : (
{formatDistanceToNow(date, { addSuffix: true })}
); })() : null}
{/* Description */} {currentTask.description ? (
) : (

No description

)}
{/* Changes */} {isTaskCompleted && onViewChanges ? ( 0} > {changeSetLoading && !taskChangesFiles ? (
Loading changes...
) : taskChangesFiles && taskChangesFiles.length > 0 ? (
{taskChangesFiles.map((file) => ( ))}
) : (

No file changes detected

)}
) : null}
{/* Dependencies */} {blockedByIds.length > 0 ? (
Blocked by {blockedByIds.map((id) => { const depTask = taskMap.get(id); const isCompleted = depTask?.status === 'completed'; return ( ); })}
) : null} {blocksIds.length > 0 ? (
Blocks {blocksIds.map((id) => { const depTask = taskMap.get(id); const isCompleted = depTask?.status === 'completed'; return ( ); })}
) : null} {/* Related tasks (explicit) */} {relatedIds.length > 0 || relatedByIds.length > 0 ? (
Related tasks
{relatedIds.length > 0 ? (
Links {relatedIds.map((id) => { const depTask = taskMap.get(id); return ( ); })}
) : null} {relatedByIds.length > 0 ? (
Linked from {relatedByIds.map((id) => { const depTask = taskMap.get(id); return ( ); })}
) : null}
) : null} {/* Review info */} {kanbanTaskState ? (
{kanbanTaskState.reviewer ? ( Reviewer: {kanbanTaskState.reviewer} ) : null} {kanbanTaskState.errorDescription ? ( {kanbanTaskState.errorDescription} ) : null}
) : null}
{/* Comments */} 0 ? (currentTask.comments?.length ?? 0) : undefined } defaultOpen > {/* Execution Logs — sessions that reference this task */}
); };