Integration with TaskMaster AI

This commit is contained in:
simos
2025-08-28 12:11:42 +03:00
parent c1e7bb6c10
commit 75e8161213
33 changed files with 7856 additions and 111 deletions

View File

@@ -0,0 +1,324 @@
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { api } from '../utils/api';
import { useAuth } from './AuthContext';
import { useWebSocketContext } from './WebSocketContext';
const TaskMasterContext = createContext({
// TaskMaster project state
projects: [],
currentProject: null,
projectTaskMaster: null,
// MCP server state
mcpServerStatus: null,
// Tasks state
tasks: [],
nextTask: null,
// Loading states
isLoading: false,
isLoadingTasks: false,
isLoadingMCP: false,
// Error state
error: null,
// Actions
refreshProjects: () => {},
setCurrentProject: () => {},
refreshTasks: () => {},
refreshMCPStatus: () => {},
clearError: () => {}
});
export const useTaskMaster = () => {
const context = useContext(TaskMasterContext);
if (!context) {
throw new Error('useTaskMaster must be used within a TaskMasterProvider');
}
return context;
};
export const TaskMasterProvider = ({ children }) => {
// Get WebSocket messages from shared context to avoid duplicate connections
const { messages } = useWebSocketContext();
// Authentication context
const { user, token, isLoading: authLoading } = useAuth();
// State
const [projects, setProjects] = useState([]);
const [currentProject, setCurrentProjectState] = useState(null);
const [projectTaskMaster, setProjectTaskMaster] = useState(null);
const [mcpServerStatus, setMCPServerStatus] = useState(null);
const [tasks, setTasks] = useState([]);
const [nextTask, setNextTask] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingTasks, setIsLoadingTasks] = useState(false);
const [isLoadingMCP, setIsLoadingMCP] = useState(false);
const [error, setError] = useState(null);
// Helper to handle API errors
const handleError = (error, context) => {
console.error(`TaskMaster ${context} error:`, error);
setError({
message: error.message || `Failed to ${context}`,
context,
timestamp: new Date().toISOString()
});
};
// Clear error state
const clearError = useCallback(() => {
setError(null);
}, []);
// This will be defined after the functions are declared
// Refresh projects with TaskMaster metadata
const refreshProjects = useCallback(async () => {
// Only make API calls if user is authenticated
if (!user || !token) {
setProjects([]);
setCurrentProjectState(null); // This might be the problem!
return;
}
try {
setIsLoading(true);
clearError();
const response = await api.get('/projects');
if (!response.ok) {
throw new Error(`Failed to fetch projects: ${response.status}`);
}
const projectsData = await response.json();
// Check if projectsData is an array
if (!Array.isArray(projectsData)) {
console.error('Projects API returned non-array data:', projectsData);
setProjects([]);
return;
}
// Filter and enrich projects with TaskMaster data
const enrichedProjects = projectsData.map(project => ({
...project,
taskMasterConfigured: project.taskmaster?.hasTaskmaster || false,
taskMasterStatus: project.taskmaster?.status || 'not-configured',
taskCount: project.taskmaster?.metadata?.taskCount || 0,
completedCount: project.taskmaster?.metadata?.completed || 0
}));
setProjects(enrichedProjects);
// If current project is set, update its TaskMaster data
if (currentProject) {
const updatedCurrent = enrichedProjects.find(p => p.name === currentProject.name);
if (updatedCurrent) {
setCurrentProjectState(updatedCurrent);
setProjectTaskMaster(updatedCurrent.taskmaster);
}
}
} catch (err) {
handleError(err, 'load projects');
} finally {
setIsLoading(false);
}
}, [user, token]); // Remove currentProject dependency to avoid infinite loops
// Set current project and load its TaskMaster details
const setCurrentProject = useCallback(async (project) => {
try {
setCurrentProjectState(project);
// Clear previous project's data immediately when switching projects
setTasks([]);
setNextTask(null);
setProjectTaskMaster(null); // Clear previous TaskMaster data
// Try to fetch fresh TaskMaster detection data for the project
if (project?.name) {
try {
const response = await api.get(`/taskmaster/detect/${encodeURIComponent(project.name)}`);
if (response.ok) {
const detectionData = await response.json();
setProjectTaskMaster(detectionData.taskmaster);
} else {
// If individual detection fails, fall back to project data from /api/projects
console.warn('Individual TaskMaster detection failed, using project data:', response.status);
setProjectTaskMaster(project.taskmaster || null);
}
} catch (detectionError) {
// If individual detection fails, fall back to project data from /api/projects
console.warn('TaskMaster detection error, using project data:', detectionError.message);
setProjectTaskMaster(project.taskmaster || null);
}
} else {
setProjectTaskMaster(null);
}
} catch (err) {
console.error('Error in setCurrentProject:', err);
handleError(err, 'set current project');
// Fall back to project data if available
setProjectTaskMaster(project?.taskmaster || null);
}
}, []);
// Refresh MCP server status
const refreshMCPStatus = useCallback(async () => {
// Only make API calls if user is authenticated
if (!user || !token) {
setMCPServerStatus(null);
return;
}
try {
setIsLoadingMCP(true);
clearError();
const mcpStatus = await api.get('/mcp-utils/taskmaster-server');
setMCPServerStatus(mcpStatus);
} catch (err) {
handleError(err, 'check MCP server status');
} finally {
setIsLoadingMCP(false);
}
}, [user, token]);
// Refresh tasks for current project - load real TaskMaster data
const refreshTasks = useCallback(async () => {
if (!currentProject) {
setTasks([]);
setNextTask(null);
return;
}
// Only make API calls if user is authenticated
if (!user || !token) {
setTasks([]);
setNextTask(null);
return;
}
try {
setIsLoadingTasks(true);
clearError();
// Load tasks from the TaskMaster API endpoint
const response = await api.get(`/taskmaster/tasks/${encodeURIComponent(currentProject.name)}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to load tasks');
}
const data = await response.json();
setTasks(data.tasks || []);
// Find next task (pending or in-progress)
const nextTask = data.tasks?.find(task =>
task.status === 'pending' || task.status === 'in-progress'
) || null;
setNextTask(nextTask);
} catch (err) {
console.error('Error loading tasks:', err);
handleError(err, 'load tasks');
// Set empty state on error
setTasks([]);
setNextTask(null);
} finally {
setIsLoadingTasks(false);
}
}, [currentProject, user, token]);
// Load initial data on mount or when auth changes
useEffect(() => {
if (!authLoading && user && token) {
refreshProjects();
refreshMCPStatus();
} else {
console.log('Auth not ready or no user, skipping project load:', { authLoading, user: !!user, token: !!token });
}
}, [refreshProjects, refreshMCPStatus, authLoading, user, token]);
// Clear errors when authentication changes
useEffect(() => {
if (user && token) {
clearError();
}
}, [user, token, clearError]);
// Refresh tasks when current project changes
useEffect(() => {
if (currentProject?.name && user && token) {
refreshTasks();
}
}, [currentProject?.name, user, token, refreshTasks]);
// Handle WebSocket messages for TaskMaster updates
useEffect(() => {
const latestMessage = messages[messages.length - 1];
if (!latestMessage) return;
switch (latestMessage.type) {
case 'taskmaster-project-updated':
// Refresh projects when TaskMaster state changes
if (latestMessage.projectName) {
refreshProjects();
}
break;
case 'taskmaster-tasks-updated':
// Refresh tasks for the current project
if (latestMessage.projectName === currentProject?.name) {
refreshTasks();
}
break;
case 'taskmaster-mcp-status-changed':
// Refresh MCP server status
refreshMCPStatus();
break;
default:
// Ignore non-TaskMaster messages
break;
}
}, [messages, refreshProjects, refreshTasks, refreshMCPStatus, currentProject]);
// Context value
const contextValue = {
// State
projects,
currentProject,
projectTaskMaster,
mcpServerStatus,
tasks,
nextTask,
isLoading,
isLoadingTasks,
isLoadingMCP,
error,
// Actions
refreshProjects,
setCurrentProject,
refreshTasks,
refreshMCPStatus,
clearError
};
return (
<TaskMasterContext.Provider value={contextValue}>
{children}
</TaskMasterContext.Provider>
);
};
export default TaskMasterContext;

View File

@@ -0,0 +1,95 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { api } from '../utils/api';
const TasksSettingsContext = createContext({
tasksEnabled: true,
setTasksEnabled: () => {},
toggleTasksEnabled: () => {},
isTaskMasterInstalled: null,
isTaskMasterReady: null,
installationStatus: null,
isCheckingInstallation: true
});
export const useTasksSettings = () => {
const context = useContext(TasksSettingsContext);
if (!context) {
throw new Error('useTasksSettings must be used within a TasksSettingsProvider');
}
return context;
};
export const TasksSettingsProvider = ({ children }) => {
const [tasksEnabled, setTasksEnabled] = useState(() => {
// Load from localStorage on initialization
const saved = localStorage.getItem('tasks-enabled');
return saved !== null ? JSON.parse(saved) : true; // Default to true
});
const [isTaskMasterInstalled, setIsTaskMasterInstalled] = useState(null);
const [isTaskMasterReady, setIsTaskMasterReady] = useState(null);
const [installationStatus, setInstallationStatus] = useState(null);
const [isCheckingInstallation, setIsCheckingInstallation] = useState(true);
// Save to localStorage whenever tasksEnabled changes
useEffect(() => {
localStorage.setItem('tasks-enabled', JSON.stringify(tasksEnabled));
}, [tasksEnabled]);
// Check TaskMaster installation status asynchronously on component mount
useEffect(() => {
const checkInstallation = async () => {
try {
const response = await api.get('/taskmaster/installation-status');
if (response.ok) {
const data = await response.json();
setInstallationStatus(data);
setIsTaskMasterInstalled(data.installation?.isInstalled || false);
setIsTaskMasterReady(data.isReady || false);
// If TaskMaster is not installed and user hasn't explicitly enabled tasks,
// disable tasks automatically
const userEnabledTasks = localStorage.getItem('tasks-enabled');
if (!data.installation?.isInstalled && !userEnabledTasks) {
setTasksEnabled(false);
}
} else {
console.error('Failed to check TaskMaster installation status');
setIsTaskMasterInstalled(false);
setIsTaskMasterReady(false);
}
} catch (error) {
console.error('Error checking TaskMaster installation:', error);
setIsTaskMasterInstalled(false);
setIsTaskMasterReady(false);
} finally {
setIsCheckingInstallation(false);
}
};
// Run check asynchronously without blocking initial render
setTimeout(checkInstallation, 0);
}, []);
const toggleTasksEnabled = () => {
setTasksEnabled(prev => !prev);
};
const contextValue = {
tasksEnabled,
setTasksEnabled,
toggleTasksEnabled,
isTaskMasterInstalled,
isTaskMasterReady,
installationStatus,
isCheckingInstallation
};
return (
<TasksSettingsContext.Provider value={contextValue}>
{children}
</TasksSettingsContext.Provider>
);
};
export default TasksSettingsContext;

View File

@@ -0,0 +1,29 @@
import React, { createContext, useContext } from 'react';
import { useWebSocket } from '../utils/websocket';
const WebSocketContext = createContext({
ws: null,
sendMessage: () => {},
messages: [],
isConnected: false
});
export const useWebSocketContext = () => {
const context = useContext(WebSocketContext);
if (!context) {
throw new Error('useWebSocketContext must be used within a WebSocketProvider');
}
return context;
};
export const WebSocketProvider = ({ children }) => {
const webSocketData = useWebSocket();
return (
<WebSocketContext.Provider value={webSocketData}>
{children}
</WebSocketContext.Provider>
);
};
export default WebSocketContext;