From dadfa471cd7f9160c873610e64aeff99bda5a8db Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:08:37 +0300 Subject: [PATCH] feat: fix ai actions - add react-query - implement cache invalidation strategy --- .../TaskDetails/AIActionsSection.tsx | 24 ++++- .../TaskDetails/TaskMetadataSidebar.tsx | 5 +- .../components/TaskDetails/useTaskDetails.ts | 10 +- .../src/components/TaskDetailsView.tsx | 101 ++++++++++++------ .../extension/src/services/webview-manager.ts | 22 +--- .../src/utils/task-master-api/index.ts | 8 +- apps/extension/src/webview/App.tsx | 41 +------ .../src/webview/components/AppContent.tsx | 40 +++++++ .../src/webview/components/TaskCard.tsx | 22 +++- .../webview/components/TaskMasterKanban.tsx | 46 +++++++- .../src/webview/hooks/useTaskQueries.ts | 70 +++++++++--- .../src/webview/hooks/useVSCodeMessages.ts | 17 --- 12 files changed, 261 insertions(+), 145 deletions(-) create mode 100644 apps/extension/src/webview/components/AppContent.tsx diff --git a/apps/extension/src/components/TaskDetails/AIActionsSection.tsx b/apps/extension/src/components/TaskDetails/AIActionsSection.tsx index bdc387cc..098158f8 100644 --- a/apps/extension/src/components/TaskDetails/AIActionsSection.tsx +++ b/apps/extension/src/components/TaskDetails/AIActionsSection.tsx @@ -31,6 +31,9 @@ export const AIActionsSection: React.FC = ({ onAppendingChange }) => { const [prompt, setPrompt] = useState(''); + const [lastAction, setLastAction] = useState<'regenerate' | 'append' | null>( + null + ); const updateTask = useUpdateTask(); const updateSubtask = useUpdateSubtask(); @@ -39,6 +42,9 @@ export const AIActionsSection: React.FC = ({ return; } + setLastAction('regenerate'); + onRegeneratingChange?.(true); + try { if (isSubtask && parentTask) { await updateSubtask.mutateAsync({ @@ -58,6 +64,9 @@ export const AIActionsSection: React.FC = ({ refreshComplexityAfterAI(); } catch (error) { console.error('❌ TaskDetailsView: Failed to regenerate task:', error); + } finally { + setLastAction(null); + onRegeneratingChange?.(false); } }; @@ -66,6 +75,9 @@ export const AIActionsSection: React.FC = ({ return; } + setLastAction('append'); + onAppendingChange?.(true); + try { if (isSubtask && parentTask) { await updateSubtask.mutateAsync({ @@ -85,12 +97,16 @@ export const AIActionsSection: React.FC = ({ refreshComplexityAfterAI(); } catch (error) { console.error('❌ TaskDetailsView: Failed to append to task:', error); + } finally { + setLastAction(null); + onAppendingChange?.(false); } }; - // Track loading states - const isRegenerating = updateTask.isPending || updateSubtask.isPending; - const isAppending = updateTask.isPending || updateSubtask.isPending; + // Track loading states based on the last action + const isLoading = updateTask.isPending || updateSubtask.isPending; + const isRegenerating = isLoading && lastAction === 'regenerate'; + const isAppending = isLoading && lastAction === 'append'; return ( = ({

Append: Adds new content to the existing task - description based on your prompt + implementation details based on your prompt

)} diff --git a/apps/extension/src/components/TaskDetails/TaskMetadataSidebar.tsx b/apps/extension/src/components/TaskDetails/TaskMetadataSidebar.tsx index 93ee3779..ac512723 100644 --- a/apps/extension/src/components/TaskDetails/TaskMetadataSidebar.tsx +++ b/apps/extension/src/components/TaskDetails/TaskMetadataSidebar.tsx @@ -256,7 +256,10 @@ export const TaskMetadataSidebar: React.FC = ({
{currentTask.dependencies.map((depId) => { - const depTask = tasks.find((t) => t.id === depId); + // Convert both to string for comparison since depId might be string or number + const depTask = tasks.find( + (t) => String(t.id) === String(depId) + ); const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`; const truncatedTitle = fullTitle.length > 40 diff --git a/apps/extension/src/components/TaskDetails/useTaskDetails.ts b/apps/extension/src/components/TaskDetails/useTaskDetails.ts index 8d1f9b30..d3323526 100644 --- a/apps/extension/src/components/TaskDetails/useTaskDetails.ts +++ b/apps/extension/src/components/TaskDetails/useTaskDetails.ts @@ -20,7 +20,9 @@ export const useTaskDetails = ({ }: UseTaskDetailsProps) => { // Parse task ID to determine if it's a subtask (e.g., "13.2") const { isSubtask, parentId, subtaskIndex, taskIdForFetch } = useMemo(() => { - const parts = taskId.split('.'); + // Ensure taskId is a string + const taskIdStr = String(taskId); + const parts = taskIdStr.split('.'); if (parts.length === 2) { return { isSubtask: true, @@ -31,9 +33,9 @@ export const useTaskDetails = ({ } return { isSubtask: false, - parentId: taskId, + parentId: taskIdStr, subtaskIndex: -1, - taskIdForFetch: taskId + taskIdForFetch: taskIdStr }; }, [taskId]); @@ -50,7 +52,7 @@ export const useTaskDetails = ({ return { currentTask: subtask, parentTask: parent }; } } else { - const task = tasks.find((t) => t.id === taskId); + const task = tasks.find((t) => t.id === String(taskId)); if (task) { return { currentTask: task, parentTask: null }; } diff --git a/apps/extension/src/components/TaskDetailsView.tsx b/apps/extension/src/components/TaskDetailsView.tsx index f5a1095a..5fc72325 100644 --- a/apps/extension/src/components/TaskDetailsView.tsx +++ b/apps/extension/src/components/TaskDetailsView.tsx @@ -1,6 +1,8 @@ import type React from 'react'; -import { useContext } from 'react'; +import { useContext, useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; +import { useQueryClient } from '@tanstack/react-query'; +import { RefreshCw } from 'lucide-react'; import { Breadcrumb, BreadcrumbItem, @@ -14,6 +16,7 @@ import { SubtasksSection } from './TaskDetails/SubtasksSection'; import { TaskMetadataSidebar } from './TaskDetails/TaskMetadataSidebar'; import { DetailsSection } from './TaskDetails/DetailsSection'; import { useTaskDetails } from './TaskDetails/useTaskDetails'; +import { useTasks, taskKeys } from '../webview/hooks/useTaskQueries'; import type { TaskMasterTask } from '../webview/types'; interface TaskDetailsViewProps { @@ -33,7 +36,12 @@ export const TaskDetailsView: React.FC = ({ } const { state, sendMessage } = context; - const { tasks } = state; + const { currentTag } = state; + const queryClient = useQueryClient(); + const [isRefreshing, setIsRefreshing] = useState(false); + + // Use React Query to fetch all tasks + const { data: allTasks = [] } = useTasks({ tag: currentTag }); const { currentTask, @@ -43,7 +51,7 @@ export const TaskDetailsView: React.FC = ({ taskFileDataError, complexity, refreshComplexityAfterAI - } = useTaskDetails({ taskId, sendMessage, tasks }); + } = useTaskDetails({ taskId, sendMessage, tasks: allTasks }); const handleStatusChange = async (newStatus: TaskMasterTask['status']) => { if (!currentTask) return; @@ -68,6 +76,17 @@ export const TaskDetailsView: React.FC = ({ onNavigateToTask(depId); }; + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + try { + // Invalidate all task queries + await queryClient.invalidateQueries({ queryKey: taskKeys.all }); + } finally { + // Reset after a short delay to show the animation + setTimeout(() => setIsRefreshing(false), 500); + } + }, [queryClient]); + if (!currentTask) { return (
@@ -89,37 +108,49 @@ export const TaskDetailsView: React.FC = ({ {/* 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} - - - - +
+ + + + + Kanban Board + + + {isSubtask && parentTask && ( + <> + + + onNavigateToTask(parentTask.id)} + className="cursor-pointer hover:text-vscode-foreground" + > + {parentTask.title} + + + + )} + + + + {currentTask.title} + + + + + +
{/* Task title */}

@@ -172,7 +203,7 @@ export const TaskDetailsView: React.FC = ({ {/* Right column - Metadata (1/3 width) */} = { - id: taskId, + id: String(taskId), status: status, projectRoot: options?.projectRoot || this.getWorkspaceRoot() }; @@ -238,7 +238,7 @@ export class TaskMasterApi { const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`; const mcpArgs: Record = { - id: taskId, + id: String(taskId), prompt: prompt, projectRoot: options?.projectRoot || this.getWorkspaceRoot() }; @@ -284,7 +284,7 @@ export class TaskMasterApi { try { const mcpArgs: Record = { - id: taskId, + id: String(taskId), prompt: prompt, projectRoot: options?.projectRoot || this.getWorkspaceRoot() }; @@ -327,7 +327,7 @@ export class TaskMasterApi { try { const mcpArgs: Record = { - id: parentTaskId, + id: String(parentTaskId), title: subtaskData.title, projectRoot: options?.projectRoot || this.getWorkspaceRoot() }; diff --git a/apps/extension/src/webview/App.tsx b/apps/extension/src/webview/App.tsx index 3cebfc33..b100ebc5 100644 --- a/apps/extension/src/webview/App.tsx +++ b/apps/extension/src/webview/App.tsx @@ -5,9 +5,7 @@ import React, { useReducer, useState, useEffect, useRef } from 'react'; import { VSCodeContext } from './contexts/VSCodeContext'; import { QueryProvider } from './providers/QueryProvider'; -import { TaskMasterKanban } from './components/TaskMasterKanban'; -import TaskDetailsView from '@/components/TaskDetailsView'; -import { ConfigView } from '@/components/ConfigView'; +import { AppContent } from './components/AppContent'; import { ToastContainer } from './components/ToastContainer'; import { ErrorBoundary } from './components/ErrorBoundary'; import { appReducer, initialState } from './reducers/appReducer'; @@ -96,42 +94,7 @@ export const App: React.FC = () => { }); }} > - {/* Conditional rendering for different views */} - {(() => { - console.log( - '🎯 App render - currentView:', - state.currentView, - 'selectedTaskId:', - state.selectedTaskId - ); - - if (state.currentView === 'config') { - return ( - - dispatch({ type: 'NAVIGATE_TO_KANBAN' }) - } - /> - ); - } - - if (state.currentView === 'task-details' && state.selectedTaskId) { - return ( - - dispatch({ type: 'NAVIGATE_TO_KANBAN' }) - } - onNavigateToTask={(taskId: string) => - dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId }) - } - /> - ); - } - - return ; - })()} + dispatch({ type: 'REMOVE_TOAST', payload: id })} diff --git a/apps/extension/src/webview/components/AppContent.tsx b/apps/extension/src/webview/components/AppContent.tsx new file mode 100644 index 00000000..1496f178 --- /dev/null +++ b/apps/extension/src/webview/components/AppContent.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { TaskMasterKanban } from './TaskMasterKanban'; +import TaskDetailsView from '@/components/TaskDetailsView'; +import { ConfigView } from '@/components/ConfigView'; +import { useVSCodeContext } from '../contexts/VSCodeContext'; + +export const AppContent: React.FC = () => { + const { state, dispatch, sendMessage } = useVSCodeContext(); + + console.log( + '🎯 AppContent render - currentView:', + state.currentView, + 'selectedTaskId:', + state.selectedTaskId + ); + + if (state.currentView === 'config') { + return ( + dispatch({ type: 'NAVIGATE_TO_KANBAN' })} + /> + ); + } + + if (state.currentView === 'task-details' && state.selectedTaskId) { + return ( + dispatch({ type: 'NAVIGATE_TO_KANBAN' })} + onNavigateToTask={(taskId: string) => + dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId }) + } + /> + ); + } + + // Default to Kanban view + return ; +}; diff --git a/apps/extension/src/webview/components/TaskCard.tsx b/apps/extension/src/webview/components/TaskCard.tsx index 901be08a..b65985aa 100644 --- a/apps/extension/src/webview/components/TaskCard.tsx +++ b/apps/extension/src/webview/components/TaskCard.tsx @@ -53,9 +53,25 @@ export const TaskCard: React.FC = ({ #{task.id} {task.dependencies && task.dependencies.length > 0 && ( - - Deps: {task.dependencies.length} - +
+ Deps: +
+ {task.dependencies.map((depId, index) => ( + + + {index < task.dependencies!.length - 1 && ,} + + ))} +
+
)}

diff --git a/apps/extension/src/webview/components/TaskMasterKanban.tsx b/apps/extension/src/webview/components/TaskMasterKanban.tsx index b6942cc7..3a1504aa 100644 --- a/apps/extension/src/webview/components/TaskMasterKanban.tsx +++ b/apps/extension/src/webview/components/TaskMasterKanban.tsx @@ -3,6 +3,8 @@ */ import React, { useState, useCallback, useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { RefreshCw } from 'lucide-react'; import { type DragEndEvent, KanbanBoard, @@ -19,15 +21,16 @@ import { useVSCodeContext } from '../contexts/VSCodeContext'; import { useTasks, useUpdateTaskStatus, - useUpdateTask + useUpdateTask, + taskKeys } from '../hooks/useTaskQueries'; import { kanbanStatuses, HEADER_HEIGHT } from '../constants'; import type { TaskMasterTask, TaskUpdates } from '../types'; export const TaskMasterKanban: React.FC = () => { const { state, dispatch, sendMessage, availableHeight } = useVSCodeContext(); + const queryClient = useQueryClient(); const { - loading: legacyLoading, error: legacyError, editingTask, polling, @@ -35,16 +38,28 @@ export const TaskMasterKanban: React.FC = () => { availableTags } = state; const [activeTask, setActiveTask] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); // Use React Query to fetch tasks const { data: serverTasks = [], isLoading, - error + error, + isFetching, + isSuccess } = useTasks({ tag: currentTag }); const updateTaskStatus = useUpdateTaskStatus(); const updateTask = useUpdateTask(); + // Debug logging + console.log('🔍 TaskMasterKanban Query State:', { + isLoading, + isFetching, + isSuccess, + tasksCount: serverTasks?.length, + error + }); + // Temporary state only for active drag operations const [tempReorderedTasks, setTempReorderedTasks] = useState< TaskMasterTask[] | null @@ -178,6 +193,18 @@ export const TaskMasterKanban: React.FC = () => { sendMessage({ type: 'retryConnection' }); }, [sendMessage]); + // Handle refresh + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + try { + // Invalidate all task queries + await queryClient.invalidateQueries({ queryKey: taskKeys.all }); + } finally { + // Reset after a short delay to show the animation + setTimeout(() => setIsRefreshing(false), 500); + } + }, [queryClient]); + // Handle tag switching const handleTagSwitch = useCallback( async (tagName: string) => { @@ -192,14 +219,13 @@ export const TaskMasterKanban: React.FC = () => { ); // Use React Query loading state - const loading = isLoading || legacyLoading; const displayError = error ? error instanceof Error ? error.message : String(error) : legacyError; - if (loading) { + if (isLoading) { return (
{ sendMessage={sendMessage} dispatch={dispatch} /> +
{ + console.log('🔍 Fetching tasks with options:', options); const response = await sendMessage({ type: 'getTasks', data: { @@ -26,8 +27,10 @@ export function useTasks(options?: { tag?: string; status?: string }) { withSubtasks: true } }); + console.log('📋 Tasks fetched:', response); return response as TaskMasterTask[]; - } + }, + staleTime: 0 // Consider data stale immediately }); } @@ -141,17 +144,36 @@ export function useUpdateTask() { updates: TaskUpdates | { description: string }; options?: { append?: boolean; research?: boolean }; }) => { - await sendMessage({ + console.log('🔄 Updating task:', taskId, updates, options); + + const response = await sendMessage({ type: 'updateTask', data: { taskId, updates, options } }); + + console.log('📥 Update task response:', response); + + // Check for error in response + if (response && typeof response === 'object' && 'error' in response) { + throw new Error(response.error || 'Failed to update task'); + } + + return response; }, - onSuccess: (_, variables) => { - // Invalidate the specific task and all lists - queryClient.invalidateQueries({ - queryKey: taskKeys.detail(variables.taskId) + onSuccess: async (data, variables) => { + console.log('✅ Task update successful, invalidating all task queries'); + console.log('Response data:', data); + console.log('Task ID:', variables.taskId); + + // Invalidate ALL task-related queries (same as handleRefresh) + await queryClient.invalidateQueries({ + queryKey: taskKeys.all }); - queryClient.invalidateQueries({ queryKey: taskKeys.lists() }); + + console.log( + '🔄 All task queries invalidated for task:', + variables.taskId + ); } }); } @@ -171,19 +193,37 @@ export function useUpdateSubtask() { prompt: string; options?: { research?: boolean }; }) => { - await sendMessage({ + console.log('🔄 Updating subtask:', taskId, prompt, options); + + const response = await sendMessage({ type: 'updateSubtask', data: { taskId, prompt, options } }); + + console.log('📥 Update subtask response:', response); + + // Check for error in response + if (response && typeof response === 'object' && 'error' in response) { + throw new Error(response.error || 'Failed to update subtask'); + } + + return response; }, - onSuccess: (_, variables) => { - // Extract parent task ID from subtask ID (e.g., "1.2" -> "1") - const parentTaskId = variables.taskId.split('.')[0]; - // Invalidate the parent task details and all lists - queryClient.invalidateQueries({ - queryKey: taskKeys.detail(parentTaskId) + onSuccess: async (data, variables) => { + console.log( + '✅ Subtask update successful, invalidating all task queries' + ); + console.log('Subtask ID:', variables.taskId); + + // Invalidate ALL task-related queries (same as handleRefresh) + await queryClient.invalidateQueries({ + queryKey: taskKeys.all }); - queryClient.invalidateQueries({ queryKey: taskKeys.lists() }); + + console.log( + '🔄 All task queries invalidated for subtask:', + variables.taskId + ); } }); } diff --git a/apps/extension/src/webview/hooks/useVSCodeMessages.ts b/apps/extension/src/webview/hooks/useVSCodeMessages.ts index 77fd4db9..f7f08de7 100644 --- a/apps/extension/src/webview/hooks/useVSCodeMessages.ts +++ b/apps/extension/src/webview/hooks/useVSCodeMessages.ts @@ -96,23 +96,6 @@ export const useVSCodeMessages = ( dispatch({ type: 'SET_TASKS', payload: message.data }); break; - case 'tasksUpdated': - console.log('📋 Tasks updated:', message.data); - // Extract tasks from the data object - const tasks = message.data?.tasks || message.data; - if (Array.isArray(tasks)) { - dispatch({ type: 'SET_TASKS', payload: tasks }); - } - break; - - case 'taskStatusUpdated': - console.log('✅ Task status updated:', message); - break; - - case 'taskUpdated': - console.log('✅ Task content updated:', message); - break; - case 'pollingStatus': dispatch({ type: 'SET_POLLING_STATUS',