diff --git a/apps/extension/src/components/TaskDetails/useTaskDetails.ts b/apps/extension/src/components/TaskDetails/useTaskDetails.ts index 71165ea9..9aaf49a4 100644 --- a/apps/extension/src/components/TaskDetails/useTaskDetails.ts +++ b/apps/extension/src/components/TaskDetails/useTaskDetails.ts @@ -47,47 +47,93 @@ export const useTaskDetails = ({ // Find the current task useEffect(() => { - console.log('🔍 TaskDetailsView: Looking for task:', taskId); - console.log('🔍 TaskDetailsView: Available tasks:', tasks); - const { isSubtask: isSub, parentId, subtaskIndex } = parseTaskId(taskId); if (isSub) { const parent = tasks.find((t) => t.id === parentId); if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) { const subtask = parent.subtasks[subtaskIndex]; - console.log('✅ TaskDetailsView: Found subtask:', subtask); setCurrentTask(subtask); setParentTask(parent); - // Use subtask's own details and testStrategy - setTaskFileData({ - details: subtask.details || '', - testStrategy: subtask.testStrategy || '' - }); } else { - console.error('❌ TaskDetailsView: Subtask not found'); setCurrentTask(null); setParentTask(null); } } else { const task = tasks.find((t) => t.id === taskId); if (task) { - console.log('✅ TaskDetailsView: Found task:', task); setCurrentTask(task); setParentTask(null); - // Use task's own details and testStrategy - setTaskFileData({ - details: task.details || '', - testStrategy: task.testStrategy || '' - }); } else { - console.error('❌ TaskDetailsView: Task not found'); setCurrentTask(null); setParentTask(null); } } }, [taskId, tasks]); + // Fetch full task details including details and testStrategy + useEffect(() => { + const fetchTaskDetails = async () => { + if (!currentTask) return; + + try { + // Use the parent task ID for MCP call since get_task returns parent with subtasks + const taskIdToFetch = + isSubtask && parentTask ? parentTask.id : currentTask.id; + + const result = await sendMessage({ + type: 'mcpRequest', + tool: 'get_task', + params: { + id: taskIdToFetch + } + }); + + // Parse the MCP response - it comes as content[0].text JSON string + let fullTaskData = null; + if (result?.data?.content?.[0]?.text) { + try { + const parsed = JSON.parse(result.data.content[0].text); + fullTaskData = parsed.data; + } catch (e) { + console.error('Failed to parse MCP response:', e); + } + } else if (result?.data?.data) { + // Fallback if response structure is different + fullTaskData = result.data.data; + } + + if (fullTaskData) { + if (isSubtask && fullTaskData.subtasks) { + // Find the specific subtask + const subtaskData = fullTaskData.subtasks.find( + (st: any) => + st.id === currentTask.id || + st.id === parseInt(currentTask.id as any) + ); + if (subtaskData) { + setTaskFileData({ + details: subtaskData.details || '', + testStrategy: subtaskData.testStrategy || '' + }); + } + } else { + // Use the main task data + setTaskFileData({ + details: fullTaskData.details || '', + testStrategy: fullTaskData.testStrategy || '' + }); + } + } + } catch (error) { + console.error('❌ Failed to fetch task details:', error); + setTaskFileDataError('Failed to load task details'); + } + }; + + fetchTaskDetails(); + }, [currentTask, isSubtask, parentTask, sendMessage]); + // Fetch complexity score const fetchComplexity = useCallback(async () => { if (!currentTask) return; diff --git a/apps/extension/src/components/TaskDetailsView.original.tsx b/apps/extension/src/components/TaskDetailsView.original.tsx deleted file mode 100644 index bf84ce19..00000000 --- a/apps/extension/src/components/TaskDetailsView.original.tsx +++ /dev/null @@ -1,1304 +0,0 @@ -import { Badge } from '@/components/ui/badge'; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator -} from '@/components/ui/breadcrumb'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger -} from '@/components/ui/collapsible'; -import { Label } from '@/components/ui/label'; -import { Separator } from '@/components/ui/separator'; -import { Textarea } from '@/components/ui/textarea'; -import { - ChevronDown, - ChevronRight, - Loader2, - Plus, - PlusCircle, - Wand2 -} from 'lucide-react'; -import type React from 'react'; -import { useCallback, useContext, useEffect, useState } from 'react'; -import { VSCodeContext } from '../webview/contexts/VSCodeContext'; -import type { TaskMasterTask } from '../webview/types'; - -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 - }); - // Loading state removed as data comes directly from tasks - const [taskFileDataError] = 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 - }; - }; - - // Note: Task file data is now loaded directly from currentTask - // The details, testStrategy, and complexityScore are already available in the task object - - // 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); - // Set task file data from the subtask itself - setTaskFileData({ - details: subtask.details || '', - testStrategy: subtask.testStrategy || '', - complexityScore: subtask.complexityScore - }); - } else { - setCurrentTask(null); - } - } else { - // Find main task - const task = tasks.find((task) => task.id === parentId); - setCurrentTask(task || null); - setParentTask(null); - // Set task file data from the task itself - if (task) { - setTaskFileData({ - details: task.details || '', - testStrategy: task.testStrategy || '', - complexityScore: task.complexityScore - }); - } - } - }, [taskId, tasks]); - - // Enhanced refresh logic for task file data when tasks are updated from polling - useEffect(() => { - if (currentTask) { - // Update task file data from currentTask whenever it changes - setTaskFileData({ - details: currentTask.details || '', - testStrategy: currentTask.testStrategy || '', - complexityScore: currentTask.complexityScore - }); - } - }, [currentTask, tasks, taskId]); // More comprehensive dependencies - - // Remove periodic refresh since we're using task data directly - // The data will update when tasks update through the context - - // 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 - // Data will be refreshed automatically when tasks update - - // 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 - // Data will be refreshed automatically when tasks update - - // 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); - - // Data will be refreshed automatically when tasks update - } 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 && ( -
-
-
- -