import React, { useState, useEffect, useContext, useCallback } from 'react'; import { VSCodeContext, TaskMasterTask } from '../webview/index'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { ChevronRight, ChevronDown, Plus, Wand2, PlusCircle, Loader2 } from 'lucide-react'; interface TaskDetailsViewProps { taskId: string; onNavigateBack: () => void; onNavigateToTask: (taskId: string) => void; } // Markdown renderer component to handle code blocks const MarkdownRenderer: React.FC<{ content: string; className?: string }> = ({ content, className = '' }) => { // Parse content to separate code blocks from regular text const parseMarkdown = (text: string) => { const parts = []; const lines = text.split('\n'); let currentBlock = []; let inCodeBlock = false; let codeLanguage = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('```')) { if (inCodeBlock) { // End of code block if (currentBlock.length > 0) { parts.push({ type: 'code', content: currentBlock.join('\n'), language: codeLanguage }); currentBlock = []; } inCodeBlock = false; codeLanguage = ''; } else { // Start of code block if (currentBlock.length > 0) { parts.push({ type: 'text', content: currentBlock.join('\n') }); currentBlock = []; } inCodeBlock = true; codeLanguage = line.substring(3).trim(); // Get language after ``` } } else { currentBlock.push(line); } } // Handle remaining content if (currentBlock.length > 0) { parts.push({ type: inCodeBlock ? 'code' : 'text', content: currentBlock.join('\n'), language: codeLanguage }); } return parts; }; const parts = parseMarkdown(content); return (
{parts.map((part, index) => { if (part.type === 'code') { return (
							{part.content}
						
); } else { // Handle inline code (single backticks) in text blocks const textWithInlineCode = part.content .split(/(`[^`]+`)/g) .map((segment, segIndex) => { if (segment.startsWith('`') && segment.endsWith('`')) { const codeContent = segment.slice(1, -1); return ( {codeContent} ); } return segment; }); return (
{textWithInlineCode}
); } })}
); }; // Custom Priority Badge Component with theme-adaptive styling const PriorityBadge: React.FC<{ priority: TaskMasterTask['priority'] }> = ({ priority }) => { const getPriorityColors = (priority: string) => { switch (priority) { case 'high': return { backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity color: '#dc2626', // red-600 - works in both themes borderColor: 'rgba(239, 68, 68, 0.4)' }; case 'medium': return { backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity color: '#d97706', // amber-600 - works in both themes borderColor: 'rgba(245, 158, 11, 0.4)' }; case 'low': return { backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity color: '#16a34a', // green-600 - works in both themes borderColor: 'rgba(34, 197, 94, 0.4)' }; default: return { backgroundColor: 'rgba(156, 163, 175, 0.2)', color: 'var(--vscode-foreground)', borderColor: 'rgba(156, 163, 175, 0.4)' }; } }; const colors = getPriorityColors(priority); return ( {priority} ); }; // Custom Status Badge Component with theme-adaptive styling const StatusBadge: React.FC<{ status: TaskMasterTask['status'] }> = ({ status }) => { const getStatusColors = (status: string) => { // Use colors that work well in both light and dark themes switch (status) { case 'pending': return { backgroundColor: 'rgba(156, 163, 175, 0.2)', // gray-400 with opacity color: 'var(--vscode-foreground)', borderColor: 'rgba(156, 163, 175, 0.4)' }; case 'in-progress': return { backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity color: '#d97706', // amber-600 - works in both themes borderColor: 'rgba(245, 158, 11, 0.4)' }; case 'review': return { backgroundColor: 'rgba(59, 130, 246, 0.2)', // blue-500 with opacity color: '#2563eb', // blue-600 - works in both themes borderColor: 'rgba(59, 130, 246, 0.4)' }; case 'done': return { backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity color: '#16a34a', // green-600 - works in both themes borderColor: 'rgba(34, 197, 94, 0.4)' }; case 'deferred': return { backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity color: '#dc2626', // red-600 - works in both themes borderColor: 'rgba(239, 68, 68, 0.4)' }; default: return { backgroundColor: 'rgba(156, 163, 175, 0.2)', color: 'var(--vscode-foreground)', borderColor: 'rgba(156, 163, 175, 0.4)' }; } }; const colors = getStatusColors(status); return ( {status === 'pending' ? 'todo' : status} ); }; // Define the TaskFileData interface here since we're no longer importing it interface TaskFileData { details?: string; testStrategy?: string; } interface CombinedTaskData { details?: string; testStrategy?: string; complexityScore?: number; // Only from MCP API } export const TaskDetailsView: React.FC = ({ taskId, onNavigateBack, onNavigateToTask }) => { const context = useContext(VSCodeContext); if (!context) { throw new Error('TaskDetailsView must be used within VSCodeContext'); } const { state, sendMessage } = context; const { tasks } = state; const [currentTask, setCurrentTask] = useState(null); const [isSubtask, setIsSubtask] = useState(false); const [parentTask, setParentTask] = useState(null); // Collapsible section states const [isAiActionsExpanded, setIsAiActionsExpanded] = useState(true); const [isImplementationExpanded, setIsImplementationExpanded] = useState(false); const [isTestStrategyExpanded, setIsTestStrategyExpanded] = useState(false); const [isSubtasksExpanded, setIsSubtasksExpanded] = useState(true); // AI Actions states const [prompt, setPrompt] = useState(''); const [isRegenerating, setIsRegenerating] = useState(false); const [isAppending, setIsAppending] = useState(false); // Add subtask states const [isAddingSubtask, setIsAddingSubtask] = useState(false); const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); const [newSubtaskDescription, setNewSubtaskDescription] = useState(''); const [isSubmittingSubtask, setIsSubmittingSubtask] = useState(false); // Task file data states (for implementation details, test strategy, and complexity score) const [taskFileData, setTaskFileData] = useState({ details: undefined, testStrategy: undefined, complexityScore: undefined }); const [isLoadingTaskFileData, setIsLoadingTaskFileData] = useState(false); const [taskFileDataError, setTaskFileDataError] = useState( null ); // Get complexity score from main task data immediately (no flash) const currentComplexityScore = currentTask?.complexityScore; // State for complexity data from MCP (only used for updates) const [mcpComplexityScore, setMcpComplexityScore] = useState< number | undefined >(undefined); const [isLoadingComplexity, setIsLoadingComplexity] = useState(false); // Use MCP complexity if available, otherwise use main task data const displayComplexityScore = mcpComplexityScore !== undefined ? mcpComplexityScore : currentComplexityScore; // Fetch complexity from MCP when needed const fetchComplexityFromMCP = useCallback( async (force = false) => { if (!currentTask || (!force && currentComplexityScore !== undefined)) { return; // Don't fetch if we already have a score unless forced } setIsLoadingComplexity(true); try { const complexityResult = await sendMessage({ type: 'mcpRequest', tool: 'complexity_report', params: {} }); if (complexityResult?.data?.report?.complexityAnalysis) { const taskComplexity = complexityResult.data.report.complexityAnalysis.find( (analysis: any) => analysis.taskId === currentTask.id ); if (taskComplexity?.complexityScore !== undefined) { setMcpComplexityScore(taskComplexity.complexityScore); } } } catch (error) { console.error('Failed to fetch complexity from MCP:', error); } finally { setIsLoadingComplexity(false); } }, [currentTask, currentComplexityScore, sendMessage] ); // Refresh complexity after AI operations or when task changes useEffect(() => { if (currentTask) { // Reset MCP complexity when task changes setMcpComplexityScore(undefined); // Fetch from MCP if no complexity score in main data if (currentComplexityScore === undefined) { fetchComplexityFromMCP(); } } }, [currentTask?.id, currentComplexityScore, fetchComplexityFromMCP]); // Refresh complexity after AI operations const refreshComplexityAfterAI = useCallback(() => { // Force refresh complexity after AI operations setTimeout(() => { fetchComplexityFromMCP(true); }, 2000); // Wait for AI operation to complete }, [fetchComplexityFromMCP]); // Handle running complexity analysis for a task const handleRunComplexityAnalysis = useCallback(async () => { if (!currentTask) { return; } setIsLoadingComplexity(true); try { // Run complexity analysis on this specific task await sendMessage({ type: 'mcpRequest', tool: 'analyze_project_complexity', params: { ids: currentTask.id.toString(), research: false } }); // After analysis, fetch the updated complexity report setTimeout(() => { fetchComplexityFromMCP(true); }, 1000); // Wait for analysis to complete } catch (error) { console.error('Failed to run complexity analysis:', error); } finally { setIsLoadingComplexity(false); } }, [currentTask, sendMessage, fetchComplexityFromMCP]); // Parse task ID to determine if it's a subtask (e.g., "13.2") const parseTaskId = (id: string) => { const parts = id.split('.'); if (parts.length === 2) { return { isSubtask: true, parentId: parts[0], subtaskIndex: parseInt(parts[1]) - 1 // Convert to 0-based index }; } return { isSubtask: false, parentId: id, subtaskIndex: -1 }; }; // Function to fetch task file data (implementation details and test strategy only) const fetchTaskFileData = async () => { if (!currentTask?.id) { return; } setIsLoadingTaskFileData(true); setTaskFileDataError(null); try { // For subtasks, construct the full dotted ID (e.g., "1.2") // For main tasks, use the task ID as-is const fileTaskId = isSubtask && parentTask ? `${parentTask.id}.${currentTask.id}` : currentTask.id; console.log('📄 Fetching task file data for task:', fileTaskId); // Get implementation details and test strategy from file const fileData = await sendMessage({ type: 'readTaskFileData', data: { taskId: fileTaskId, tag: 'master' // TODO: Make this configurable } }); console.log('📄 Task file data response:', fileData); // Combine file data with complexity score from task data (already loaded) const combinedData = { details: fileData.details, testStrategy: fileData.testStrategy, complexityScore: currentTask.complexityScore // Use complexity score from already-loaded task data }; console.log('📊 Combined task data:', combinedData); setTaskFileData(combinedData); } catch (error) { console.error('❌ Error fetching task file data:', error); setTaskFileDataError( error instanceof Error ? error.message : 'Failed to load task data' ); } finally { setIsLoadingTaskFileData(false); } }; // Find task or subtask by ID useEffect(() => { const { isSubtask: isSubtaskId, parentId, subtaskIndex } = parseTaskId(taskId); setIsSubtask(isSubtaskId); if (isSubtaskId) { // Find parent task const parent = tasks.find((task) => task.id === parentId); setParentTask(parent || null); // Find subtask if ( parent && parent.subtasks && subtaskIndex >= 0 && subtaskIndex < parent.subtasks.length ) { const subtask = parent.subtasks[subtaskIndex]; setCurrentTask(subtask); // Fetch file data for subtask fetchTaskFileData(); } else { setCurrentTask(null); } } else { // Find main task const task = tasks.find((task) => task.id === parentId); setCurrentTask(task || null); setParentTask(null); // Fetch file data for main task if (task) { fetchTaskFileData(); } } }, [taskId, tasks]); // Enhanced refresh logic for task file data when tasks are updated from polling useEffect(() => { if (currentTask) { // Create a comprehensive hash of task data to detect any changes const taskHash = JSON.stringify({ id: currentTask.id, title: currentTask.title, description: currentTask.description, status: currentTask.status, priority: currentTask.priority, dependencies: currentTask.dependencies, subtasksCount: currentTask.subtasks?.length || 0, subtasksStatus: currentTask.subtasks?.map((st) => st.status) || [], lastUpdate: Date.now() // Include timestamp to ensure periodic refresh }); // Small delay to ensure the tasks.json file has been updated const timeoutId = setTimeout(() => { console.log( '🔄 TaskDetailsView: Refreshing task file data due to task changes' ); fetchTaskFileData(); }, 500); return () => clearTimeout(timeoutId); } }, [currentTask, tasks, taskId]); // More comprehensive dependencies // Periodic refresh to ensure we have the latest data useEffect(() => { if (currentTask) { const intervalId = setInterval(() => { console.log('🔄 TaskDetailsView: Periodic refresh of task file data'); fetchTaskFileData(); }, 30000); // Refresh every 30 seconds return () => clearInterval(intervalId); } }, [currentTask, taskId]); // Handle AI Actions const handleRegenerate = async () => { if (!currentTask || !prompt.trim()) { return; } setIsRegenerating(true); try { if (isSubtask && parentTask) { await sendMessage({ type: 'updateSubtask', data: { taskId: `${parentTask.id}.${currentTask.id}`, prompt: prompt, options: { research: false } } }); } else { await sendMessage({ type: 'updateTask', data: { taskId: currentTask.id, updates: { description: prompt }, options: { append: false, research: false } } }); } // Refresh both task file data and complexity after AI operation setTimeout(() => { console.log('🔄 TaskDetailsView: Refreshing after AI regeneration'); fetchTaskFileData(); }, 2000); // Wait 2 seconds for AI to finish processing // Refresh complexity after AI operation refreshComplexityAfterAI(); } catch (error) { console.error('❌ TaskDetailsView: Failed to regenerate task:', error); } finally { setIsRegenerating(false); setPrompt(''); } }; const handleAppend = async () => { if (!currentTask || !prompt.trim()) { return; } setIsAppending(true); try { if (isSubtask && parentTask) { await sendMessage({ type: 'updateSubtask', data: { taskId: `${parentTask.id}.${currentTask.id}`, prompt: prompt, options: { research: false } } }); } else { await sendMessage({ type: 'updateTask', data: { taskId: currentTask.id, updates: { description: prompt }, options: { append: true, research: false } } }); } // Refresh both task file data and complexity after AI operation setTimeout(() => { console.log('🔄 TaskDetailsView: Refreshing after AI append'); fetchTaskFileData(); }, 2000); // Wait 2 seconds for AI to finish processing // Refresh complexity after AI operation refreshComplexityAfterAI(); } catch (error) { console.error('❌ TaskDetailsView: Failed to append to task:', error); } finally { setIsAppending(false); setPrompt(''); } }; // Handle adding a new subtask const handleAddSubtask = async () => { if (!currentTask || !newSubtaskTitle.trim() || isSubtask) { return; } setIsSubmittingSubtask(true); try { await sendMessage({ type: 'addSubtask', data: { parentTaskId: currentTask.id, subtaskData: { title: newSubtaskTitle.trim(), description: newSubtaskDescription.trim() || undefined, status: 'pending' } } }); // Reset form and close setNewSubtaskTitle(''); setNewSubtaskDescription(''); setIsAddingSubtask(false); // Refresh task data to show the new subtask setTimeout(() => { console.log('🔄 TaskDetailsView: Refreshing after adding subtask'); fetchTaskFileData(); }, 1000); } catch (error) { console.error('❌ TaskDetailsView: Failed to add subtask:', error); } finally { setIsSubmittingSubtask(false); } }; const handleCancelAddSubtask = () => { setIsAddingSubtask(false); setNewSubtaskTitle(''); setNewSubtaskDescription(''); }; // Handle dependency navigation const handleDependencyClick = (depId: string) => { onNavigateToTask(depId); }; // Handle status change const handleStatusChange = async (newStatus: TaskMasterTask['status']) => { if (!currentTask) { return; } try { await sendMessage({ type: 'updateTaskStatus', data: { taskId: isSubtask && parentTask ? `${parentTask.id}.${currentTask.id}` : currentTask.id, newStatus: newStatus } }); } catch (error) { console.error('❌ TaskDetailsView: Failed to update task status:', error); } }; if (!currentTask) { return (

Task not found

); } return (
{/* Main content area with two-column layout */}
{/* Left column - Main content (2/3 width) */}
{/* Breadcrumb navigation */} Kanban Board {isSubtask && parentTask && ( <> onNavigateToTask(parentTask.id)} className="cursor-pointer hover:text-vscode-foreground" > {parentTask.title} )} {currentTask.title} {/* Task title */}

{currentTask.title}

{/* Description (non-editable) */}

{currentTask.description || 'No description available.'}

{/* AI Actions */}
{isAiActionsExpanded && (