diff --git a/README.md b/README.md
index fcb505c..759fa18 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
- **File Explorer** - Interactive file tree with syntax highlighting and live editing
- **Git Explorer** - View, stage and commit your changes. You can also switch branches
- **Session Management** - Resume conversations, manage multiple sessions, and track history
+- **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation
- **Model Compatibility** - Works with Claude Sonnet 4, Opus 4.1, and GPT-5
@@ -109,6 +110,19 @@ To use Claude Code's full functionality, you'll need to manually enable tools:
**Recommended approach**: Start with basic tools enabled and add more as needed. You can always adjust these settings later.
+## TaskMaster AI Integration *(Optional)*
+
+Claude Code UI supports **[TaskMaster AI](https://github.com/eyaltoledano/claude-task-master)** (aka claude-task-master) integration for advanced project management and AI-powered task planning.
+
+It provides
+- AI-powered task generation from PRDs (Product Requirements Documents)
+- Smart task breakdown and dependency management
+- Visual task boards and progress tracking
+
+**Setup & Documentation**: Visit the [TaskMaster AI GitHub repository](https://github.com/eyaltoledano/claude-task-master) for installation instructions, configuration guides, and usage examples.
+After installing it you should be able to enable it from the Settings
+
+
## Usage Guide
### Core Features
@@ -136,6 +150,11 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a
#### Git Explorer
+#### TaskMaster AI Integration *(Optional)*
+- **Visual Task Board** - Kanban-style interface for managing development tasks
+- **PRD Parser** - Create Product Requirements Documents and parse them into structured tasks
+- **Progress Tracking** - Real-time status updates and completion tracking
+
#### Session Management
- **Session Persistence** - All conversations automatically saved
- **Session Organization** - Group sessions by project and timestamp
@@ -238,7 +257,7 @@ This project is open source and free to use, modify, and distribute under the GP
- **[Vite](https://vitejs.dev/)** - Fast build tool and dev server
- **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework
- **[CodeMirror](https://codemirror.net/)** - Advanced code editor
-
+- **[TaskMaster AI](https://github.com/eyaltoledano/claude-task-master)** *(Optional)* - AI-powered project management and task planning
## Support & Community
diff --git a/package.json b/package.json
index c5b308c..43175f2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "claude-code-ui",
- "version": "1.7.0",
+ "version": "v1.8.0",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",
diff --git a/server/index.js b/server/index.js
index ca45133..f074c57 100755
--- a/server/index.js
+++ b/server/index.js
@@ -43,6 +43,8 @@ import gitRoutes from './routes/git.js';
import authRoutes from './routes/auth.js';
import mcpRoutes from './routes/mcp.js';
import cursorRoutes from './routes/cursor.js';
+import taskmasterRoutes from './routes/taskmaster.js';
+import mcpUtilsRoutes from './routes/mcp-utils.js';
import { initializeDatabase } from './database/db.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -162,6 +164,9 @@ const wss = new WebSocketServer({
}
});
+// Make WebSocket server available to routes
+app.locals.wss = wss;
+
app.use(cors());
app.use(express.json());
@@ -180,6 +185,12 @@ app.use('/api/mcp', authenticateToken, mcpRoutes);
// Cursor API Routes (protected)
app.use('/api/cursor', authenticateToken, cursorRoutes);
+// TaskMaster API Routes (protected)
+app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
+
+// MCP utilities
+app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
+
// Static files served after API routes
app.use(express.static(path.join(__dirname, '../dist')));
@@ -547,16 +558,26 @@ function handleShellConnection(ws) {
const sessionId = data.sessionId;
const hasSession = data.hasSession;
const provider = data.provider || 'claude';
+ const initialCommand = data.initialCommand;
+ const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
console.log('🚀 Starting shell in:', projectPath);
- console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : 'New session');
- console.log('🤖 Provider:', provider);
+ console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
+ console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
+ if (initialCommand) {
+ console.log('⚡ Initial command:', initialCommand);
+ }
// First send a welcome message
- const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
- const welcomeMsg = hasSession ?
- `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
- `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
+ let welcomeMsg;
+ if (isPlainShell) {
+ welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
+ } else {
+ const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
+ welcomeMsg = hasSession ?
+ `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
+ `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
+ }
ws.send(JSON.stringify({
type: 'output',
@@ -566,7 +587,14 @@ function handleShellConnection(ws) {
try {
// Prepare the shell command adapted to the platform and provider
let shellCommand;
- if (provider === 'cursor') {
+ if (isPlainShell) {
+ // Plain shell mode - just run the initial command in the project directory
+ if (os.platform() === 'win32') {
+ shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
+ } else {
+ shellCommand = `cd "${projectPath}" && ${initialCommand}`;
+ }
+ } else if (provider === 'cursor') {
// Use cursor-agent command
if (os.platform() === 'win32') {
if (hasSession && sessionId) {
@@ -582,19 +610,20 @@ function handleShellConnection(ws) {
}
}
} else {
- // Use claude command (default)
+ // Use claude command (default) or initialCommand if provided
+ const command = initialCommand || 'claude';
if (os.platform() === 'win32') {
if (hasSession && sessionId) {
// Try to resume session, but with fallback to new session if it fails
shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`;
} else {
- shellCommand = `Set-Location -Path "${projectPath}"; claude`;
+ shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
}
} else {
if (hasSession && sessionId) {
shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`;
} else {
- shellCommand = `cd "${projectPath}" && claude`;
+ shellCommand = `cd "${projectPath}" && ${command}`;
}
}
}
diff --git a/server/projects.js b/server/projects.js
index 88d0e51..51178c9 100755
--- a/server/projects.js
+++ b/server/projects.js
@@ -66,6 +66,134 @@ import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import os from 'os';
+// Import TaskMaster detection functions
+async function detectTaskMasterFolder(projectPath) {
+ try {
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
+
+ // Check if .taskmaster directory exists
+ try {
+ const stats = await fs.stat(taskMasterPath);
+ if (!stats.isDirectory()) {
+ return {
+ hasTaskmaster: false,
+ reason: '.taskmaster exists but is not a directory'
+ };
+ }
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ return {
+ hasTaskmaster: false,
+ reason: '.taskmaster directory not found'
+ };
+ }
+ throw error;
+ }
+
+ // Check for key TaskMaster files
+ const keyFiles = [
+ 'tasks/tasks.json',
+ 'config.json'
+ ];
+
+ const fileStatus = {};
+ let hasEssentialFiles = true;
+
+ for (const file of keyFiles) {
+ const filePath = path.join(taskMasterPath, file);
+ try {
+ await fs.access(filePath);
+ fileStatus[file] = true;
+ } catch (error) {
+ fileStatus[file] = false;
+ if (file === 'tasks/tasks.json') {
+ hasEssentialFiles = false;
+ }
+ }
+ }
+
+ // Parse tasks.json if it exists for metadata
+ let taskMetadata = null;
+ if (fileStatus['tasks/tasks.json']) {
+ try {
+ const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
+ const tasksContent = await fs.readFile(tasksPath, 'utf8');
+ const tasksData = JSON.parse(tasksContent);
+
+ // Handle both tagged and legacy formats
+ let tasks = [];
+ if (tasksData.tasks) {
+ // Legacy format
+ tasks = tasksData.tasks;
+ } else {
+ // Tagged format - get tasks from all tags
+ Object.values(tasksData).forEach(tagData => {
+ if (tagData.tasks) {
+ tasks = tasks.concat(tagData.tasks);
+ }
+ });
+ }
+
+ // Calculate task statistics
+ const stats = tasks.reduce((acc, task) => {
+ acc.total++;
+ acc[task.status] = (acc[task.status] || 0) + 1;
+
+ // Count subtasks
+ if (task.subtasks) {
+ task.subtasks.forEach(subtask => {
+ acc.subtotalTasks++;
+ acc.subtasks = acc.subtasks || {};
+ acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
+ });
+ }
+
+ return acc;
+ }, {
+ total: 0,
+ subtotalTasks: 0,
+ pending: 0,
+ 'in-progress': 0,
+ done: 0,
+ review: 0,
+ deferred: 0,
+ cancelled: 0,
+ subtasks: {}
+ });
+
+ taskMetadata = {
+ taskCount: stats.total,
+ subtaskCount: stats.subtotalTasks,
+ completed: stats.done || 0,
+ pending: stats.pending || 0,
+ inProgress: stats['in-progress'] || 0,
+ review: stats.review || 0,
+ completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
+ lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
+ };
+ } catch (parseError) {
+ console.warn('Failed to parse tasks.json:', parseError.message);
+ taskMetadata = { error: 'Failed to parse tasks.json' };
+ }
+ }
+
+ return {
+ hasTaskmaster: true,
+ hasEssentialFiles,
+ files: fileStatus,
+ metadata: taskMetadata,
+ path: taskMasterPath
+ };
+
+ } catch (error) {
+ console.error('Error detecting TaskMaster folder:', error);
+ return {
+ hasTaskmaster: false,
+ reason: `Error checking directory: ${error.message}`
+ };
+ }
+}
+
// Cache for extracted project directories
const projectDirectoryCache = new Map();
@@ -298,6 +426,25 @@ async function getProjects() {
project.cursorSessions = [];
}
+ // Add TaskMaster detection
+ try {
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
+ project.taskmaster = {
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
+ metadata: taskMasterResult.metadata,
+ status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
+ };
+ } catch (e) {
+ console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
+ project.taskmaster = {
+ hasTaskmaster: false,
+ hasEssentialFiles: false,
+ metadata: null,
+ status: 'error'
+ };
+ }
+
projects.push(project);
}
}
@@ -341,6 +488,32 @@ async function getProjects() {
console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
}
+ // Add TaskMaster detection for manual projects
+ try {
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
+
+ // Determine TaskMaster status
+ let taskMasterStatus = 'not-configured';
+ if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
+ taskMasterStatus = 'taskmaster-only'; // We don't check MCP for manual projects in bulk
+ }
+
+ project.taskmaster = {
+ status: taskMasterStatus,
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
+ metadata: taskMasterResult.metadata
+ };
+ } catch (error) {
+ console.warn(`TaskMaster detection failed for manual project ${projectName}:`, error.message);
+ project.taskmaster = {
+ status: 'error',
+ hasTaskmaster: false,
+ hasEssentialFiles: false,
+ error: error.message
+ };
+ }
+
projects.push(project);
}
}
diff --git a/server/routes/mcp-utils.js b/server/routes/mcp-utils.js
new file mode 100644
index 0000000..8b3cd29
--- /dev/null
+++ b/server/routes/mcp-utils.js
@@ -0,0 +1,48 @@
+/**
+ * MCP UTILITIES API ROUTES
+ * ========================
+ *
+ * API endpoints for MCP server detection and configuration utilities.
+ * These endpoints expose centralized MCP detection functionality.
+ */
+
+import express from 'express';
+import { detectTaskMasterMCPServer, getAllMCPServers } from '../utils/mcp-detector.js';
+
+const router = express.Router();
+
+/**
+ * GET /api/mcp-utils/taskmaster-server
+ * Check if TaskMaster MCP server is configured
+ */
+router.get('/taskmaster-server', async (req, res) => {
+ try {
+ const result = await detectTaskMasterMCPServer();
+ res.json(result);
+ } catch (error) {
+ console.error('TaskMaster MCP detection error:', error);
+ res.status(500).json({
+ error: 'Failed to detect TaskMaster MCP server',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/mcp-utils/all-servers
+ * Get all configured MCP servers
+ */
+router.get('/all-servers', async (req, res) => {
+ try {
+ const result = await getAllMCPServers();
+ res.json(result);
+ } catch (error) {
+ console.error('MCP servers detection error:', error);
+ res.status(500).json({
+ error: 'Failed to get MCP servers',
+ message: error.message
+ });
+ }
+});
+
+export default router;
\ No newline at end of file
diff --git a/server/routes/taskmaster.js b/server/routes/taskmaster.js
new file mode 100644
index 0000000..913ca80
--- /dev/null
+++ b/server/routes/taskmaster.js
@@ -0,0 +1,1971 @@
+/**
+ * TASKMASTER API ROUTES
+ * ====================
+ *
+ * This module provides API endpoints for TaskMaster integration including:
+ * - .taskmaster folder detection in project directories
+ * - MCP server configuration detection
+ * - TaskMaster state and metadata management
+ */
+
+import express from 'express';
+import fs from 'fs';
+import path from 'path';
+import { promises as fsPromises } from 'fs';
+import { spawn } from 'child_process';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import os from 'os';
+import { extractProjectDirectory } from '../projects.js';
+import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
+import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const router = express.Router();
+
+/**
+ * Check if TaskMaster CLI is installed globally
+ * @returns {Promise} Installation status result
+ */
+async function checkTaskMasterInstallation() {
+ return new Promise((resolve) => {
+ // Check if task-master command is available
+ const child = spawn('which', ['task-master'], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ shell: true
+ });
+
+ let output = '';
+ let errorOutput = '';
+
+ child.stdout.on('data', (data) => {
+ output += data.toString();
+ });
+
+ child.stderr.on('data', (data) => {
+ errorOutput += data.toString();
+ });
+
+ child.on('close', (code) => {
+ if (code === 0 && output.trim()) {
+ // TaskMaster is installed, get version
+ const versionChild = spawn('task-master', ['--version'], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ shell: true
+ });
+
+ let versionOutput = '';
+
+ versionChild.stdout.on('data', (data) => {
+ versionOutput += data.toString();
+ });
+
+ versionChild.on('close', (versionCode) => {
+ resolve({
+ isInstalled: true,
+ installPath: output.trim(),
+ version: versionCode === 0 ? versionOutput.trim() : 'unknown',
+ reason: null
+ });
+ });
+
+ versionChild.on('error', () => {
+ resolve({
+ isInstalled: true,
+ installPath: output.trim(),
+ version: 'unknown',
+ reason: null
+ });
+ });
+ } else {
+ resolve({
+ isInstalled: false,
+ installPath: null,
+ version: null,
+ reason: 'TaskMaster CLI not found in PATH'
+ });
+ }
+ });
+
+ child.on('error', (error) => {
+ resolve({
+ isInstalled: false,
+ installPath: null,
+ version: null,
+ reason: `Error checking installation: ${error.message}`
+ });
+ });
+ });
+}
+
+/**
+ * Detect .taskmaster folder presence in a given project directory
+ * @param {string} projectPath - Absolute path to project directory
+ * @returns {Promise} Detection result with status and metadata
+ */
+async function detectTaskMasterFolder(projectPath) {
+ try {
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
+
+ // Check if .taskmaster directory exists
+ try {
+ const stats = await fsPromises.stat(taskMasterPath);
+ if (!stats.isDirectory()) {
+ return {
+ hasTaskmaster: false,
+ reason: '.taskmaster exists but is not a directory'
+ };
+ }
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ return {
+ hasTaskmaster: false,
+ reason: '.taskmaster directory not found'
+ };
+ }
+ throw error;
+ }
+
+ // Check for key TaskMaster files
+ const keyFiles = [
+ 'tasks/tasks.json',
+ 'config.json'
+ ];
+
+ const fileStatus = {};
+ let hasEssentialFiles = true;
+
+ for (const file of keyFiles) {
+ const filePath = path.join(taskMasterPath, file);
+ try {
+ await fsPromises.access(filePath, fs.constants.R_OK);
+ fileStatus[file] = true;
+ } catch (error) {
+ fileStatus[file] = false;
+ if (file === 'tasks/tasks.json') {
+ hasEssentialFiles = false;
+ }
+ }
+ }
+
+ // Parse tasks.json if it exists for metadata
+ let taskMetadata = null;
+ if (fileStatus['tasks/tasks.json']) {
+ try {
+ const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
+ const tasksContent = await fsPromises.readFile(tasksPath, 'utf8');
+ const tasksData = JSON.parse(tasksContent);
+
+ // Handle both tagged and legacy formats
+ let tasks = [];
+ if (tasksData.tasks) {
+ // Legacy format
+ tasks = tasksData.tasks;
+ } else {
+ // Tagged format - get tasks from all tags
+ Object.values(tasksData).forEach(tagData => {
+ if (tagData.tasks) {
+ tasks = tasks.concat(tagData.tasks);
+ }
+ });
+ }
+
+ // Calculate task statistics
+ const stats = tasks.reduce((acc, task) => {
+ acc.total++;
+ acc[task.status] = (acc[task.status] || 0) + 1;
+
+ // Count subtasks
+ if (task.subtasks) {
+ task.subtasks.forEach(subtask => {
+ acc.subtotalTasks++;
+ acc.subtasks = acc.subtasks || {};
+ acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
+ });
+ }
+
+ return acc;
+ }, {
+ total: 0,
+ subtotalTasks: 0,
+ pending: 0,
+ 'in-progress': 0,
+ done: 0,
+ review: 0,
+ deferred: 0,
+ cancelled: 0,
+ subtasks: {}
+ });
+
+ taskMetadata = {
+ taskCount: stats.total,
+ subtaskCount: stats.subtotalTasks,
+ completed: stats.done || 0,
+ pending: stats.pending || 0,
+ inProgress: stats['in-progress'] || 0,
+ review: stats.review || 0,
+ completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
+ lastModified: (await fsPromises.stat(tasksPath)).mtime.toISOString()
+ };
+ } catch (parseError) {
+ console.warn('Failed to parse tasks.json:', parseError.message);
+ taskMetadata = { error: 'Failed to parse tasks.json' };
+ }
+ }
+
+ return {
+ hasTaskmaster: true,
+ hasEssentialFiles,
+ files: fileStatus,
+ metadata: taskMetadata,
+ path: taskMasterPath
+ };
+
+ } catch (error) {
+ console.error('Error detecting TaskMaster folder:', error);
+ return {
+ hasTaskmaster: false,
+ reason: `Error checking directory: ${error.message}`
+ };
+ }
+}
+
+// MCP detection is now handled by the centralized utility
+
+// API Routes
+
+/**
+ * GET /api/taskmaster/installation-status
+ * Check if TaskMaster CLI is installed on the system
+ */
+router.get('/installation-status', async (req, res) => {
+ try {
+ const installationStatus = await checkTaskMasterInstallation();
+
+ // Also check for MCP server configuration
+ const mcpStatus = await detectTaskMasterMCPServer();
+
+ res.json({
+ success: true,
+ installation: installationStatus,
+ mcpServer: mcpStatus,
+ isReady: installationStatus.isInstalled && mcpStatus.hasMCPServer
+ });
+ } catch (error) {
+ console.error('Error checking TaskMaster installation:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Failed to check TaskMaster installation status',
+ installation: {
+ isInstalled: false,
+ reason: `Server error: ${error.message}`
+ },
+ mcpServer: {
+ hasMCPServer: false,
+ reason: `Server error: ${error.message}`
+ },
+ isReady: false
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/detect/:projectName
+ * Detect TaskMaster configuration for a specific project
+ */
+router.get('/detect/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+
+ // Use the existing extractProjectDirectory function to get actual project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ console.error('Error extracting project directory:', error);
+ return res.status(404).json({
+ error: 'Project path not found',
+ projectName,
+ message: error.message
+ });
+ }
+
+ // Verify the project path exists
+ try {
+ await fsPromises.access(projectPath, fs.constants.R_OK);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project path not accessible',
+ projectPath,
+ projectName,
+ message: error.message
+ });
+ }
+
+ // Run detection in parallel
+ const [taskMasterResult, mcpResult] = await Promise.all([
+ detectTaskMasterFolder(projectPath),
+ detectTaskMasterMCPServer()
+ ]);
+
+ // Determine overall status
+ let status = 'not-configured';
+ if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
+ if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
+ status = 'fully-configured';
+ } else {
+ status = 'taskmaster-only';
+ }
+ } else if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
+ status = 'mcp-only';
+ }
+
+ const responseData = {
+ projectName,
+ projectPath,
+ status,
+ taskmaster: taskMasterResult,
+ mcp: mcpResult,
+ timestamp: new Date().toISOString()
+ };
+
+ // Broadcast TaskMaster project update via WebSocket
+ if (req.app.locals.wss) {
+ broadcastTaskMasterProjectUpdate(
+ req.app.locals.wss,
+ projectName,
+ taskMasterResult
+ );
+ }
+
+ res.json(responseData);
+
+ } catch (error) {
+ console.error('TaskMaster detection error:', error);
+ res.status(500).json({
+ error: 'Failed to detect TaskMaster configuration',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/detect-all
+ * Detect TaskMaster configuration for all known projects
+ * This endpoint works with the existing projects system
+ */
+router.get('/detect-all', async (req, res) => {
+ try {
+ // Import getProjects from the projects module
+ const { getProjects } = await import('../projects.js');
+ const projects = await getProjects();
+
+ // Run detection for all projects in parallel
+ const detectionPromises = projects.map(async (project) => {
+ try {
+ // Use the project's fullPath if available, otherwise extract the directory
+ let projectPath;
+ if (project.fullPath) {
+ projectPath = project.fullPath;
+ } else {
+ try {
+ projectPath = await extractProjectDirectory(project.name);
+ } catch (error) {
+ throw new Error(`Failed to extract project directory: ${error.message}`);
+ }
+ }
+
+ const [taskMasterResult, mcpResult] = await Promise.all([
+ detectTaskMasterFolder(projectPath),
+ detectTaskMasterMCPServer()
+ ]);
+
+ // Determine status
+ let status = 'not-configured';
+ if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
+ if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
+ status = 'fully-configured';
+ } else {
+ status = 'taskmaster-only';
+ }
+ } else if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
+ status = 'mcp-only';
+ }
+
+ return {
+ projectName: project.name,
+ displayName: project.displayName,
+ projectPath,
+ status,
+ taskmaster: taskMasterResult,
+ mcp: mcpResult
+ };
+ } catch (error) {
+ return {
+ projectName: project.name,
+ displayName: project.displayName,
+ status: 'error',
+ error: error.message
+ };
+ }
+ });
+
+ const results = await Promise.all(detectionPromises);
+
+ res.json({
+ projects: results,
+ summary: {
+ total: results.length,
+ fullyConfigured: results.filter(p => p.status === 'fully-configured').length,
+ taskmasterOnly: results.filter(p => p.status === 'taskmaster-only').length,
+ mcpOnly: results.filter(p => p.status === 'mcp-only').length,
+ notConfigured: results.filter(p => p.status === 'not-configured').length,
+ errors: results.filter(p => p.status === 'error').length
+ },
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ console.error('Bulk TaskMaster detection error:', error);
+ res.status(500).json({
+ error: 'Failed to detect TaskMaster configuration for projects',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * POST /api/taskmaster/initialize/:projectName
+ * Initialize TaskMaster in a project (placeholder for future CLI integration)
+ */
+router.post('/initialize/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+ const { rules } = req.body; // Optional rule profiles
+
+ // This will be implemented in a later subtask with CLI integration
+ res.status(501).json({
+ error: 'TaskMaster initialization not yet implemented',
+ message: 'This endpoint will execute task-master init via CLI in a future update',
+ projectName,
+ rules
+ });
+
+ } catch (error) {
+ console.error('TaskMaster initialization error:', error);
+ res.status(500).json({
+ error: 'Failed to initialize TaskMaster',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/next/:projectName
+ * Get the next recommended task using task-master CLI
+ */
+router.get('/next/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ // Try to execute task-master next command
+ try {
+ const { spawn } = await import('child_process');
+
+ const nextTaskCommand = spawn('task-master', ['next'], {
+ cwd: projectPath,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ nextTaskCommand.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ nextTaskCommand.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ await new Promise((resolve, reject) => {
+ nextTaskCommand.on('close', (code) => {
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`task-master next failed with code ${code}: ${stderr}`));
+ }
+ });
+
+ nextTaskCommand.on('error', (error) => {
+ reject(error);
+ });
+ });
+
+ // Parse the output - task-master next usually returns JSON
+ let nextTaskData = null;
+ if (stdout.trim()) {
+ try {
+ nextTaskData = JSON.parse(stdout);
+ } catch (parseError) {
+ // If not JSON, treat as plain text
+ nextTaskData = { message: stdout.trim() };
+ }
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ nextTask: nextTaskData,
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (cliError) {
+ console.warn('Failed to execute task-master CLI:', cliError.message);
+
+ // Fallback to loading tasks and finding next one locally
+ const tasksResponse = await fetch(`${req.protocol}://${req.get('host')}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, {
+ headers: {
+ 'Authorization': req.headers.authorization
+ }
+ });
+
+ if (tasksResponse.ok) {
+ const tasksData = await tasksResponse.json();
+ const nextTask = tasksData.tasks?.find(task =>
+ task.status === 'pending' || task.status === 'in-progress'
+ ) || null;
+
+ res.json({
+ projectName,
+ projectPath,
+ nextTask,
+ fallback: true,
+ message: 'Used fallback method (CLI not available)',
+ timestamp: new Date().toISOString()
+ });
+ } else {
+ throw new Error('Failed to load tasks via fallback method');
+ }
+ }
+
+ } catch (error) {
+ console.error('TaskMaster next task error:', error);
+ res.status(500).json({
+ error: 'Failed to get next task',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/tasks/:projectName
+ * Load actual tasks from .taskmaster/tasks/tasks.json
+ */
+router.get('/tasks/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
+ const tasksFilePath = path.join(taskMasterPath, 'tasks', 'tasks.json');
+
+ // Check if tasks file exists
+ try {
+ await fsPromises.access(tasksFilePath);
+ } catch (error) {
+ return res.json({
+ projectName,
+ tasks: [],
+ message: 'No tasks.json file found'
+ });
+ }
+
+ // Read and parse tasks file
+ try {
+ const tasksContent = await fsPromises.readFile(tasksFilePath, 'utf8');
+ const tasksData = JSON.parse(tasksContent);
+
+ let tasks = [];
+ let currentTag = 'master';
+
+ // Handle both tagged and legacy formats
+ if (Array.isArray(tasksData)) {
+ // Legacy format
+ tasks = tasksData;
+ } else if (tasksData.tasks) {
+ // Simple format with tasks array
+ tasks = tasksData.tasks;
+ } else {
+ // Tagged format - get tasks from current tag or master
+ if (tasksData[currentTag] && tasksData[currentTag].tasks) {
+ tasks = tasksData[currentTag].tasks;
+ } else if (tasksData.master && tasksData.master.tasks) {
+ tasks = tasksData.master.tasks;
+ } else {
+ // Get tasks from first available tag
+ const firstTag = Object.keys(tasksData).find(key =>
+ tasksData[key].tasks && Array.isArray(tasksData[key].tasks)
+ );
+ if (firstTag) {
+ tasks = tasksData[firstTag].tasks;
+ currentTag = firstTag;
+ }
+ }
+ }
+
+ // Transform tasks to ensure all have required fields
+ const transformedTasks = tasks.map(task => ({
+ id: task.id,
+ title: task.title || 'Untitled Task',
+ description: task.description || '',
+ status: task.status || 'pending',
+ priority: task.priority || 'medium',
+ dependencies: task.dependencies || [],
+ createdAt: task.createdAt || task.created || new Date().toISOString(),
+ updatedAt: task.updatedAt || task.updated || new Date().toISOString(),
+ details: task.details || '',
+ testStrategy: task.testStrategy || task.test_strategy || '',
+ subtasks: task.subtasks || []
+ }));
+
+ res.json({
+ projectName,
+ projectPath,
+ tasks: transformedTasks,
+ currentTag,
+ totalTasks: transformedTasks.length,
+ tasksByStatus: {
+ pending: transformedTasks.filter(t => t.status === 'pending').length,
+ 'in-progress': transformedTasks.filter(t => t.status === 'in-progress').length,
+ done: transformedTasks.filter(t => t.status === 'done').length,
+ review: transformedTasks.filter(t => t.status === 'review').length,
+ deferred: transformedTasks.filter(t => t.status === 'deferred').length,
+ cancelled: transformedTasks.filter(t => t.status === 'cancelled').length
+ },
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (parseError) {
+ console.error('Failed to parse tasks.json:', parseError);
+ return res.status(500).json({
+ error: 'Failed to parse tasks file',
+ message: parseError.message
+ });
+ }
+
+ } catch (error) {
+ console.error('TaskMaster tasks loading error:', error);
+ res.status(500).json({
+ error: 'Failed to load TaskMaster tasks',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/prd/:projectName
+ * List all PRD files in the project's .taskmaster/docs directory
+ */
+router.get('/prd/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ const docsPath = path.join(projectPath, '.taskmaster', 'docs');
+
+ // Check if docs directory exists
+ try {
+ await fsPromises.access(docsPath, fs.constants.R_OK);
+ } catch (error) {
+ return res.json({
+ projectName,
+ prdFiles: [],
+ message: 'No .taskmaster/docs directory found'
+ });
+ }
+
+ // Read directory and filter for PRD files
+ try {
+ const files = await fsPromises.readdir(docsPath);
+ const prdFiles = [];
+
+ for (const file of files) {
+ const filePath = path.join(docsPath, file);
+ const stats = await fsPromises.stat(filePath);
+
+ if (stats.isFile() && (file.endsWith('.txt') || file.endsWith('.md'))) {
+ prdFiles.push({
+ name: file,
+ path: path.relative(projectPath, filePath),
+ size: stats.size,
+ modified: stats.mtime.toISOString(),
+ created: stats.birthtime.toISOString()
+ });
+ }
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ prdFiles: prdFiles.sort((a, b) => new Date(b.modified) - new Date(a.modified)),
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (readError) {
+ console.error('Error reading docs directory:', readError);
+ return res.status(500).json({
+ error: 'Failed to read PRD files',
+ message: readError.message
+ });
+ }
+
+ } catch (error) {
+ console.error('PRD list error:', error);
+ res.status(500).json({
+ error: 'Failed to list PRD files',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * POST /api/taskmaster/prd/:projectName
+ * Create or update a PRD file in the project's .taskmaster/docs directory
+ */
+router.post('/prd/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+ const { fileName, content } = req.body;
+
+ if (!fileName || !content) {
+ return res.status(400).json({
+ error: 'Missing required fields',
+ message: 'fileName and content are required'
+ });
+ }
+
+ // Validate filename
+ if (!fileName.match(/^[\w\-. ]+\.(txt|md)$/)) {
+ return res.status(400).json({
+ error: 'Invalid filename',
+ message: 'Filename must end with .txt or .md and contain only alphanumeric characters, spaces, dots, and dashes'
+ });
+ }
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ const docsPath = path.join(projectPath, '.taskmaster', 'docs');
+ const filePath = path.join(docsPath, fileName);
+
+ // Ensure docs directory exists
+ try {
+ await fsPromises.mkdir(docsPath, { recursive: true });
+ } catch (error) {
+ console.error('Failed to create docs directory:', error);
+ return res.status(500).json({
+ error: 'Failed to create directory',
+ message: error.message
+ });
+ }
+
+ // Write the PRD file
+ try {
+ await fsPromises.writeFile(filePath, content, 'utf8');
+
+ // Get file stats
+ const stats = await fsPromises.stat(filePath);
+
+ res.json({
+ projectName,
+ projectPath,
+ fileName,
+ filePath: path.relative(projectPath, filePath),
+ size: stats.size,
+ created: stats.birthtime.toISOString(),
+ modified: stats.mtime.toISOString(),
+ message: 'PRD file saved successfully',
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (writeError) {
+ console.error('Failed to write PRD file:', writeError);
+ return res.status(500).json({
+ error: 'Failed to write PRD file',
+ message: writeError.message
+ });
+ }
+
+ } catch (error) {
+ console.error('PRD create/update error:', error);
+ res.status(500).json({
+ error: 'Failed to create/update PRD file',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/prd/:projectName/:fileName
+ * Get content of a specific PRD file
+ */
+router.get('/prd/:projectName/:fileName', async (req, res) => {
+ try {
+ const { projectName, fileName } = req.params;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ const filePath = path.join(projectPath, '.taskmaster', 'docs', fileName);
+
+ // Check if file exists
+ try {
+ await fsPromises.access(filePath, fs.constants.R_OK);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'PRD file not found',
+ message: `File "${fileName}" does not exist`
+ });
+ }
+
+ // Read file content
+ try {
+ const content = await fsPromises.readFile(filePath, 'utf8');
+ const stats = await fsPromises.stat(filePath);
+
+ res.json({
+ projectName,
+ projectPath,
+ fileName,
+ filePath: path.relative(projectPath, filePath),
+ content,
+ size: stats.size,
+ created: stats.birthtime.toISOString(),
+ modified: stats.mtime.toISOString(),
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (readError) {
+ console.error('Failed to read PRD file:', readError);
+ return res.status(500).json({
+ error: 'Failed to read PRD file',
+ message: readError.message
+ });
+ }
+
+ } catch (error) {
+ console.error('PRD read error:', error);
+ res.status(500).json({
+ error: 'Failed to read PRD file',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * DELETE /api/taskmaster/prd/:projectName/:fileName
+ * Delete a specific PRD file
+ */
+router.delete('/prd/:projectName/:fileName', async (req, res) => {
+ try {
+ const { projectName, fileName } = req.params;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ const filePath = path.join(projectPath, '.taskmaster', 'docs', fileName);
+
+ // Check if file exists
+ try {
+ await fsPromises.access(filePath, fs.constants.F_OK);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'PRD file not found',
+ message: `File "${fileName}" does not exist`
+ });
+ }
+
+ // Delete the file
+ try {
+ await fsPromises.unlink(filePath);
+
+ res.json({
+ projectName,
+ projectPath,
+ fileName,
+ message: 'PRD file deleted successfully',
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (deleteError) {
+ console.error('Failed to delete PRD file:', deleteError);
+ return res.status(500).json({
+ error: 'Failed to delete PRD file',
+ message: deleteError.message
+ });
+ }
+
+ } catch (error) {
+ console.error('PRD delete error:', error);
+ res.status(500).json({
+ error: 'Failed to delete PRD file',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * POST /api/taskmaster/init/:projectName
+ * Initialize TaskMaster in a project
+ */
+router.post('/init/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ // Check if TaskMaster is already initialized
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
+ try {
+ await fsPromises.access(taskMasterPath, fs.constants.F_OK);
+ return res.status(400).json({
+ error: 'TaskMaster already initialized',
+ message: 'TaskMaster is already configured for this project'
+ });
+ } catch (error) {
+ // Directory doesn't exist, we can proceed
+ }
+
+ // Run taskmaster init command
+ const initProcess = spawn('npx', ['task-master', 'init'], {
+ cwd: projectPath,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ initProcess.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ initProcess.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ initProcess.on('close', (code) => {
+ if (code === 0) {
+ // Broadcast TaskMaster project update via WebSocket
+ if (req.app.locals.wss) {
+ broadcastTaskMasterProjectUpdate(
+ req.app.locals.wss,
+ projectName,
+ { hasTaskmaster: true, status: 'initialized' }
+ );
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ message: 'TaskMaster initialized successfully',
+ output: stdout,
+ timestamp: new Date().toISOString()
+ });
+ } else {
+ console.error('TaskMaster init failed:', stderr);
+ res.status(500).json({
+ error: 'Failed to initialize TaskMaster',
+ message: stderr || stdout,
+ code
+ });
+ }
+ });
+
+ // Send 'yes' responses to automated prompts
+ initProcess.stdin.write('yes\n');
+ initProcess.stdin.end();
+
+ } catch (error) {
+ console.error('TaskMaster init error:', error);
+ res.status(500).json({
+ error: 'Failed to initialize TaskMaster',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * POST /api/taskmaster/add-task/:projectName
+ * Add a new task to the project
+ */
+router.post('/add-task/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+ const { prompt, title, description, priority = 'medium', dependencies } = req.body;
+
+ if (!prompt && (!title || !description)) {
+ return res.status(400).json({
+ error: 'Missing required parameters',
+ message: 'Either "prompt" or both "title" and "description" are required'
+ });
+ }
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ // Build the task-master add-task command
+ const args = ['task-master-ai', 'add-task'];
+
+ if (prompt) {
+ args.push('--prompt', prompt);
+ args.push('--research'); // Use research for AI-generated tasks
+ } else {
+ args.push('--prompt', `Create a task titled "${title}" with description: ${description}`);
+ }
+
+ if (priority) {
+ args.push('--priority', priority);
+ }
+
+ if (dependencies) {
+ args.push('--dependencies', dependencies);
+ }
+
+ // Run task-master add-task command
+ const addTaskProcess = spawn('npx', args, {
+ cwd: projectPath,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ addTaskProcess.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ addTaskProcess.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ addTaskProcess.on('close', (code) => {
+ console.log('Add task process completed with code:', code);
+ console.log('Stdout:', stdout);
+ console.log('Stderr:', stderr);
+
+ if (code === 0) {
+ // Broadcast task update via WebSocket
+ if (req.app.locals.wss) {
+ broadcastTaskMasterTasksUpdate(
+ req.app.locals.wss,
+ projectName
+ );
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ message: 'Task added successfully',
+ output: stdout,
+ timestamp: new Date().toISOString()
+ });
+ } else {
+ console.error('Add task failed:', stderr);
+ res.status(500).json({
+ error: 'Failed to add task',
+ message: stderr || stdout,
+ code
+ });
+ }
+ });
+
+ addTaskProcess.stdin.end();
+
+ } catch (error) {
+ console.error('Add task error:', error);
+ res.status(500).json({
+ error: 'Failed to add task',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * PUT /api/taskmaster/update-task/:projectName/:taskId
+ * Update a specific task using TaskMaster CLI
+ */
+router.put('/update-task/:projectName/:taskId', async (req, res) => {
+ try {
+ const { projectName, taskId } = req.params;
+ const { title, description, status, priority, details } = req.body;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ // If only updating status, use set-status command
+ if (status && Object.keys(req.body).length === 1) {
+ const setStatusProcess = spawn('npx', ['task-master-ai', 'set-status', `--id=${taskId}`, `--status=${status}`], {
+ cwd: projectPath,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ setStatusProcess.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ setStatusProcess.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ setStatusProcess.on('close', (code) => {
+ if (code === 0) {
+ // Broadcast task update via WebSocket
+ if (req.app.locals.wss) {
+ broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ taskId,
+ message: 'Task status updated successfully',
+ output: stdout,
+ timestamp: new Date().toISOString()
+ });
+ } else {
+ console.error('Set task status failed:', stderr);
+ res.status(500).json({
+ error: 'Failed to update task status',
+ message: stderr || stdout,
+ code
+ });
+ }
+ });
+
+ setStatusProcess.stdin.end();
+ } else {
+ // For other updates, use update-task command with a prompt describing the changes
+ const updates = [];
+ if (title) updates.push(`title: "${title}"`);
+ if (description) updates.push(`description: "${description}"`);
+ if (priority) updates.push(`priority: "${priority}"`);
+ if (details) updates.push(`details: "${details}"`);
+
+ const prompt = `Update task with the following changes: ${updates.join(', ')}`;
+
+ const updateProcess = spawn('npx', ['task-master-ai', 'update-task', `--id=${taskId}`, `--prompt=${prompt}`], {
+ cwd: projectPath,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ updateProcess.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ updateProcess.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ updateProcess.on('close', (code) => {
+ if (code === 0) {
+ // Broadcast task update via WebSocket
+ if (req.app.locals.wss) {
+ broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ taskId,
+ message: 'Task updated successfully',
+ output: stdout,
+ timestamp: new Date().toISOString()
+ });
+ } else {
+ console.error('Update task failed:', stderr);
+ res.status(500).json({
+ error: 'Failed to update task',
+ message: stderr || stdout,
+ code
+ });
+ }
+ });
+
+ updateProcess.stdin.end();
+ }
+
+ } catch (error) {
+ console.error('Update task error:', error);
+ res.status(500).json({
+ error: 'Failed to update task',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * POST /api/taskmaster/parse-prd/:projectName
+ * Parse a PRD file to generate tasks
+ */
+router.post('/parse-prd/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+ const { fileName = 'prd.txt', numTasks, append = false } = req.body;
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ const prdPath = path.join(projectPath, '.taskmaster', 'docs', fileName);
+
+ // Check if PRD file exists
+ try {
+ await fsPromises.access(prdPath, fs.constants.F_OK);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'PRD file not found',
+ message: `File "${fileName}" does not exist in .taskmaster/docs/`
+ });
+ }
+
+ // Build the command args
+ const args = ['task-master-ai', 'parse-prd', prdPath];
+
+ if (numTasks) {
+ args.push('--num-tasks', numTasks.toString());
+ }
+
+ if (append) {
+ args.push('--append');
+ }
+
+ args.push('--research'); // Use research for better PRD parsing
+
+ // Run task-master parse-prd command
+ const parsePRDProcess = spawn('npx', args, {
+ cwd: projectPath,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ parsePRDProcess.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ parsePRDProcess.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ parsePRDProcess.on('close', (code) => {
+ if (code === 0) {
+ // Broadcast task update via WebSocket
+ if (req.app.locals.wss) {
+ broadcastTaskMasterTasksUpdate(
+ req.app.locals.wss,
+ projectName
+ );
+ }
+
+ res.json({
+ projectName,
+ projectPath,
+ prdFile: fileName,
+ message: 'PRD parsed and tasks generated successfully',
+ output: stdout,
+ timestamp: new Date().toISOString()
+ });
+ } else {
+ console.error('Parse PRD failed:', stderr);
+ res.status(500).json({
+ error: 'Failed to parse PRD',
+ message: stderr || stdout,
+ code
+ });
+ }
+ });
+
+ parsePRDProcess.stdin.end();
+
+ } catch (error) {
+ console.error('Parse PRD error:', error);
+ res.status(500).json({
+ error: 'Failed to parse PRD',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/taskmaster/prd-templates
+ * Get available PRD templates
+ */
+router.get('/prd-templates', async (req, res) => {
+ try {
+ // Return built-in templates
+ const templates = [
+ {
+ id: 'web-app',
+ name: 'Web Application',
+ description: 'Template for web application projects with frontend and backend components',
+ category: 'web',
+ content: `# Product Requirements Document - Web Application
+
+## Overview
+**Product Name:** [Your App Name]
+**Version:** 1.0
+**Date:** ${new Date().toISOString().split('T')[0]}
+**Author:** [Your Name]
+
+## Executive Summary
+Brief description of what this web application will do and why it's needed.
+
+## Product Goals
+- Goal 1: [Specific measurable goal]
+- Goal 2: [Specific measurable goal]
+- Goal 3: [Specific measurable goal]
+
+## User Stories
+### Core Features
+1. **User Registration & Authentication**
+ - As a user, I want to create an account so I can access personalized features
+ - As a user, I want to log in securely so my data is protected
+ - As a user, I want to reset my password if I forget it
+
+2. **Main Application Features**
+ - As a user, I want to [core feature 1] so I can [benefit]
+ - As a user, I want to [core feature 2] so I can [benefit]
+ - As a user, I want to [core feature 3] so I can [benefit]
+
+3. **User Interface**
+ - As a user, I want a responsive design so I can use the app on any device
+ - As a user, I want intuitive navigation so I can easily find features
+
+## Technical Requirements
+### Frontend
+- Framework: React/Vue/Angular or vanilla JavaScript
+- Styling: CSS framework (Tailwind, Bootstrap, etc.)
+- State Management: Redux/Vuex/Context API
+- Build Tools: Webpack/Vite
+- Testing: Jest/Vitest for unit tests
+
+### Backend
+- Runtime: Node.js/Python/Java
+- Database: PostgreSQL/MySQL/MongoDB
+- API: RESTful API or GraphQL
+- Authentication: JWT tokens
+- Testing: Integration and unit tests
+
+### Infrastructure
+- Hosting: Cloud provider (AWS, Azure, GCP)
+- CI/CD: GitHub Actions/GitLab CI
+- Monitoring: Application monitoring tools
+- Security: HTTPS, input validation, rate limiting
+
+## Success Metrics
+- User engagement metrics
+- Performance benchmarks (load time < 2s)
+- Error rates < 1%
+- User satisfaction scores
+
+## Timeline
+- Phase 1: Core functionality (4-6 weeks)
+- Phase 2: Advanced features (2-4 weeks)
+- Phase 3: Polish and launch (2 weeks)
+
+## Constraints & Assumptions
+- Budget constraints
+- Technical limitations
+- Team size and expertise
+- Timeline constraints`
+ },
+ {
+ id: 'api',
+ name: 'REST API',
+ description: 'Template for REST API development projects',
+ category: 'backend',
+ content: `# Product Requirements Document - REST API
+
+## Overview
+**API Name:** [Your API Name]
+**Version:** v1.0
+**Date:** ${new Date().toISOString().split('T')[0]}
+**Author:** [Your Name]
+
+## Executive Summary
+Description of the API's purpose, target users, and primary use cases.
+
+## API Goals
+- Goal 1: Provide secure data access
+- Goal 2: Ensure scalable architecture
+- Goal 3: Maintain high availability (99.9% uptime)
+
+## Functional Requirements
+### Core Endpoints
+1. **Authentication Endpoints**
+ - POST /api/auth/login - User authentication
+ - POST /api/auth/logout - User logout
+ - POST /api/auth/refresh - Token refresh
+ - POST /api/auth/register - User registration
+
+2. **Data Management Endpoints**
+ - GET /api/resources - List resources with pagination
+ - GET /api/resources/{id} - Get specific resource
+ - POST /api/resources - Create new resource
+ - PUT /api/resources/{id} - Update existing resource
+ - DELETE /api/resources/{id} - Delete resource
+
+3. **Administrative Endpoints**
+ - GET /api/admin/users - Manage users (admin only)
+ - GET /api/admin/analytics - System analytics
+ - POST /api/admin/backup - Trigger system backup
+
+## Technical Requirements
+### API Design
+- RESTful architecture following OpenAPI 3.0 specification
+- JSON request/response format
+- Consistent error response format
+- API versioning strategy
+
+### Authentication & Security
+- JWT token-based authentication
+- Role-based access control (RBAC)
+- Rate limiting (100 requests/minute per user)
+- Input validation and sanitization
+- HTTPS enforcement
+
+### Database
+- Database type: [PostgreSQL/MongoDB/MySQL]
+- Connection pooling
+- Database migrations
+- Backup and recovery procedures
+
+### Performance Requirements
+- Response time: < 200ms for 95% of requests
+- Throughput: 1000+ requests/second
+- Concurrent users: 10,000+
+- Database query optimization
+
+### Documentation
+- Auto-generated API documentation (Swagger/OpenAPI)
+- Code examples for common use cases
+- SDK development for major languages
+- Postman collection for testing
+
+## Error Handling
+- Standardized error codes and messages
+- Proper HTTP status codes
+- Detailed error logging
+- Graceful degradation strategies
+
+## Testing Strategy
+- Unit tests (80%+ coverage)
+- Integration tests for all endpoints
+- Load testing and performance testing
+- Security testing (OWASP compliance)
+
+## Monitoring & Logging
+- Application performance monitoring
+- Error tracking and alerting
+- Access logs and audit trails
+- Health check endpoints
+
+## Deployment
+- Containerized deployment (Docker)
+- CI/CD pipeline setup
+- Environment management (dev, staging, prod)
+- Blue-green deployment strategy
+
+## Success Metrics
+- API uptime > 99.9%
+- Average response time < 200ms
+- Zero critical security vulnerabilities
+- Developer adoption metrics`
+ },
+ {
+ id: 'mobile-app',
+ name: 'Mobile Application',
+ description: 'Template for mobile app development projects (iOS/Android)',
+ category: 'mobile',
+ content: `# Product Requirements Document - Mobile Application
+
+## Overview
+**App Name:** [Your App Name]
+**Platform:** iOS / Android / Cross-platform
+**Version:** 1.0
+**Date:** ${new Date().toISOString().split('T')[0]}
+**Author:** [Your Name]
+
+## Executive Summary
+Brief description of the mobile app's purpose, target audience, and key value proposition.
+
+## Product Goals
+- Goal 1: [Specific user engagement goal]
+- Goal 2: [Specific functionality goal]
+- Goal 3: [Specific performance goal]
+
+## User Stories
+### Core Features
+1. **Onboarding & Authentication**
+ - As a new user, I want a simple onboarding process
+ - As a user, I want to sign up with email or social media
+ - As a user, I want biometric authentication for security
+
+2. **Main App Features**
+ - As a user, I want [core feature 1] accessible from home screen
+ - As a user, I want [core feature 2] to work offline
+ - As a user, I want to sync data across devices
+
+3. **User Experience**
+ - As a user, I want intuitive navigation patterns
+ - As a user, I want fast loading times
+ - As a user, I want accessibility features
+
+## Technical Requirements
+### Mobile Development
+- **Cross-platform:** React Native / Flutter / Xamarin
+- **Native:** Swift (iOS) / Kotlin (Android)
+- **State Management:** Redux / MobX / Provider
+- **Navigation:** React Navigation / Flutter Navigation
+
+### Backend Integration
+- REST API or GraphQL integration
+- Real-time features (WebSockets/Push notifications)
+- Offline data synchronization
+- Background processing
+
+### Device Features
+- Camera and photo library access
+- GPS location services
+- Push notifications
+- Biometric authentication
+- Device storage
+
+### Performance Requirements
+- App launch time < 3 seconds
+- Screen transition animations < 300ms
+- Memory usage optimization
+- Battery usage optimization
+
+## Platform-Specific Considerations
+### iOS Requirements
+- iOS 13.0+ minimum version
+- App Store guidelines compliance
+- iOS design guidelines (Human Interface Guidelines)
+- TestFlight beta testing
+
+### Android Requirements
+- Android 8.0+ (API level 26) minimum
+- Google Play Store guidelines
+- Material Design guidelines
+- Google Play Console testing
+
+## User Interface Design
+- Responsive design for different screen sizes
+- Dark mode support
+- Accessibility compliance (WCAG 2.1)
+- Consistent design system
+
+## Security & Privacy
+- Secure data storage (Keychain/Keystore)
+- API communication encryption
+- Privacy policy compliance (GDPR/CCPA)
+- App security best practices
+
+## Testing Strategy
+- Unit testing (80%+ coverage)
+- UI/E2E testing (Detox/Appium)
+- Device testing on multiple screen sizes
+- Performance testing
+- Security testing
+
+## App Store Deployment
+- App store optimization (ASO)
+- App icons and screenshots
+- Store listing content
+- Release management strategy
+
+## Analytics & Monitoring
+- User analytics (Firebase/Analytics)
+- Crash reporting (Crashlytics/Sentry)
+- Performance monitoring
+- User feedback collection
+
+## Success Metrics
+- App store ratings > 4.0
+- User retention rates
+- Daily/Monthly active users
+- App performance metrics
+- Conversion rates`
+ },
+ {
+ id: 'data-analysis',
+ name: 'Data Analysis Project',
+ description: 'Template for data analysis and visualization projects',
+ category: 'data',
+ content: `# Product Requirements Document - Data Analysis Project
+
+## Overview
+**Project Name:** [Your Analysis Project]
+**Analysis Type:** [Descriptive/Predictive/Prescriptive]
+**Date:** ${new Date().toISOString().split('T')[0]}
+**Author:** [Your Name]
+
+## Executive Summary
+Description of the business problem, data sources, and expected insights.
+
+## Project Goals
+- Goal 1: [Specific business question to answer]
+- Goal 2: [Specific prediction to make]
+- Goal 3: [Specific recommendation to provide]
+
+## Business Requirements
+### Key Questions
+1. What patterns exist in the current data?
+2. What factors influence [target variable]?
+3. What predictions can be made for [future outcome]?
+4. What recommendations can improve [business metric]?
+
+### Success Criteria
+- Actionable insights for stakeholders
+- Statistical significance in findings
+- Reproducible analysis pipeline
+- Clear visualization and reporting
+
+## Data Requirements
+### Data Sources
+1. **Primary Data**
+ - Source: [Database/API/Files]
+ - Format: [CSV/JSON/SQL]
+ - Size: [Volume estimate]
+ - Update frequency: [Real-time/Daily/Monthly]
+
+2. **External Data**
+ - Third-party APIs
+ - Public datasets
+ - Market research data
+
+### Data Quality Requirements
+- Data completeness (< 5% missing values)
+- Data accuracy validation
+- Data consistency checks
+- Historical data availability
+
+## Technical Requirements
+### Data Pipeline
+- Data extraction and ingestion
+- Data cleaning and preprocessing
+- Data transformation and feature engineering
+- Data validation and quality checks
+
+### Analysis Tools
+- **Programming:** Python/R/SQL
+- **Libraries:** pandas, numpy, scikit-learn, matplotlib
+- **Visualization:** Tableau, PowerBI, or custom dashboards
+- **Version Control:** Git for code and DVC for data
+
+### Computing Resources
+- Local development environment
+- Cloud computing (AWS/GCP/Azure) if needed
+- Database access and permissions
+- Storage requirements
+
+## Analysis Methodology
+### Data Exploration
+1. Descriptive statistics and data profiling
+2. Data visualization and pattern identification
+3. Correlation analysis
+4. Outlier detection and handling
+
+### Statistical Analysis
+1. Hypothesis formulation
+2. Statistical testing
+3. Confidence intervals
+4. Effect size calculations
+
+### Machine Learning (if applicable)
+1. Feature selection and engineering
+2. Model selection and training
+3. Cross-validation and evaluation
+4. Model interpretation and explainability
+
+## Deliverables
+### Reports
+- Executive summary for stakeholders
+- Technical analysis report
+- Data quality report
+- Methodology documentation
+
+### Visualizations
+- Interactive dashboards
+- Static charts and graphs
+- Data story presentations
+- Key findings infographics
+
+### Code & Documentation
+- Reproducible analysis scripts
+- Data pipeline code
+- Documentation and comments
+- Testing and validation code
+
+## Timeline
+- Phase 1: Data collection and exploration (2 weeks)
+- Phase 2: Analysis and modeling (3 weeks)
+- Phase 3: Reporting and visualization (1 week)
+- Phase 4: Stakeholder presentation (1 week)
+
+## Risks & Assumptions
+- Data availability and quality risks
+- Technical complexity assumptions
+- Resource and timeline constraints
+- Stakeholder engagement assumptions
+
+## Success Metrics
+- Stakeholder satisfaction with insights
+- Accuracy of predictions (if applicable)
+- Business impact of recommendations
+- Reproducibility of results`
+ }
+ ];
+
+ res.json({
+ templates,
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ console.error('PRD templates error:', error);
+ res.status(500).json({
+ error: 'Failed to get PRD templates',
+ message: error.message
+ });
+ }
+});
+
+/**
+ * POST /api/taskmaster/apply-template/:projectName
+ * Apply a PRD template to create a new PRD file
+ */
+router.post('/apply-template/:projectName', async (req, res) => {
+ try {
+ const { projectName } = req.params;
+ const { templateId, fileName = 'prd.txt', customizations = {} } = req.body;
+
+ if (!templateId) {
+ return res.status(400).json({
+ error: 'Missing required parameter',
+ message: 'templateId is required'
+ });
+ }
+
+ // Get project path
+ let projectPath;
+ try {
+ projectPath = await extractProjectDirectory(projectName);
+ } catch (error) {
+ return res.status(404).json({
+ error: 'Project not found',
+ message: `Project "${projectName}" does not exist`
+ });
+ }
+
+ // Get the template content (this would normally fetch from the templates list)
+ const templates = await getAvailableTemplates();
+ const template = templates.find(t => t.id === templateId);
+
+ if (!template) {
+ return res.status(404).json({
+ error: 'Template not found',
+ message: `Template "${templateId}" does not exist`
+ });
+ }
+
+ // Apply customizations to template content
+ let content = template.content;
+
+ // Replace placeholders with customizations
+ for (const [key, value] of Object.entries(customizations)) {
+ const placeholder = `[${key}]`;
+ content = content.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), value);
+ }
+
+ // Ensure .taskmaster/docs directory exists
+ const docsDir = path.join(projectPath, '.taskmaster', 'docs');
+ try {
+ await fsPromises.mkdir(docsDir, { recursive: true });
+ } catch (error) {
+ console.error('Failed to create docs directory:', error);
+ }
+
+ const filePath = path.join(docsDir, fileName);
+
+ // Write the template content to the file
+ try {
+ await fsPromises.writeFile(filePath, content, 'utf8');
+
+ res.json({
+ projectName,
+ projectPath,
+ templateId,
+ templateName: template.name,
+ fileName,
+ filePath: filePath,
+ message: 'PRD template applied successfully',
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (writeError) {
+ console.error('Failed to write PRD template:', writeError);
+ return res.status(500).json({
+ error: 'Failed to write PRD template',
+ message: writeError.message
+ });
+ }
+
+ } catch (error) {
+ console.error('Apply template error:', error);
+ res.status(500).json({
+ error: 'Failed to apply PRD template',
+ message: error.message
+ });
+ }
+});
+
+// Helper function to get available templates
+async function getAvailableTemplates() {
+ // This could be extended to read from files or database
+ return [
+ {
+ id: 'web-app',
+ name: 'Web Application',
+ description: 'Template for web application projects',
+ category: 'web',
+ content: `# Product Requirements Document - Web Application
+
+## Overview
+**Product Name:** [Your App Name]
+**Version:** 1.0
+**Date:** ${new Date().toISOString().split('T')[0]}
+**Author:** [Your Name]
+
+## Executive Summary
+Brief description of what this web application will do and why it's needed.
+
+## User Stories
+1. As a user, I want [feature] so I can [benefit]
+2. As a user, I want [feature] so I can [benefit]
+3. As a user, I want [feature] so I can [benefit]
+
+## Technical Requirements
+- Frontend framework
+- Backend services
+- Database requirements
+- Security considerations
+
+## Success Metrics
+- User engagement metrics
+- Performance benchmarks
+- Business objectives`
+ },
+ // Add other templates here if needed
+ ];
+}
+
+export default router;
\ No newline at end of file
diff --git a/server/utils/mcp-detector.js b/server/utils/mcp-detector.js
new file mode 100644
index 0000000..4439353
--- /dev/null
+++ b/server/utils/mcp-detector.js
@@ -0,0 +1,198 @@
+/**
+ * MCP SERVER DETECTION UTILITY
+ * ============================
+ *
+ * Centralized utility for detecting MCP server configurations.
+ * Used across TaskMaster integration and other MCP-dependent features.
+ */
+
+import { promises as fsPromises } from 'fs';
+import path from 'path';
+import os from 'os';
+
+/**
+ * Check if task-master-ai MCP server is configured
+ * Reads directly from Claude configuration files like claude-cli.js does
+ * @returns {Promise} MCP detection result
+ */
+export async function detectTaskMasterMCPServer() {
+ try {
+ // Read Claude configuration files directly (same logic as mcp.js)
+ const homeDir = os.homedir();
+ const configPaths = [
+ path.join(homeDir, '.claude.json'),
+ path.join(homeDir, '.claude', 'settings.json')
+ ];
+
+ let configData = null;
+ let configPath = null;
+
+ // Try to read from either config file
+ for (const filepath of configPaths) {
+ try {
+ const fileContent = await fsPromises.readFile(filepath, 'utf8');
+ configData = JSON.parse(fileContent);
+ configPath = filepath;
+ break;
+ } catch (error) {
+ // File doesn't exist or is not valid JSON, try next
+ continue;
+ }
+ }
+
+ if (!configData) {
+ return {
+ hasMCPServer: false,
+ reason: 'No Claude configuration file found',
+ hasConfig: false
+ };
+ }
+
+ // Look for task-master-ai in user-scoped MCP servers
+ let taskMasterServer = null;
+ if (configData.mcpServers && typeof configData.mcpServers === 'object') {
+ const serverEntry = Object.entries(configData.mcpServers).find(([name, config]) =>
+ name === 'task-master-ai' ||
+ name.includes('task-master') ||
+ (config && config.command && config.command.includes('task-master'))
+ );
+
+ if (serverEntry) {
+ const [name, config] = serverEntry;
+ taskMasterServer = {
+ name,
+ scope: 'user',
+ config,
+ type: config.command ? 'stdio' : (config.url ? 'http' : 'unknown')
+ };
+ }
+ }
+
+ // Also check project-specific MCP servers if not found globally
+ if (!taskMasterServer && configData.projects) {
+ for (const [projectPath, projectConfig] of Object.entries(configData.projects)) {
+ if (projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
+ const serverEntry = Object.entries(projectConfig.mcpServers).find(([name, config]) =>
+ name === 'task-master-ai' ||
+ name.includes('task-master') ||
+ (config && config.command && config.command.includes('task-master'))
+ );
+
+ if (serverEntry) {
+ const [name, config] = serverEntry;
+ taskMasterServer = {
+ name,
+ scope: 'local',
+ projectPath,
+ config,
+ type: config.command ? 'stdio' : (config.url ? 'http' : 'unknown')
+ };
+ break;
+ }
+ }
+ }
+ }
+
+ if (taskMasterServer) {
+ const isValid = !!(taskMasterServer.config &&
+ (taskMasterServer.config.command || taskMasterServer.config.url));
+ const hasEnvVars = !!(taskMasterServer.config &&
+ taskMasterServer.config.env &&
+ Object.keys(taskMasterServer.config.env).length > 0);
+
+ return {
+ hasMCPServer: true,
+ isConfigured: isValid,
+ hasApiKeys: hasEnvVars,
+ scope: taskMasterServer.scope,
+ config: {
+ command: taskMasterServer.config?.command,
+ args: taskMasterServer.config?.args || [],
+ url: taskMasterServer.config?.url,
+ envVars: hasEnvVars ? Object.keys(taskMasterServer.config.env) : [],
+ type: taskMasterServer.type
+ }
+ };
+ } else {
+ // Get list of available servers for debugging
+ const availableServers = [];
+ if (configData.mcpServers) {
+ availableServers.push(...Object.keys(configData.mcpServers));
+ }
+ if (configData.projects) {
+ for (const projectConfig of Object.values(configData.projects)) {
+ if (projectConfig.mcpServers) {
+ availableServers.push(...Object.keys(projectConfig.mcpServers).map(name => `local:${name}`));
+ }
+ }
+ }
+
+ return {
+ hasMCPServer: false,
+ reason: 'task-master-ai not found in configured MCP servers',
+ hasConfig: true,
+ configPath,
+ availableServers
+ };
+ }
+ } catch (error) {
+ console.error('Error detecting MCP server config:', error);
+ return {
+ hasMCPServer: false,
+ reason: `Error checking MCP config: ${error.message}`,
+ hasConfig: false
+ };
+ }
+}
+
+/**
+ * Get all configured MCP servers (not just TaskMaster)
+ * @returns {Promise} All MCP servers configuration
+ */
+export async function getAllMCPServers() {
+ try {
+ const homeDir = os.homedir();
+ const configPaths = [
+ path.join(homeDir, '.claude.json'),
+ path.join(homeDir, '.claude', 'settings.json')
+ ];
+
+ let configData = null;
+ let configPath = null;
+
+ // Try to read from either config file
+ for (const filepath of configPaths) {
+ try {
+ const fileContent = await fsPromises.readFile(filepath, 'utf8');
+ configData = JSON.parse(fileContent);
+ configPath = filepath;
+ break;
+ } catch (error) {
+ continue;
+ }
+ }
+
+ if (!configData) {
+ return {
+ hasConfig: false,
+ servers: {},
+ projectServers: {}
+ };
+ }
+
+ return {
+ hasConfig: true,
+ configPath,
+ servers: configData.mcpServers || {},
+ projectServers: configData.projects || {}
+ };
+ } catch (error) {
+ console.error('Error getting all MCP servers:', error);
+ return {
+ hasConfig: false,
+ error: error.message,
+ servers: {},
+ projectServers: {}
+ };
+ }
+}
\ No newline at end of file
diff --git a/server/utils/taskmaster-websocket.js b/server/utils/taskmaster-websocket.js
new file mode 100644
index 0000000..87c0549
--- /dev/null
+++ b/server/utils/taskmaster-websocket.js
@@ -0,0 +1,129 @@
+/**
+ * TASKMASTER WEBSOCKET UTILITIES
+ * ==============================
+ *
+ * Utilities for broadcasting TaskMaster state changes via WebSocket.
+ * Integrates with the existing WebSocket system to provide real-time updates.
+ */
+
+/**
+ * Broadcast TaskMaster project update to all connected clients
+ * @param {WebSocket.Server} wss - WebSocket server instance
+ * @param {string} projectName - Name of the updated project
+ * @param {Object} taskMasterData - Updated TaskMaster data
+ */
+export function broadcastTaskMasterProjectUpdate(wss, projectName, taskMasterData) {
+ if (!wss || !projectName) {
+ console.warn('TaskMaster WebSocket broadcast: Missing wss or projectName');
+ return;
+ }
+
+ const message = {
+ type: 'taskmaster-project-updated',
+ projectName,
+ taskMasterData,
+ timestamp: new Date().toISOString()
+ };
+
+
+ wss.clients.forEach((client) => {
+ if (client.readyState === 1) { // WebSocket.OPEN
+ try {
+ client.send(JSON.stringify(message));
+ } catch (error) {
+ console.error('Error sending TaskMaster project update:', error);
+ }
+ }
+ });
+}
+
+/**
+ * Broadcast TaskMaster tasks update for a specific project
+ * @param {WebSocket.Server} wss - WebSocket server instance
+ * @param {string} projectName - Name of the project with updated tasks
+ * @param {Object} tasksData - Updated tasks data
+ */
+export function broadcastTaskMasterTasksUpdate(wss, projectName, tasksData) {
+ if (!wss || !projectName) {
+ console.warn('TaskMaster WebSocket broadcast: Missing wss or projectName');
+ return;
+ }
+
+ const message = {
+ type: 'taskmaster-tasks-updated',
+ projectName,
+ tasksData,
+ timestamp: new Date().toISOString()
+ };
+
+
+ wss.clients.forEach((client) => {
+ if (client.readyState === 1) { // WebSocket.OPEN
+ try {
+ client.send(JSON.stringify(message));
+ } catch (error) {
+ console.error('Error sending TaskMaster tasks update:', error);
+ }
+ }
+ });
+}
+
+/**
+ * Broadcast MCP server status change
+ * @param {WebSocket.Server} wss - WebSocket server instance
+ * @param {Object} mcpStatus - Updated MCP server status
+ */
+export function broadcastMCPStatusChange(wss, mcpStatus) {
+ if (!wss) {
+ console.warn('TaskMaster WebSocket broadcast: Missing wss');
+ return;
+ }
+
+ const message = {
+ type: 'taskmaster-mcp-status-changed',
+ mcpStatus,
+ timestamp: new Date().toISOString()
+ };
+
+
+ wss.clients.forEach((client) => {
+ if (client.readyState === 1) { // WebSocket.OPEN
+ try {
+ client.send(JSON.stringify(message));
+ } catch (error) {
+ console.error('Error sending TaskMaster MCP status update:', error);
+ }
+ }
+ });
+}
+
+/**
+ * Broadcast general TaskMaster update notification
+ * @param {WebSocket.Server} wss - WebSocket server instance
+ * @param {string} updateType - Type of update (e.g., 'initialization', 'configuration')
+ * @param {Object} data - Additional data about the update
+ */
+export function broadcastTaskMasterUpdate(wss, updateType, data = {}) {
+ if (!wss || !updateType) {
+ console.warn('TaskMaster WebSocket broadcast: Missing wss or updateType');
+ return;
+ }
+
+ const message = {
+ type: 'taskmaster-update',
+ updateType,
+ data,
+ timestamp: new Date().toISOString()
+ };
+
+
+ wss.clients.forEach((client) => {
+ if (client.readyState === 1) { // WebSocket.OPEN
+ try {
+ client.send(JSON.stringify(message));
+ } catch (error) {
+ console.error('Error sending TaskMaster update:', error);
+ }
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index 1cbd7eb..ec0278d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -26,9 +26,11 @@ import MobileNav from './components/MobileNav';
import ToolsSettings from './components/ToolsSettings';
import QuickSettingsPanel from './components/QuickSettingsPanel';
-import { useWebSocket } from './utils/websocket';
import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider } from './contexts/AuthContext';
+import { TaskMasterProvider } from './contexts/TaskMasterContext';
+import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
+import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext';
import ProtectedRoute from './components/ProtectedRoute';
import { useVersionCheck } from './hooks/useVersionCheck';
import { api, authenticatedFetch } from './utils/api';
@@ -74,7 +76,7 @@ function AppContent() {
// until the conversation completes or is aborted.
const [activeSessions, setActiveSessions] = useState(new Set()); // Track sessions with active conversations
- const { ws, sendMessage, messages } = useWebSocket();
+ const { ws, sendMessage, messages } = useWebSocketContext();
useEffect(() => {
const checkMobile = () => {
@@ -690,14 +692,20 @@ function App() {
return (
-
-
-
- } />
- } />
-
-
-
+
+
+
+
+
+
+ } />
+ } />
+
+
+
+
+
+
);
diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx
index 26820b2..e38efcd 100644
--- a/src/components/ChatInterface.jsx
+++ b/src/components/ChatInterface.jsx
@@ -22,6 +22,8 @@ import { useDropzone } from 'react-dropzone';
import TodoList from './TodoList';
import ClaudeLogo from './ClaudeLogo.jsx';
import CursorLogo from './CursorLogo.jsx';
+import NextTaskBanner from './NextTaskBanner.jsx';
+import { useTasksSettings } from '../contexts/TasksSettingsContext';
import ClaudeStatus from './ClaudeStatus';
import { MicButton } from './MicButton.jsx';
@@ -1162,7 +1164,8 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
//
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
-function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) {
+function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter, onTaskClick, onShowAllTasks }) {
+ const { tasksEnabled } = useTasksSettings();
const [input, setInput] = useState(() => {
if (typeof window !== 'undefined' && selectedProject) {
return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
@@ -3092,6 +3095,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
: 'Select a provider above to begin'
}
+
+ {/* Show NextTaskBanner when provider is selected and ready */}
+ {provider && tasksEnabled && (
+
+ setInput('Start the next task')}
+ onShowAllTasks={onShowAllTasks}
+ />
+
+ )}
)}
{selectedSession && (
@@ -3100,6 +3113,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
Ask questions about your code, request changes, or get help with development tasks
+
+ {/* Show NextTaskBanner for existing sessions too */}
+ {tasksEnabled && (
+
+ setInput('Start the next task')}
+ onShowAllTasks={onShowAllTasks}
+ />
+
+ )}
)}
diff --git a/src/components/CreateTaskModal.jsx b/src/components/CreateTaskModal.jsx
new file mode 100644
index 0000000..81af285
--- /dev/null
+++ b/src/components/CreateTaskModal.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { X, Sparkles } from 'lucide-react';
+
+const CreateTaskModal = ({ currentProject, onClose, onTaskCreated }) => {
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
Create AI-Generated Task
+
+
+
+
+
+
+ {/* Content */}
+
+ {/* AI-First Approach */}
+
+
+
+
+
+
+
+ 💡 Pro Tip: Ask Claude Code Directly!
+
+
+ You can simply ask Claude Code in the chat to create tasks for you.
+ The AI assistant will automatically generate detailed tasks with research-backed insights.
+
+
+
+
Example:
+
+ "Please add a new task to implement user profile image uploads using Cloudinary, research the best approach."
+
+
+
+
+ This runs:
+ task-master add-task --prompt="Implement user profile image uploads using Cloudinary" --research
+
+
+
+
+
+
+ {/* Learn More Link */}
+
+
+ {/* Footer */}
+
+
+ Got it, I'll ask Claude Code directly
+
+
+
+
+
+ );
+};
+
+export default CreateTaskModal;
\ No newline at end of file
diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx
index 09a210a..5bbbc7a 100644
--- a/src/components/MainContent.jsx
+++ b/src/components/MainContent.jsx
@@ -20,6 +20,13 @@ import GitPanel from './GitPanel';
import ErrorBoundary from './ErrorBoundary';
import ClaudeLogo from './ClaudeLogo';
import CursorLogo from './CursorLogo';
+import TaskList from './TaskList';
+import TaskDetail from './TaskDetail';
+import PRDEditor from './PRDEditor';
+import Tooltip from './Tooltip';
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+import { useTasksSettings } from '../contexts/TasksSettingsContext';
+import { api } from '../utils/api';
function MainContent({
selectedProject,
@@ -46,6 +53,60 @@ function MainContent({
sendByCtrlEnter // Send by Ctrl+Enter mode for East Asian language input
}) {
const [editingFile, setEditingFile] = useState(null);
+ const [selectedTask, setSelectedTask] = useState(null);
+ const [showTaskDetail, setShowTaskDetail] = useState(false);
+
+ // PRD Editor state
+ const [showPRDEditor, setShowPRDEditor] = useState(false);
+ const [selectedPRD, setSelectedPRD] = useState(null);
+ const [existingPRDs, setExistingPRDs] = useState([]);
+ const [prdNotification, setPRDNotification] = useState(null);
+
+ // TaskMaster context
+ const { tasks, currentProject, refreshTasks, setCurrentProject } = useTaskMaster();
+ const { tasksEnabled, isTaskMasterInstalled, isTaskMasterReady } = useTasksSettings();
+
+ // Only show tasks tab if TaskMaster is installed and enabled
+ const shouldShowTasksTab = tasksEnabled && isTaskMasterInstalled;
+
+ // Sync selectedProject with TaskMaster context
+ useEffect(() => {
+ if (selectedProject && selectedProject !== currentProject) {
+ setCurrentProject(selectedProject);
+ }
+ }, [selectedProject, currentProject, setCurrentProject]);
+
+ // Switch away from tasks tab when tasks are disabled or TaskMaster is not installed
+ useEffect(() => {
+ if (!shouldShowTasksTab && activeTab === 'tasks') {
+ setActiveTab('chat');
+ }
+ }, [shouldShowTasksTab, activeTab, setActiveTab]);
+
+ // Load existing PRDs when current project changes
+ useEffect(() => {
+ const loadExistingPRDs = async () => {
+ if (!currentProject?.name) {
+ setExistingPRDs([]);
+ return;
+ }
+
+ try {
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
+ if (response.ok) {
+ const data = await response.json();
+ setExistingPRDs(data.prdFiles || []);
+ } else {
+ setExistingPRDs([]);
+ }
+ } catch (error) {
+ console.error('Failed to load existing PRDs:', error);
+ setExistingPRDs([]);
+ }
+ };
+
+ loadExistingPRDs();
+ }, [currentProject?.name]);
const handleFileOpen = (filePath, diffInfo = null) => {
// Create a file object that CodeEditor expects
@@ -61,6 +122,31 @@ function MainContent({
const handleCloseEditor = () => {
setEditingFile(null);
};
+
+ const handleTaskClick = (task) => {
+ // If task is just an ID (from dependency click), find the full task object
+ if (typeof task === 'object' && task.id && !task.title) {
+ const fullTask = tasks?.find(t => t.id === task.id);
+ if (fullTask) {
+ setSelectedTask(fullTask);
+ setShowTaskDetail(true);
+ }
+ } else {
+ setSelectedTask(task);
+ setShowTaskDetail(true);
+ }
+ };
+
+ const handleTaskDetailClose = () => {
+ setShowTaskDetail(false);
+ setSelectedTask(null);
+ };
+
+ const handleTaskStatusChange = (taskId, newStatus) => {
+ // This would integrate with TaskMaster API to update task status
+ console.log('Update task status:', taskId, newStatus);
+ refreshTasks?.();
+ };
if (isLoading) {
return (
@@ -187,7 +273,10 @@ function MainContent({
) : (
- {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
+ {activeTab === 'files' ? 'Project Files' :
+ activeTab === 'git' ? 'Source Control' :
+ (activeTab === 'tasks' && shouldShowTasksTab) ? 'TaskMaster' :
+ 'Project'}
{selectedProject.displayName}
@@ -201,66 +290,93 @@ function MainContent({
{/* Modern Tab Navigation - Right Side */}
-
setActiveTab('chat')}
- className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
- activeTab === 'chat'
- ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
- }`}
- >
-
-
-
-
- Chat
-
-
-
setActiveTab('shell')}
- className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
- activeTab === 'shell'
- ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
- }`}
- >
-
-
-
-
- Shell
-
-
-
setActiveTab('files')}
- className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
- activeTab === 'files'
- ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
- }`}
- >
-
-
-
-
- Files
-
-
-
setActiveTab('git')}
- className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
- activeTab === 'git'
- ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
- }`}
- >
-
-
-
-
- Source Control
-
-
+
+ setActiveTab('chat')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
+ activeTab === 'chat'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Chat
+
+
+
+
+ setActiveTab('shell')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ activeTab === 'shell'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Shell
+
+
+
+
+ setActiveTab('files')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ activeTab === 'files'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Files
+
+
+
+
+ setActiveTab('git')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ activeTab === 'git'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Source Control
+
+
+
+ {shouldShowTasksTab && (
+
+ setActiveTab('tasks')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ activeTab === 'tasks'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Tasks
+
+
+
+ )}
{/*
setActiveTab('preview')}
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
@@ -302,6 +418,7 @@ function MainContent({
showRawParameters={showRawParameters}
autoScrollToBottom={autoScrollToBottom}
sendByCtrlEnter={sendByCtrlEnter}
+ onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
/>
@@ -318,6 +435,40 @@ function MainContent({
+ {shouldShowTasksTab && (
+
+
+ {
+ setSelectedPRD(prd);
+ setShowPRDEditor(true);
+ }}
+ existingPRDs={existingPRDs}
+ onRefreshPRDs={(showNotification = false) => {
+ // Reload existing PRDs
+ if (currentProject?.name) {
+ api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`)
+ .then(response => response.ok ? response.json() : Promise.reject())
+ .then(data => {
+ setExistingPRDs(data.prdFiles || []);
+ if (showNotification) {
+ setPRDNotification('PRD saved successfully!');
+ setTimeout(() => setPRDNotification(null), 3000);
+ }
+ })
+ .catch(error => console.error('Failed to refresh PRDs:', error));
+ }
+ }}
+ />
+
+
+ )}
{/*
)}
+
+ {/* Task Detail Modal */}
+ {shouldShowTasksTab && showTaskDetail && selectedTask && (
+
+ )}
+ {/* PRD Editor Modal */}
+ {showPRDEditor && (
+
{
+ setShowPRDEditor(false);
+ setSelectedPRD(null);
+ }}
+ isNewFile={!selectedPRD?.isExisting}
+ file={{
+ name: selectedPRD?.name || 'prd.txt',
+ content: selectedPRD?.content || ''
+ }}
+ onSave={async () => {
+ setShowPRDEditor(false);
+ setSelectedPRD(null);
+
+ // Reload existing PRDs with notification
+ try {
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
+ if (response.ok) {
+ const data = await response.json();
+ setExistingPRDs(data.prdFiles || []);
+ setPRDNotification('PRD saved successfully!');
+ setTimeout(() => setPRDNotification(null), 3000);
+ }
+ } catch (error) {
+ console.error('Failed to refresh PRDs:', error);
+ }
+
+ refreshTasks?.();
+ }}
+ />
+ )}
+ {/* PRD Notification */}
+ {prdNotification && (
+
+
+
+
+
+
{prdNotification}
+
+
+ )}
);
}
diff --git a/src/components/MobileNav.jsx b/src/components/MobileNav.jsx
index 185a9f4..b985142 100644
--- a/src/components/MobileNav.jsx
+++ b/src/components/MobileNav.jsx
@@ -1,7 +1,9 @@
import React from 'react';
-import { MessageSquare, Folder, Terminal, GitBranch, Globe } from 'lucide-react';
+import { MessageSquare, Folder, Terminal, GitBranch, Globe, CheckSquare } from 'lucide-react';
+import { useTasksSettings } from '../contexts/TasksSettingsContext';
function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
+ const { tasksEnabled } = useTasksSettings();
// Detect dark mode
const isDarkMode = document.documentElement.classList.contains('dark');
const navItems = [
@@ -24,7 +26,13 @@ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
id: 'git',
icon: GitBranch,
onClick: () => setActiveTab('git')
- }
+ },
+ // Conditionally add tasks tab if enabled
+ ...(tasksEnabled ? [{
+ id: 'tasks',
+ icon: CheckSquare,
+ onClick: () => setActiveTab('tasks')
+ }] : [])
];
return (
diff --git a/src/components/NextTaskBanner.jsx b/src/components/NextTaskBanner.jsx
new file mode 100644
index 0000000..2a55cb8
--- /dev/null
+++ b/src/components/NextTaskBanner.jsx
@@ -0,0 +1,695 @@
+import React, { useState } from 'react';
+import { ArrowRight, List, Clock, Flag, CheckCircle, Circle, AlertCircle, Pause, ChevronDown, ChevronUp, Plus, FileText, Settings, X, Terminal, Eye, Play, Zap, Target } from 'lucide-react';
+import { cn } from '../lib/utils';
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+import { api } from '../utils/api';
+import Shell from './Shell';
+import TaskDetail from './TaskDetail';
+
+const NextTaskBanner = ({ onShowAllTasks, onStartTask, className = '' }) => {
+ const { nextTask, tasks, currentProject, isLoadingTasks, projectTaskMaster, refreshTasks, refreshProjects } = useTaskMaster();
+ const [showDetails, setShowDetails] = useState(false);
+ const [showTaskOptions, setShowTaskOptions] = useState(false);
+ const [showCreateTaskModal, setShowCreateTaskModal] = useState(false);
+ const [showTemplateSelector, setShowTemplateSelector] = useState(false);
+ const [showCLI, setShowCLI] = useState(false);
+ const [showTaskDetail, setShowTaskDetail] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Handler functions
+ const handleInitializeTaskMaster = async () => {
+ if (!currentProject) return;
+
+ setIsLoading(true);
+ try {
+ const response = await api.taskmaster.init(currentProject.name);
+ if (response.ok) {
+ await refreshProjects();
+ setShowTaskOptions(false);
+ } else {
+ const error = await response.json();
+ console.error('Failed to initialize TaskMaster:', error);
+ alert(`Failed to initialize TaskMaster: ${error.message}`);
+ }
+ } catch (error) {
+ console.error('Error initializing TaskMaster:', error);
+ alert('Error initializing TaskMaster. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleCreateManualTask = () => {
+ setShowCreateTaskModal(true);
+ setShowTaskOptions(false);
+ };
+
+ const handleParsePRD = () => {
+ setShowTemplateSelector(true);
+ setShowTaskOptions(false);
+ };
+
+ // Don't show if no project or still loading
+ if (!currentProject || isLoadingTasks) {
+ return null;
+ }
+
+ let bannerContent;
+
+ // Show setup message only if no tasks exist AND TaskMaster is not configured
+ if ((!tasks || tasks.length === 0) && !projectTaskMaster?.hasTaskmaster) {
+ bannerContent = (
+
+
+
+
+
+
+ TaskMaster AI is not configured
+
+
+
+
+
+
+ setShowTaskOptions(!showTaskOptions)}
+ className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors flex items-center gap-1"
+ >
+
+ Initialize TaskMaster AI
+
+
+
+
+ {showTaskOptions && (
+
+ {!projectTaskMaster?.hasTaskmaster && (
+
+
+ 🎯 What is TaskMaster?
+
+
+
• AI-Powered Task Management: Break complex projects into manageable subtasks
+
• PRD Templates: Generate tasks from Product Requirements Documents
+
• Dependency Tracking: Understand task relationships and execution order
+
• Progress Visualization: Kanban boards and detailed task analytics
+
• CLI Integration: Use taskmaster commands for advanced workflows
+
+
+ )}
+
+ {!projectTaskMaster?.hasTaskmaster ? (
+
setShowCLI(true)}
+ >
+
+ Initialize TaskMaster
+
+ ) : (
+ <>
+
+ Add more tasks: Create additional tasks manually or generate them from a PRD template
+
+
+
+ Create a new task manually
+
+
+
+ {isLoading ? 'Parsing...' : 'Generate tasks from PRD template'}
+
+ >
+ )}
+
+
+ )}
+
+ );
+ } else if (nextTask) {
+ // Show next task if available
+ bannerContent = (
+
+
+
+
+
+
+
+
Task {nextTask.id}
+ {nextTask.priority === 'high' && (
+
+
+
+ )}
+ {nextTask.priority === 'medium' && (
+
+
+
+ )}
+ {nextTask.priority === 'low' && (
+
+
+
+ )}
+
+
+ {nextTask.title}
+
+
+
+
+
onStartTask?.()}
+ className="text-xs px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium transition-colors shadow-sm flex items-center gap-1"
+ >
+
+ Start Task
+
+
setShowTaskDetail(true)}
+ className="text-xs px-2 py-1.5 border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-md transition-colors flex items-center gap-1"
+ title="View task details"
+ >
+
+
+ {onShowAllTasks && (
+
+
+
+ )}
+
+
+
+
+ );
+ } else if (tasks && tasks.length > 0) {
+ // Show completion message only if there are tasks and all are done
+ const completedTasks = tasks.filter(task => task.status === 'done').length;
+ const totalTasks = tasks.length;
+
+ bannerContent = (
+
+
+
+
+
+ {completedTasks === totalTasks ? "All done! 🎉" : "No pending tasks"}
+
+
+
+
+ {completedTasks}/{totalTasks}
+
+
+ Review
+
+
+
+
+ );
+ } else {
+ // TaskMaster is configured but no tasks exist - don't show anything in chat
+ bannerContent = null;
+ }
+
+ return (
+ <>
+ {bannerContent}
+
+ {/* Create Task Modal */}
+ {showCreateTaskModal && (
+
setShowCreateTaskModal(false)}
+ onTaskCreated={() => {
+ refreshTasks();
+ setShowCreateTaskModal(false);
+ }}
+ />
+ )}
+
+ {/* Template Selector Modal */}
+ {showTemplateSelector && (
+ setShowTemplateSelector(false)}
+ onTemplateApplied={() => {
+ refreshTasks();
+ setShowTemplateSelector(false);
+ }}
+ />
+ )}
+
+ {/* TaskMaster CLI Setup Modal */}
+ {showCLI && (
+
+
+ {/* Modal Header */}
+
+
+
+
+
+
+
TaskMaster Setup
+
Interactive CLI for {currentProject?.displayName}
+
+
+
setShowCLI(false)}
+ className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
+ >
+
+
+
+
+ {/* Terminal Container */}
+
+
+ {/* Modal Footer */}
+
+
+
+ TaskMaster initialization will start automatically
+
+
setShowCLI(false)}
+ className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
+ >
+ Close
+
+
+
+
+
+ )}
+
+ {/* Task Detail Modal */}
+ {showTaskDetail && nextTask && (
+ setShowTaskDetail(false)}
+ onStatusChange={() => refreshTasks?.()}
+ onTaskClick={null} // Disable dependency navigation in NextTaskBanner for now
+ />
+ )}
+ >
+ );
+};
+
+// Simple Create Task Modal Component
+const CreateTaskModal = ({ currentProject, onClose, onTaskCreated }) => {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ priority: 'medium',
+ useAI: false,
+ prompt: ''
+ });
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!currentProject) return;
+
+ setIsSubmitting(true);
+ try {
+ const taskData = formData.useAI
+ ? { prompt: formData.prompt, priority: formData.priority }
+ : { title: formData.title, description: formData.description, priority: formData.priority };
+
+ const response = await api.taskmaster.addTask(currentProject.name, taskData);
+
+ if (response.ok) {
+ onTaskCreated();
+ } else {
+ const error = await response.json();
+ console.error('Failed to create task:', error);
+ alert(`Failed to create task: ${error.message}`);
+ }
+ } catch (error) {
+ console.error('Error creating task:', error);
+ alert('Error creating task. Please try again.');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
Create New Task
+
+
+
+
+
+
+
+
+ );
+};
+
+// Template Selector Modal Component
+const TemplateSelector = ({ currentProject, onClose, onTemplateApplied }) => {
+ const [templates, setTemplates] = useState([]);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [customizations, setCustomizations] = useState({});
+ const [fileName, setFileName] = useState('prd.txt');
+ const [isLoading, setIsLoading] = useState(true);
+ const [isApplying, setIsApplying] = useState(false);
+ const [step, setStep] = useState('select'); // 'select', 'customize', 'generate'
+
+ useEffect(() => {
+ const loadTemplates = async () => {
+ try {
+ const response = await api.taskmaster.getTemplates();
+ if (response.ok) {
+ const data = await response.json();
+ setTemplates(data.templates);
+ }
+ } catch (error) {
+ console.error('Error loading templates:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadTemplates();
+ }, []);
+
+ const handleSelectTemplate = (template) => {
+ setSelectedTemplate(template);
+ // Find placeholders in template content
+ const placeholders = template.content.match(/\[([^\]]+)\]/g) || [];
+ const uniquePlaceholders = [...new Set(placeholders.map(p => p.slice(1, -1)))];
+
+ const initialCustomizations = {};
+ uniquePlaceholders.forEach(placeholder => {
+ initialCustomizations[placeholder] = '';
+ });
+
+ setCustomizations(initialCustomizations);
+ setStep('customize');
+ };
+
+ const handleApplyTemplate = async () => {
+ if (!selectedTemplate || !currentProject) return;
+
+ setIsApplying(true);
+ try {
+ // Apply template
+ const applyResponse = await api.taskmaster.applyTemplate(currentProject.name, {
+ templateId: selectedTemplate.id,
+ fileName,
+ customizations
+ });
+
+ if (!applyResponse.ok) {
+ const error = await applyResponse.json();
+ throw new Error(error.message || 'Failed to apply template');
+ }
+
+ // Parse PRD to generate tasks
+ const parseResponse = await api.taskmaster.parsePRD(currentProject.name, {
+ fileName,
+ numTasks: 10
+ });
+
+ if (!parseResponse.ok) {
+ const error = await parseResponse.json();
+ throw new Error(error.message || 'Failed to generate tasks');
+ }
+
+ setStep('generate');
+ setTimeout(() => {
+ onTemplateApplied();
+ }, 2000);
+
+ } catch (error) {
+ console.error('Error applying template:', error);
+ alert(`Error: ${error.message}`);
+ setIsApplying(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
Loading templates...
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {step === 'select' ? 'Select PRD Template' :
+ step === 'customize' ? 'Customize Template' :
+ 'Generating Tasks'}
+
+
+
+
+
+
+ {step === 'select' && (
+
+ {templates.map((template) => (
+
handleSelectTemplate(template)}
+ >
+
+
+
{template.name}
+
{template.description}
+
+ {template.category}
+
+
+
+
+
+ ))}
+
+ )}
+
+ {step === 'customize' && selectedTemplate && (
+
+
+
+ File Name
+
+ setFileName(e.target.value)}
+ className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
+ placeholder="prd.txt"
+ />
+
+
+ {Object.keys(customizations).length > 0 && (
+
+
+ Customize Template
+
+
+ {Object.entries(customizations).map(([key, value]) => (
+
+
+ {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
+
+ setCustomizations(prev => ({ ...prev, [key]: e.target.value }))}
+ className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
+ placeholder={`Enter ${key.toLowerCase()}`}
+ />
+
+ ))}
+
+
+ )}
+
+
+ setStep('select')}
+ className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Back
+
+
+ {isApplying ? 'Applying...' : 'Apply & Generate Tasks'}
+
+
+
+ )}
+
+ {step === 'generate' && (
+
+
+
+
+
+ Template Applied Successfully!
+
+
+ Your PRD has been created and tasks are being generated...
+
+
+ )}
+
+
+ );
+};
+
+export default NextTaskBanner;
\ No newline at end of file
diff --git a/src/components/PRDEditor.jsx b/src/components/PRDEditor.jsx
new file mode 100644
index 0000000..3f1ff35
--- /dev/null
+++ b/src/components/PRDEditor.jsx
@@ -0,0 +1,871 @@
+import React, { useState, useEffect, useRef } from 'react';
+import CodeMirror from '@uiw/react-codemirror';
+import { markdown } from '@codemirror/lang-markdown';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { EditorView } from '@codemirror/view';
+import { X, Save, Download, Maximize2, Minimize2, Eye, FileText, Sparkles, AlertTriangle } from 'lucide-react';
+import { cn } from '../lib/utils';
+import { api, authenticatedFetch } from '../utils/api';
+
+const PRDEditor = ({
+ file,
+ onClose,
+ projectPath,
+ project, // Add project object
+ initialContent = '',
+ isNewFile = false,
+ onSave
+}) => {
+ const [content, setContent] = useState(initialContent);
+ const [loading, setLoading] = useState(!isNewFile);
+ const [saving, setSaving] = useState(false);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [isDarkMode, setIsDarkMode] = useState(true);
+ const [saveSuccess, setSaveSuccess] = useState(false);
+ const [previewMode, setPreviewMode] = useState(false);
+ const [wordWrap, setWordWrap] = useState(true); // Default to true for markdown
+ const [fileName, setFileName] = useState('');
+ const [showGenerateModal, setShowGenerateModal] = useState(false);
+ const [showOverwriteConfirm, setShowOverwriteConfirm] = useState(false);
+ const [existingPRDs, setExistingPRDs] = useState([]);
+
+ const editorRef = useRef(null);
+
+ const PRD_TEMPLATE = `# Product Requirements Document - Example Project
+
+## 1. Overview
+**Product Name:** AI-Powered Task Manager
+**Version:** 1.0
+**Date:** 2024-12-27
+**Author:** Development Team
+
+This document outlines the requirements for building an AI-powered task management application that integrates with development workflows and provides intelligent task breakdown and prioritization.
+
+## 2. Objectives
+- Create an intuitive task management system that works seamlessly with developer tools
+- Provide AI-powered task generation from high-level requirements
+- Enable real-time collaboration and progress tracking
+- Integrate with popular development environments (VS Code, Cursor, etc.)
+
+### Success Metrics
+- User adoption rate > 80% within development teams
+- Task completion rate improvement of 25%
+- Time-to-delivery reduction of 15%
+
+## 3. User Stories
+
+### Core Functionality
+- As a project manager, I want to create PRDs that automatically generate detailed tasks so I can save time on project planning
+- As a developer, I want to see my next task clearly highlighted so I can maintain focus
+- As a team lead, I want to track progress across multiple projects so I can provide accurate status updates
+- As a developer, I want tasks to be broken down into implementable subtasks so I can work more efficiently
+
+### AI Integration
+- As a user, I want to describe a feature in natural language and get detailed implementation tasks so I can start working immediately
+- As a project manager, I want the AI to analyze task complexity and suggest appropriate time estimates
+- As a developer, I want intelligent task prioritization based on dependencies and deadlines
+
+### Collaboration
+- As a team member, I want to see real-time updates when tasks are completed so I can coordinate my work
+- As a stakeholder, I want to view project progress through intuitive dashboards
+- As a developer, I want to add implementation notes to tasks for future reference
+
+## 4. Functional Requirements
+
+### Task Management
+- Create, edit, and delete tasks with rich metadata (priority, status, dependencies, estimates)
+- Hierarchical task structure with subtasks and sub-subtasks
+- Real-time status updates and progress tracking
+- Dependency management with circular dependency detection
+- Bulk operations (move, update status, assign)
+
+### AI Features
+- Natural language PRD parsing to generate structured tasks
+- Intelligent task breakdown with complexity analysis
+- Automated subtask generation with implementation details
+- Smart dependency suggestion
+- Progress prediction based on historical data
+
+### Integration Features
+- VS Code/Cursor extension for in-editor task management
+- Git integration for linking commits to tasks
+- API for third-party tool integration
+- Webhook support for external notifications
+- CLI tool for command-line task management
+
+### User Interface
+- Responsive web application (desktop and mobile)
+- Multiple view modes (Kanban, list, calendar)
+- Dark/light theme support
+- Drag-and-drop task organization
+- Advanced filtering and search capabilities
+- Keyboard shortcuts for power users
+
+## 5. Technical Requirements
+
+### Frontend
+- React.js with TypeScript for type safety
+- Modern UI framework (Tailwind CSS)
+- State management (Context API or Redux)
+- Real-time updates via WebSockets
+- Progressive Web App (PWA) support
+- Accessibility compliance (WCAG 2.1 AA)
+
+### Backend
+- Node.js with Express.js framework
+- RESTful API design with OpenAPI documentation
+- Real-time communication via Socket.io
+- Background job processing
+- Rate limiting and security middleware
+
+### AI Integration
+- Integration with multiple AI providers (OpenAI, Anthropic, etc.)
+- Fallback model support
+- Context-aware prompt engineering
+- Token usage optimization
+- Model response caching
+
+### Database
+- Primary: PostgreSQL for relational data
+- Cache: Redis for session management and real-time features
+- Full-text search capabilities
+- Database migrations and seeding
+- Backup and recovery procedures
+
+### Infrastructure
+- Docker containerization
+- Cloud deployment (AWS/GCP/Azure)
+- Auto-scaling capabilities
+- Monitoring and logging (structured logging)
+- CI/CD pipeline with automated testing
+
+## 6. Non-Functional Requirements
+
+### Performance
+- Page load time < 2 seconds
+- API response time < 500ms for 95% of requests
+- Support for 1000+ concurrent users
+- Efficient handling of large task lists (10,000+ tasks)
+
+### Security
+- JWT-based authentication with refresh tokens
+- Role-based access control (RBAC)
+- Data encryption at rest and in transit
+- Regular security audits and penetration testing
+- GDPR and privacy compliance
+
+### Reliability
+- 99.9% uptime SLA
+- Graceful error handling and recovery
+- Data backup every 6 hours with point-in-time recovery
+- Disaster recovery plan with RTO < 4 hours
+
+### Scalability
+- Horizontal scaling for both frontend and backend
+- Database read replicas for query optimization
+- CDN for static asset delivery
+- Microservices architecture for future expansion
+
+## 7. User Experience Design
+
+### Information Architecture
+- Intuitive navigation with breadcrumbs
+- Context-aware menus and actions
+- Progressive disclosure of complex features
+- Consistent design patterns throughout
+
+### Interaction Design
+- Smooth animations and transitions
+- Immediate feedback for user actions
+- Undo/redo functionality for critical operations
+- Smart defaults and auto-save features
+
+### Visual Design
+- Modern, clean interface with plenty of whitespace
+- Consistent color scheme and typography
+- Clear visual hierarchy with proper contrast ratios
+- Iconography that supports comprehension
+
+## 8. Integration Requirements
+
+### Development Tools
+- VS Code extension with task panel and quick actions
+- Cursor IDE integration with AI task suggestions
+- Terminal CLI for command-line workflow
+- Browser extension for web-based tools
+
+### Third-Party Services
+- GitHub/GitLab integration for issue sync
+- Slack/Discord notifications
+- Calendar integration (Google Calendar, Outlook)
+- Time tracking tools (Toggl, Harvest)
+
+### APIs and Webhooks
+- RESTful API with comprehensive documentation
+- GraphQL endpoint for complex queries
+- Webhook system for external integrations
+- SDK development for major programming languages
+
+## 9. Implementation Phases
+
+### Phase 1: Core MVP (8-10 weeks)
+- Basic task management (CRUD operations)
+- Simple AI task generation
+- Web interface with essential features
+- User authentication and basic permissions
+
+### Phase 2: Enhanced Features (6-8 weeks)
+- Advanced AI features (complexity analysis, subtask generation)
+- Real-time collaboration
+- Mobile-responsive design
+- Integration with one development tool (VS Code)
+
+### Phase 3: Enterprise Features (4-6 weeks)
+- Advanced user management and permissions
+- API and webhook system
+- Performance optimization
+- Comprehensive testing and security audit
+
+### Phase 4: Ecosystem Expansion (4-6 weeks)
+- Additional tool integrations
+- Mobile app development
+- Advanced analytics and reporting
+- Third-party marketplace preparation
+
+## 10. Risk Assessment
+
+### Technical Risks
+- AI model reliability and cost management
+- Real-time synchronization complexity
+- Database performance with large datasets
+- Integration complexity with multiple tools
+
+### Business Risks
+- User adoption in competitive market
+- AI provider dependency
+- Data privacy and security concerns
+- Feature scope creep and timeline delays
+
+### Mitigation Strategies
+- Implement robust error handling and fallback systems
+- Develop comprehensive testing strategy
+- Create detailed documentation and user guides
+- Establish clear project scope and change management process
+
+## 11. Success Criteria
+
+### Development Milestones
+- Alpha version with core features completed
+- Beta version with selected user group feedback
+- Production-ready version with full feature set
+- Post-launch iterations based on user feedback
+
+### Business Metrics
+- User engagement and retention rates
+- Task completion and productivity metrics
+- Customer satisfaction scores (NPS > 50)
+- Revenue targets and subscription growth
+
+## 12. Appendices
+
+### Glossary
+- **PRD**: Product Requirements Document
+- **AI**: Artificial Intelligence
+- **CRUD**: Create, Read, Update, Delete
+- **API**: Application Programming Interface
+- **CI/CD**: Continuous Integration/Continuous Deployment
+
+### References
+- Industry best practices for task management
+- AI integration patterns and examples
+- Security and compliance requirements
+- Performance benchmarking data
+
+---
+
+**Document Control:**
+- Version: 1.0
+- Last Updated: December 27, 2024
+- Next Review: January 15, 2025
+- Approved By: Product Owner, Technical Lead`;
+
+ // Initialize filename and load content
+ useEffect(() => {
+ const initializeEditor = async () => {
+ // Set initial filename
+ if (file?.name) {
+ setFileName(file.name.replace(/\.(txt|md)$/, '')); // Remove extension for editing
+ } else if (isNewFile) {
+ // Generate default filename based on current date
+ const now = new Date();
+ const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
+ setFileName(`prd-${dateStr}`);
+ }
+
+ // Load content
+ if (isNewFile) {
+ setContent(PRD_TEMPLATE);
+ setLoading(false);
+ return;
+ }
+
+ // If content is directly provided (for existing PRDs loaded from API)
+ if (file.content) {
+ setContent(file.content);
+ setLoading(false);
+ return;
+ }
+
+ // Fallback to loading from file path (legacy support)
+ try {
+ setLoading(true);
+
+ const response = await api.readFile(file.projectName, file.path);
+
+ if (!response.ok) {
+ throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ setContent(data.content || PRD_TEMPLATE);
+ } catch (error) {
+ console.error('Error loading PRD file:', error);
+ setContent(`# Error Loading PRD\n\nError: ${error.message}\n\nFile: ${file?.name || 'New PRD'}\nPath: ${file?.path || 'Not saved yet'}\n\n${PRD_TEMPLATE}`);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ initializeEditor();
+ }, [file, projectPath, isNewFile]);
+
+ // Fetch existing PRDs to check for conflicts
+ useEffect(() => {
+ const fetchExistingPRDs = async () => {
+ if (!project?.name) {
+ console.log('No project name available:', project);
+ return;
+ }
+
+ try {
+ console.log('Fetching PRDs for project:', project.name);
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
+ if (response.ok) {
+ const data = await response.json();
+ console.log('Fetched existing PRDs:', data.prds);
+ setExistingPRDs(data.prds || []);
+ } else {
+ console.log('Failed to fetch PRDs:', response.status, response.statusText);
+ }
+ } catch (error) {
+ console.error('Error fetching existing PRDs:', error);
+ }
+ };
+
+ fetchExistingPRDs();
+ }, [project?.name]);
+
+ const handleSave = async () => {
+ if (!content.trim()) {
+ alert('Please add content before saving.');
+ return;
+ }
+
+ if (!fileName.trim()) {
+ alert('Please provide a filename for the PRD.');
+ return;
+ }
+
+ // Check if file already exists
+ const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
+ const existingFile = existingPRDs.find(prd => prd.name === fullFileName);
+
+ console.log('Save check:', {
+ fullFileName,
+ existingPRDs,
+ existingFile,
+ isExisting: file?.isExisting,
+ fileObject: file,
+ shouldShowModal: existingFile && !file?.isExisting
+ });
+
+ if (existingFile && !file?.isExisting) {
+ console.log('Showing overwrite confirmation modal');
+ // Show confirmation modal for overwrite
+ setShowOverwriteConfirm(true);
+ return;
+ }
+
+ await performSave();
+ };
+
+ const performSave = async () => {
+ setSaving(true);
+ try {
+ // Ensure filename has .txt extension
+ const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
+
+ const response = await authenticatedFetch(`/api/taskmaster/prd/${encodeURIComponent(project?.name)}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ fileName: fullFileName,
+ content
+ })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || `Save failed: ${response.status}`);
+ }
+
+ // Show success feedback
+ setSaveSuccess(true);
+ setTimeout(() => setSaveSuccess(false), 2000);
+
+ // Update existing PRDs list
+ const response2 = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
+ if (response2.ok) {
+ const data = await response2.json();
+ setExistingPRDs(data.prds || []);
+ }
+
+ // Call the onSave callback if provided (for UI updates)
+ if (onSave) {
+ await onSave();
+ }
+
+ } catch (error) {
+ console.error('Error saving PRD:', error);
+ alert(`Error saving PRD: ${error.message}`);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleDownload = () => {
+ const blob = new Blob([content], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ const downloadFileName = fileName ? `${fileName}.txt` : 'prd.txt';
+ a.download = downloadFileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ const handleGenerateTasks = async () => {
+ if (!content.trim()) {
+ alert('Please add content to the PRD before generating tasks.');
+ return;
+ }
+
+ // Show AI-first modal instead of simple confirm
+ setShowGenerateModal(true);
+ };
+
+
+ const toggleFullscreen = () => {
+ setIsFullscreen(!isFullscreen);
+ };
+
+ // Handle keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (e.ctrlKey || e.metaKey) {
+ if (e.key === 's') {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ onClose();
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [content]);
+
+ // Simple markdown to HTML converter for preview
+ const renderMarkdown = (markdown) => {
+ return markdown
+ .replace(/^### (.*$)/gim, '$1 ')
+ .replace(/^## (.*$)/gim, '$1 ')
+ .replace(/^# (.*$)/gim, '$1 ')
+ .replace(/\*\*(.*)\*\*/gim, '$1 ')
+ .replace(/\*(.*)\*/gim, '$1 ')
+ .replace(/^\- (.*$)/gim, '$1 ')
+ .replace(/(.*<\/li>)/gims, '')
+ .replace(/\n\n/gim, '')
+ .replace(/^(?!<[h|u|l])(.*$)/gim, '
$1
')
+ .replace(/<\/ul>\s*/gim, '');
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+ {/* Mobile: Stack filename and tags vertically for more space */}
+
+ {/* Filename input row - full width on mobile */}
+
+
+ {
+ // Remove invalid filename characters
+ const sanitizedValue = e.target.value.replace(/[<>:"/\\|?*]/g, '');
+ setFileName(sanitizedValue);
+ }}
+ className="font-medium text-gray-900 dark:text-white bg-transparent border-none outline-none min-w-0 flex-1 text-base sm:text-sm placeholder-gray-400 dark:placeholder-gray-500"
+ placeholder="Enter PRD filename"
+ maxLength={100}
+ />
+ .txt
+
+
document.querySelector('input[placeholder="Enter PRD filename"]')?.focus()}
+ className="p-1 text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
+ title="Click to edit filename"
+ >
+
+
+
+
+
+
+ {/* Tags row - moves to second line on mobile for more filename space */}
+
+
+ 📋 PRD
+
+ {isNewFile && (
+
+ ✨ New
+
+ )}
+
+
+
+ {/* Description - smaller on mobile */}
+
+ Product Requirements Document
+
+
+
+
+
+
setPreviewMode(!previewMode)}
+ className={cn(
+ 'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
+ 'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
+ previewMode
+ ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
+ )}
+ title={previewMode ? 'Switch to edit mode' : 'Preview markdown'}
+ >
+
+
+
+
setWordWrap(!wordWrap)}
+ className={cn(
+ 'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
+ 'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
+ wordWrap
+ ? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
+ )}
+ title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
+ >
+ ↵
+
+
+
setIsDarkMode(!isDarkMode)}
+ className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
+ title="Toggle theme"
+ >
+ {isDarkMode ? '☀️' : '🌙'}
+
+
+
+
+
+
+
+
+ Generate Tasks
+
+
+
+ {saveSuccess ? (
+ <>
+
+
+
+ Saved!
+ >
+ ) : (
+ <>
+
+ {saving ? 'Saving...' : 'Save PRD'}
+ >
+ )}
+
+
+
+ {isFullscreen ? : }
+
+
+
+
+
+
+
+
+ {/* Editor/Preview Content */}
+
+ {previewMode ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Lines: {content.split('\n').length}
+ Characters: {content.length}
+ Words: {content.split(/\s+/).filter(word => word.length > 0).length}
+ Format: Markdown
+
+
+
+ Press Ctrl+S to save • Esc to close
+
+
+
+
+ {/* Generate Tasks Modal */}
+ {showGenerateModal && (
+
+
+ {/* Header */}
+
+
+
+
+
+
Generate Tasks from PRD
+
+
setShowGenerateModal(false)}
+ className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
+ >
+
+
+
+
+ {/* Content */}
+
+ {/* AI-First Approach */}
+
+
+
+
+
+
+
+ 💡 Pro Tip: Ask Claude Code Directly!
+
+
+ You can simply ask Claude Code in the chat to parse your PRD and generate tasks.
+ The AI assistant will automatically save your PRD and create detailed tasks with implementation details.
+
+
+
+
💬 Example:
+
+ "I've just initialized a new project with Claude Task Master. I have a PRD at .taskmaster/docs/{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}. Can you help me parse it and set up the initial tasks?"
+
+
+
+
+ This will: Save your PRD, analyze its content, and generate structured tasks with subtasks, dependencies, and implementation details.
+
+
+
+
+
+ {/* Learn More Link */}
+
+
+ {/* Footer */}
+
+ setShowGenerateModal(false)}
+ className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
+ >
+ Got it, I'll ask Claude Code directly
+
+
+
+
+
+ )}
+
+ {/* Overwrite Confirmation Modal */}
+ {showOverwriteConfirm && (
+
+
setShowOverwriteConfirm(false)} />
+
+
+
+
+
+ File Already Exists
+
+
+
+
+ A PRD file named "{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}" already exists.
+ Do you want to overwrite it with the current content?
+
+
+
+ setShowOverwriteConfirm(false)}
+ className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
+ >
+ Cancel
+
+ {
+ setShowOverwriteConfirm(false);
+ await performSave();
+ }}
+ className="px-4 py-2 text-sm text-white bg-yellow-600 hover:bg-yellow-700 rounded-md flex items-center space-x-2 transition-colors"
+ >
+
+ Overwrite
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default PRDEditor;
\ No newline at end of file
diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx
index 03d2bfd..7c8fe80 100644
--- a/src/components/Shell.jsx
+++ b/src/components/Shell.jsx
@@ -29,7 +29,7 @@ if (typeof document !== 'undefined') {
// Global store for shell sessions to persist across tab switches
const shellSessions = new Map();
-function Shell({ selectedProject, selectedSession, isActive }) {
+function Shell({ selectedProject, selectedSession, isActive, initialCommand, isPlainShell = false, onProcessComplete }) {
const terminalRef = useRef(null);
const terminal = useRef(null);
const fitAddon = useRef(null);
@@ -434,13 +434,17 @@ function Shell({ selectedProject, selectedSession, isActive }) {
const initPayload = {
type: 'init',
projectPath: selectedProject.fullPath || selectedProject.path,
- sessionId: selectedSession?.id,
- hasSession: !!selectedSession,
- provider: selectedSession?.__provider || 'claude',
+ sessionId: isPlainShell ? null : selectedSession?.id,
+ hasSession: isPlainShell ? false : !!selectedSession,
+ provider: isPlainShell ? 'plain-shell' : (selectedSession?.__provider || 'claude'),
cols: terminal.current.cols,
- rows: terminal.current.rows
+ rows: terminal.current.rows,
+ initialCommand: initialCommand,
+ isPlainShell: isPlainShell
};
+ console.log('Shell init payload:', initPayload);
+
ws.current.send(JSON.stringify(initPayload));
// Also send resize message immediately after init
@@ -473,6 +477,18 @@ function Shell({ selectedProject, selectedSession, isActive }) {
urls.push(match[1]);
}
+ if (isPlainShell && onProcessComplete) {
+ const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI codes
+ if (cleanOutput.includes('Process exited with code 0')) {
+ onProcessComplete(0); // Success
+ } else if (cleanOutput.match(/Process exited with code (\d+)/)) {
+ const exitCode = parseInt(cleanOutput.match(/Process exited with code (\d+)/)[1]);
+ if (exitCode !== 0) {
+ onProcessComplete(exitCode); // Error
+ }
+ }
+ }
+
// If URLs found, log them for potential opening
terminal.current.write(output);
@@ -606,14 +622,16 @@ function Shell({ selectedProject, selectedSession, isActive }) {
Continue in Shell
- {selectedSession ?
- (() => {
- const displaySessionName = selectedSession.__provider === 'cursor'
- ? (selectedSession.name || 'Untitled Session')
- : (selectedSession.summary || 'New Session');
- return `Resume session: ${displaySessionName.slice(0, 50)}...`;
- })() :
- 'Start a new Claude session'
+ {isPlainShell ?
+ `Run ${initialCommand || 'command'} in ${selectedProject.displayName}` :
+ selectedSession ?
+ (() => {
+ const displaySessionName = selectedSession.__provider === 'cursor'
+ ? (selectedSession.name || 'Untitled Session')
+ : (selectedSession.summary || 'New Session');
+ return `Resume session: ${displaySessionName.slice(0, 50)}...`;
+ })() :
+ 'Start a new Claude session'
}
@@ -629,7 +647,10 @@ function Shell({ selectedProject, selectedSession, isActive }) {
Connecting to shell...
- Starting Claude CLI in {selectedProject.displayName}
+ {isPlainShell ?
+ `Running ${initialCommand || 'command'} in ${selectedProject.displayName}` :
+ `Starting Claude CLI in ${selectedProject.displayName}`
+ }
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index 36f3bbf..4d1d4e2 100644
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -8,7 +8,10 @@ import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRig
import { cn } from '../lib/utils';
import ClaudeLogo from './ClaudeLogo';
import CursorLogo from './CursorLogo.jsx';
+import TaskIndicator from './TaskIndicator';
import { api } from '../utils/api';
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+import { useTasksSettings } from '../contexts/TasksSettingsContext';
// Move formatTimeAgo outside component to avoid recreation on every render
const formatTimeAgo = (dateString, currentTime) => {
@@ -70,6 +73,10 @@ function Sidebar({
const [generatingSummary, setGeneratingSummary] = useState({});
const [searchFilter, setSearchFilter] = useState('');
+ // TaskMaster context
+ const { setCurrentProject, mcpServerStatus } = useTaskMaster();
+ const { tasksEnabled } = useTasksSettings();
+
// Starred projects state - persisted in localStorage
const [starredProjects, setStarredProjects] = useState(() => {
@@ -417,6 +424,15 @@ function Sidebar({
return displayName.includes(searchLower) || projectName.includes(searchLower);
});
+ // Enhanced project selection that updates both the main UI and TaskMaster context
+ const handleProjectSelect = (project) => {
+ // Call the original project select handler
+ onProjectSelect(project);
+
+ // Update TaskMaster context with the selected project
+ setCurrentProject(project);
+ };
+
return (
{/* Header */}
@@ -500,6 +516,7 @@ function Sidebar({
+
{/* New Project Form */}
{showNewProject && (
@@ -717,9 +734,25 @@ function Sidebar({
/>
) : (
<>
-
- {project.displayName}
-
+
+
+ {project.displayName}
+
+ {tasksEnabled && (
+ {
+ const projectConfigured = project.taskmaster?.hasTaskmaster;
+ const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
+ if (projectConfigured && mcpConfigured) return 'fully-configured';
+ if (projectConfigured) return 'taskmaster-only';
+ if (mcpConfigured) return 'mcp-only';
+ return 'not-configured';
+ })()}
+ size="xs"
+ className="flex-shrink-0 ml-2"
+ />
+ )}
+
{(() => {
const sessionCount = getAllSessions(project).length;
@@ -825,13 +858,13 @@ function Sidebar({
onClick={() => {
// Desktop behavior: select project and toggle
if (selectedProject?.name !== project.name) {
- onProjectSelect(project);
+ handleProjectSelect(project);
}
toggleProject(project.name);
}}
onTouchEnd={handleTouchClick(() => {
if (selectedProject?.name !== project.name) {
- onProjectSelect(project);
+ handleProjectSelect(project);
}
toggleProject(project.name);
})}
@@ -1013,11 +1046,11 @@ function Sidebar({
isActive ? "border-green-500/30 bg-green-50/5 dark:bg-green-900/5" : "border-border/30"
)}
onClick={() => {
- onProjectSelect(project);
+ handleProjectSelect(project);
onSessionSelect(session);
}}
onTouchEnd={handleTouchClick(() => {
- onProjectSelect(project);
+ handleProjectSelect(project);
onSessionSelect(session);
})}
>
@@ -1239,7 +1272,7 @@ function Sidebar({
{
- onProjectSelect(project);
+ handleProjectSelect(project);
onNewSession(project);
}}
>
diff --git a/src/components/TaskCard.jsx b/src/components/TaskCard.jsx
new file mode 100644
index 0000000..3b363a4
--- /dev/null
+++ b/src/components/TaskCard.jsx
@@ -0,0 +1,210 @@
+import React from 'react';
+import { Clock, CheckCircle, Circle, AlertCircle, Pause, X, ArrowRight, ChevronUp, Minus, Flag } from 'lucide-react';
+import { cn } from '../lib/utils';
+import Tooltip from './Tooltip';
+
+const TaskCard = ({
+ task,
+ onClick,
+ showParent = false,
+ className = ''
+}) => {
+ const getStatusConfig = (status) => {
+ switch (status) {
+ case 'done':
+ return {
+ icon: CheckCircle,
+ bgColor: 'bg-green-50 dark:bg-green-950',
+ borderColor: 'border-green-200 dark:border-green-800',
+ iconColor: 'text-green-600 dark:text-green-400',
+ textColor: 'text-green-900 dark:text-green-100',
+ statusText: 'Done'
+ };
+
+ case 'in-progress':
+ return {
+ icon: Clock,
+ bgColor: 'bg-blue-50 dark:bg-blue-950',
+ borderColor: 'border-blue-200 dark:border-blue-800',
+ iconColor: 'text-blue-600 dark:text-blue-400',
+ textColor: 'text-blue-900 dark:text-blue-100',
+ statusText: 'In Progress'
+ };
+
+ case 'review':
+ return {
+ icon: AlertCircle,
+ bgColor: 'bg-amber-50 dark:bg-amber-950',
+ borderColor: 'border-amber-200 dark:border-amber-800',
+ iconColor: 'text-amber-600 dark:text-amber-400',
+ textColor: 'text-amber-900 dark:text-amber-100',
+ statusText: 'Review'
+ };
+
+ case 'deferred':
+ return {
+ icon: Pause,
+ bgColor: 'bg-gray-50 dark:bg-gray-800',
+ borderColor: 'border-gray-200 dark:border-gray-700',
+ iconColor: 'text-gray-500 dark:text-gray-400',
+ textColor: 'text-gray-700 dark:text-gray-300',
+ statusText: 'Deferred'
+ };
+
+ case 'cancelled':
+ return {
+ icon: X,
+ bgColor: 'bg-red-50 dark:bg-red-950',
+ borderColor: 'border-red-200 dark:border-red-800',
+ iconColor: 'text-red-600 dark:text-red-400',
+ textColor: 'text-red-900 dark:text-red-100',
+ statusText: 'Cancelled'
+ };
+
+ case 'pending':
+ default:
+ return {
+ icon: Circle,
+ bgColor: 'bg-slate-50 dark:bg-slate-800',
+ borderColor: 'border-slate-200 dark:border-slate-700',
+ iconColor: 'text-slate-500 dark:text-slate-400',
+ textColor: 'text-slate-900 dark:text-slate-100',
+ statusText: 'Pending'
+ };
+ }
+ };
+
+ const config = getStatusConfig(task.status);
+ const Icon = config.icon;
+
+ const getPriorityIcon = (priority) => {
+ switch (priority) {
+ case 'high':
+ return (
+
+
+
+
+
+ );
+ case 'medium':
+ return (
+
+
+
+
+
+ );
+ case 'low':
+ return (
+
+
+
+
+
+ );
+ default:
+ return (
+
+
+
+
+
+ );
+ }
+ };
+
+ return (
+
+ {/* Header with Task ID, Title, and Priority */}
+
+ {/* Task ID and Title */}
+
+
+
+
+ {task.id}
+
+
+
+
+ {task.title}
+
+ {showParent && task.parentId && (
+
+ Task {task.parentId}
+
+ )}
+
+
+ {/* Priority Icon */}
+
+ {getPriorityIcon(task.priority)}
+
+
+
+ {/* Footer with Dependencies and Status */}
+
+ {/* Dependencies */}
+
+ {task.dependencies && Array.isArray(task.dependencies) && task.dependencies.length > 0 && (
+
`Task ${dep}`).join(', ')}`}>
+
+
+
Depends on: {task.dependencies.join(', ')}
+
+
+ )}
+
+
+ {/* Status Badge */}
+
+
+
+
+ {config.statusText}
+
+
+
+
+
+ {/* Subtask Progress (if applicable) */}
+ {task.subtasks && task.subtasks.length > 0 && (
+
+
+
Progress:
+
st.status === 'done').length} of ${task.subtasks.length} subtasks completed`}>
+
+
st.status === 'done').length / task.subtasks.length) * 100)}%`
+ }}
+ />
+
+
+
st.status === 'done').length} completed, ${task.subtasks.filter(st => st.status === 'pending').length} pending, ${task.subtasks.filter(st => st.status === 'in-progress').length} in progress`}>
+
+ {task.subtasks.filter(st => st.status === 'done').length}/{task.subtasks.length}
+
+
+
+
+ )}
+
+ );
+};
+
+export default TaskCard;
\ No newline at end of file
diff --git a/src/components/TaskDetail.jsx b/src/components/TaskDetail.jsx
new file mode 100644
index 0000000..8316983
--- /dev/null
+++ b/src/components/TaskDetail.jsx
@@ -0,0 +1,406 @@
+import React, { useState } from 'react';
+import { X, Flag, User, ArrowRight, CheckCircle, Circle, AlertCircle, Pause, Edit, Save, Copy, ChevronDown, ChevronRight, Clock } from 'lucide-react';
+import { cn } from '../lib/utils';
+import TaskIndicator from './TaskIndicator';
+import { api } from '../utils/api';
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+
+const TaskDetail = ({
+ task,
+ onClose,
+ onEdit,
+ onStatusChange,
+ onTaskClick,
+ isOpen = true,
+ className = ''
+}) => {
+ const [editMode, setEditMode] = useState(false);
+ const [editedTask, setEditedTask] = useState(task || {});
+ const [isSaving, setIsSaving] = useState(false);
+ const [showDetails, setShowDetails] = useState(false);
+ const [showTestStrategy, setShowTestStrategy] = useState(false);
+ const { currentProject, refreshTasks } = useTaskMaster();
+
+ if (!isOpen || !task) return null;
+
+ const handleSave = async () => {
+ if (!currentProject) return;
+
+ setIsSaving(true);
+ try {
+ // Only include changed fields
+ const updates = {};
+ if (editedTask.title !== task.title) updates.title = editedTask.title;
+ if (editedTask.description !== task.description) updates.description = editedTask.description;
+ if (editedTask.details !== task.details) updates.details = editedTask.details;
+
+ if (Object.keys(updates).length > 0) {
+ const response = await api.taskmaster.updateTask(currentProject.name, task.id, updates);
+
+ if (response.ok) {
+ // Refresh tasks to get updated data
+ refreshTasks?.();
+ onEdit?.(editedTask);
+ setEditMode(false);
+ } else {
+ const error = await response.json();
+ console.error('Failed to update task:', error);
+ alert(`Failed to update task: ${error.message}`);
+ }
+ } else {
+ setEditMode(false);
+ }
+ } catch (error) {
+ console.error('Error updating task:', error);
+ alert('Error updating task. Please try again.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleStatusChange = async (newStatus) => {
+ if (!currentProject) return;
+
+ try {
+ const response = await api.taskmaster.updateTask(currentProject.name, task.id, { status: newStatus });
+
+ if (response.ok) {
+ refreshTasks?.();
+ onStatusChange?.(task.id, newStatus);
+ } else {
+ const error = await response.json();
+ console.error('Failed to update task status:', error);
+ alert(`Failed to update task status: ${error.message}`);
+ }
+ } catch (error) {
+ console.error('Error updating task status:', error);
+ alert('Error updating task status. Please try again.');
+ }
+ };
+
+ const copyTaskId = () => {
+ navigator.clipboard.writeText(task.id.toString());
+ };
+
+ const getStatusConfig = (status) => {
+ switch (status) {
+ case 'done':
+ return { icon: CheckCircle, color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-950' };
+ case 'in-progress':
+ return { icon: Clock, color: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-950' };
+ case 'review':
+ return { icon: AlertCircle, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-950' };
+ case 'deferred':
+ return { icon: Pause, color: 'text-gray-500 dark:text-gray-400', bg: 'bg-gray-50 dark:bg-gray-800' };
+ case 'cancelled':
+ return { icon: X, color: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-950' };
+ default:
+ return { icon: Circle, color: 'text-slate-500 dark:text-slate-400', bg: 'bg-slate-50 dark:bg-slate-800' };
+ }
+ };
+
+ const statusConfig = getStatusConfig(task.status);
+ const StatusIcon = statusConfig.icon;
+
+
+ const getPriorityColor = (priority) => {
+ switch (priority) {
+ case 'high': return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950';
+ case 'medium': return 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-950';
+ case 'low': return 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950';
+ default: return 'text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800';
+ }
+ };
+
+ const statusOptions = [
+ { value: 'pending', label: 'Pending' },
+ { value: 'in-progress', label: 'In Progress' },
+ { value: 'review', label: 'Review' },
+ { value: 'done', label: 'Done' },
+ { value: 'deferred', label: 'Deferred' },
+ { value: 'cancelled', label: 'Cancelled' }
+ ];
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+ Task {task.id}
+
+
+ {task.parentId && (
+
+ Subtask of Task {task.parentId}
+
+ )}
+
+ {editMode ? (
+
setEditedTask({ ...editedTask, title: e.target.value })}
+ className="w-full text-lg font-semibold bg-transparent border-b-2 border-blue-500 focus:outline-none text-gray-900 dark:text-white"
+ placeholder="Task title"
+ />
+ ) : (
+
+ {task.title}
+
+ )}
+
+
+
+
+ {editMode ? (
+ <>
+
+
+
+ {
+ setEditMode(false);
+ setEditedTask(task);
+ }}
+ disabled={isSaving}
+ className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Cancel editing"
+ >
+
+
+ >
+ ) : (
+ setEditMode(true)}
+ className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
+ title="Edit task"
+ >
+
+
+ )}
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {/* Status and Metadata Row */}
+
+ {/* Status */}
+
+
Status
+
+
+
+
+ {statusOptions.find(option => option.value === task.status)?.label || task.status}
+
+
+
+
+
+ {/* Priority */}
+
+
Priority
+
+
+ {task.priority || 'Not set'}
+
+
+
+ {/* Dependencies */}
+
+
Dependencies
+ {task.dependencies && task.dependencies.length > 0 ? (
+
+ {task.dependencies.map(depId => (
+
onTaskClick && onTaskClick({ id: depId })}
+ className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded text-sm hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer disabled:cursor-default disabled:opacity-50"
+ disabled={!onTaskClick}
+ title={onTaskClick ? `Click to view Task ${depId}` : `Task ${depId}`}
+ >
+
+ {depId}
+
+ ))}
+
+ ) : (
+
No dependencies
+ )}
+
+
+
+ {/* Description */}
+
+
Description
+ {editMode ? (
+
setEditedTask({ ...editedTask, description: e.target.value })}
+ rows={3}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
+ placeholder="Task description"
+ />
+ ) : (
+
+ {task.description || 'No description provided'}
+
+ )}
+
+
+ {/* Implementation Details */}
+ {task.details && (
+
+
setShowDetails(!showDetails)}
+ className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
+ >
+
+ Implementation Details
+
+ {showDetails ? (
+
+ ) : (
+
+ )}
+
+ {showDetails && (
+
+ {editMode ? (
+
setEditedTask({ ...editedTask, details: e.target.value })}
+ rows={4}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
+ placeholder="Implementation details"
+ />
+ ) : (
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Test Strategy */}
+ {task.testStrategy && (
+
+
setShowTestStrategy(!showTestStrategy)}
+ className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
+ >
+
+ Test Strategy
+
+ {showTestStrategy ? (
+
+ ) : (
+
+ )}
+
+ {showTestStrategy && (
+
+
+
+ {task.testStrategy}
+
+
+
+ )}
+
+ )}
+
+ {/* Subtasks */}
+ {task.subtasks && task.subtasks.length > 0 && (
+
+
+ Subtasks ({task.subtasks.length})
+
+
+ {task.subtasks.map(subtask => {
+ const subtaskConfig = getStatusConfig(subtask.status);
+ const SubtaskIcon = subtaskConfig.icon;
+ return (
+
+
+
+
+ {subtask.title}
+
+ {subtask.description && (
+
+ {subtask.description}
+
+ )}
+
+
+ {subtask.id}
+
+
+ );
+ })}
+
+
+ )}
+
+
+
+ {/* Footer */}
+
+
+ Task ID: {task.id}
+
+
+
+
+ Close
+
+
+
+
+
+ );
+};
+
+export default TaskDetail;
\ No newline at end of file
diff --git a/src/components/TaskIndicator.jsx b/src/components/TaskIndicator.jsx
new file mode 100644
index 0000000..9b12b48
--- /dev/null
+++ b/src/components/TaskIndicator.jsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import { CheckCircle, Settings, X, AlertCircle } from 'lucide-react';
+import { cn } from '../lib/utils';
+
+/**
+ * TaskIndicator Component
+ *
+ * Displays TaskMaster status for projects in the sidebar with appropriate
+ * icons and colors based on the project's TaskMaster configuration state.
+ */
+const TaskIndicator = ({
+ status = 'not-configured',
+ size = 'sm',
+ className = '',
+ showLabel = false
+}) => {
+ const getIndicatorConfig = () => {
+ switch (status) {
+ case 'fully-configured':
+ return {
+ icon: CheckCircle,
+ color: 'text-green-500 dark:text-green-400',
+ bgColor: 'bg-green-50 dark:bg-green-950',
+ label: 'TaskMaster Ready',
+ title: 'TaskMaster fully configured with MCP server'
+ };
+
+ case 'taskmaster-only':
+ return {
+ icon: Settings,
+ color: 'text-blue-500 dark:text-blue-400',
+ bgColor: 'bg-blue-50 dark:bg-blue-950',
+ label: 'TaskMaster Init',
+ title: 'TaskMaster initialized, MCP server needs setup'
+ };
+
+ case 'mcp-only':
+ return {
+ icon: AlertCircle,
+ color: 'text-amber-500 dark:text-amber-400',
+ bgColor: 'bg-amber-50 dark:bg-amber-950',
+ label: 'MCP Ready',
+ title: 'MCP server configured, TaskMaster needs initialization'
+ };
+
+ case 'not-configured':
+ case 'error':
+ default:
+ return {
+ icon: X,
+ color: 'text-gray-400 dark:text-gray-500',
+ bgColor: 'bg-gray-50 dark:bg-gray-900',
+ label: 'No TaskMaster',
+ title: 'TaskMaster not configured'
+ };
+ }
+ };
+
+ const config = getIndicatorConfig();
+ const Icon = config.icon;
+
+ const sizeClasses = {
+ xs: 'w-3 h-3',
+ sm: 'w-4 h-4',
+ md: 'w-5 h-5',
+ lg: 'w-6 h-6'
+ };
+
+ const paddingClasses = {
+ xs: 'p-0.5',
+ sm: 'p-1',
+ md: 'p-1.5',
+ lg: 'p-2'
+ };
+
+ if (showLabel) {
+ return (
+
+
+ {config.label}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default TaskIndicator;
\ No newline at end of file
diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx
new file mode 100644
index 0000000..8e81de0
--- /dev/null
+++ b/src/components/TaskList.jsx
@@ -0,0 +1,1054 @@
+import React, { useState, useMemo, useEffect } from 'react';
+import { Search, Filter, ArrowUpDown, ArrowUp, ArrowDown, List, Grid, ChevronDown, Columns, Plus, Settings, Terminal, FileText, HelpCircle, X } from 'lucide-react';
+import { cn } from '../lib/utils';
+import TaskCard from './TaskCard';
+import CreateTaskModal from './CreateTaskModal';
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+import Shell from './Shell';
+import { api } from '../utils/api';
+
+const TaskList = ({
+ tasks = [],
+ onTaskClick,
+ className = '',
+ showParentTasks = false,
+ defaultView = 'kanban', // 'list', 'grid', or 'kanban'
+ currentProject,
+ onTaskCreated,
+ onShowPRDEditor,
+ existingPRDs = [],
+ onRefreshPRDs
+}) => {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [priorityFilter, setPriorityFilter] = useState('all');
+ const [sortBy, setSortBy] = useState('id'); // 'id', 'title', 'status', 'priority', 'updated'
+ const [sortOrder, setSortOrder] = useState('asc'); // 'asc' or 'desc'
+ const [viewMode, setViewMode] = useState(defaultView);
+ const [showFilters, setShowFilters] = useState(false);
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showCLI, setShowCLI] = useState(false);
+ const [showHelpGuide, setShowHelpGuide] = useState(false);
+ const [isTaskMasterComplete, setIsTaskMasterComplete] = useState(false);
+ const [showPRDDropdown, setShowPRDDropdown] = useState(false);
+
+ const { projectTaskMaster, refreshProjects, refreshTasks, setCurrentProject } = useTaskMaster();
+
+ // Close PRD dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (showPRDDropdown && !event.target.closest('.relative')) {
+ setShowPRDDropdown(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [showPRDDropdown]);
+
+ // Get unique status values from tasks
+ const statuses = useMemo(() => {
+ const statusSet = new Set(tasks.map(task => task.status).filter(Boolean));
+ return Array.from(statusSet).sort();
+ }, [tasks]);
+
+ // Get unique priority values from tasks
+ const priorities = useMemo(() => {
+ const prioritySet = new Set(tasks.map(task => task.priority).filter(Boolean));
+ return Array.from(prioritySet).sort();
+ }, [tasks]);
+
+ // Filter and sort tasks
+ const filteredAndSortedTasks = useMemo(() => {
+ let filtered = tasks.filter(task => {
+ // Text search
+ const searchLower = searchTerm.toLowerCase();
+ const matchesSearch = !searchTerm ||
+ task.title.toLowerCase().includes(searchLower) ||
+ task.description?.toLowerCase().includes(searchLower) ||
+ task.id.toString().includes(searchLower);
+
+ // Status filter
+ const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
+
+ // Priority filter
+ const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
+
+ return matchesSearch && matchesStatus && matchesPriority;
+ });
+
+ // Sort tasks
+ filtered.sort((a, b) => {
+ let aVal, bVal;
+
+ switch (sortBy) {
+ case 'title':
+ aVal = a.title.toLowerCase();
+ bVal = b.title.toLowerCase();
+ break;
+ case 'status':
+ // Custom status ordering: pending, in-progress, done, blocked, deferred, cancelled
+ const statusOrder = { pending: 1, 'in-progress': 2, done: 3, blocked: 4, deferred: 5, cancelled: 6 };
+ aVal = statusOrder[a.status] || 99;
+ bVal = statusOrder[b.status] || 99;
+ break;
+ case 'priority':
+ // Custom priority ordering: high should be sorted first in descending
+ const priorityOrder = { high: 3, medium: 2, low: 1 };
+ aVal = priorityOrder[a.priority] || 0;
+ bVal = priorityOrder[b.priority] || 0;
+ break;
+ case 'updated':
+ aVal = new Date(a.updatedAt || a.createdAt || 0);
+ bVal = new Date(b.updatedAt || b.createdAt || 0);
+ break;
+ case 'id':
+ default:
+ // Handle numeric and dotted IDs (1, 1.1, 1.2, 2, 2.1, etc.)
+ const parseId = (id) => {
+ const parts = id.toString().split('.');
+ return parts.map(part => parseInt(part, 10));
+ };
+
+ const aIds = parseId(a.id);
+ const bIds = parseId(b.id);
+
+ // Compare each part
+ for (let i = 0; i < Math.max(aIds.length, bIds.length); i++) {
+ const aId = aIds[i] || 0;
+ const bId = bIds[i] || 0;
+ if (aId !== bId) {
+ aVal = aId;
+ bVal = bId;
+ break;
+ }
+ }
+ break;
+ }
+
+ if (sortBy === 'updated') {
+ return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
+ }
+
+ if (typeof aVal === 'string') {
+ return sortOrder === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
+ }
+
+ return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
+ });
+
+ return filtered;
+ }, [tasks, searchTerm, statusFilter, priorityFilter, sortBy, sortOrder]);
+
+ // Organize tasks by status for Kanban view
+ const kanbanColumns = useMemo(() => {
+ const allColumns = [
+ {
+ id: 'pending',
+ title: '📋 To Do',
+ status: 'pending',
+ color: 'bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700',
+ headerColor: 'bg-slate-100 dark:bg-slate-800 text-slate-800 dark:text-slate-200'
+ },
+ {
+ id: 'in-progress',
+ title: '🚀 In Progress',
+ status: 'in-progress',
+ color: 'bg-blue-50 dark:bg-blue-900/50 border-blue-200 dark:border-blue-700',
+ headerColor: 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200'
+ },
+ {
+ id: 'done',
+ title: '✅ Done',
+ status: 'done',
+ color: 'bg-emerald-50 dark:bg-emerald-900/50 border-emerald-200 dark:border-emerald-700',
+ headerColor: 'bg-emerald-100 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200'
+ },
+ {
+ id: 'blocked',
+ title: '🚫 Blocked',
+ status: 'blocked',
+ color: 'bg-red-50 dark:bg-red-900/50 border-red-200 dark:border-red-700',
+ headerColor: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200'
+ },
+ {
+ id: 'deferred',
+ title: '⏳ Deferred',
+ status: 'deferred',
+ color: 'bg-amber-50 dark:bg-amber-900/50 border-amber-200 dark:border-amber-700',
+ headerColor: 'bg-amber-100 dark:bg-amber-800 text-amber-800 dark:text-amber-200'
+ },
+ {
+ id: 'cancelled',
+ title: '❌ Cancelled',
+ status: 'cancelled',
+ color: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700',
+ headerColor: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200'
+ }
+ ];
+
+ // Only show columns that have tasks or are part of the main workflow
+ const mainWorkflowStatuses = ['pending', 'in-progress', 'done'];
+ const columnsWithTasks = allColumns.filter(column => {
+ const hasTask = filteredAndSortedTasks.some(task => task.status === column.status);
+ const isMainWorkflow = mainWorkflowStatuses.includes(column.status);
+ return hasTask || isMainWorkflow;
+ });
+
+ return columnsWithTasks.map(column => ({
+ ...column,
+ tasks: filteredAndSortedTasks.filter(task => task.status === column.status)
+ }));
+ }, [filteredAndSortedTasks]);
+
+ const handleSortChange = (newSortBy) => {
+ if (sortBy === newSortBy) {
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
+ } else {
+ setSortBy(newSortBy);
+ setSortOrder('asc');
+ }
+ };
+
+ const clearFilters = () => {
+ setSearchTerm('');
+ setStatusFilter('all');
+ setPriorityFilter('all');
+ };
+
+ const getSortIcon = (field) => {
+ if (sortBy !== field) return
;
+ return sortOrder === 'asc' ?
:
;
+ };
+
+ if (tasks.length === 0) {
+ // Check if TaskMaster is configured by looking for .taskmaster directory
+ const hasTaskMasterDirectory = currentProject?.taskMasterConfigured ||
+ currentProject?.taskmaster?.hasTaskmaster ||
+ projectTaskMaster?.hasTaskmaster;
+
+ return (
+
+ {!hasTaskMasterDirectory ? (
+ // TaskMaster not configured
+
+
+
+
+
+ TaskMaster AI is not configured
+
+
+ TaskMaster helps break down complex projects into manageable tasks with AI-powered assistance
+
+
+ {/* What is TaskMaster section */}
+
+
+ 🎯 What is TaskMaster?
+
+
+
• AI-Powered Task Management: Break complex projects into manageable subtasks
+
• PRD Templates: Generate tasks from Product Requirements Documents
+
• Dependency Tracking: Understand task relationships and execution order
+
• Progress Visualization: Kanban boards and detailed task analytics
+
• CLI Integration: Use taskmaster commands for advanced workflows
+
+
+
+
{
+ setIsTaskMasterComplete(false); // Reset completion state
+ setShowCLI(true);
+ }}
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors flex items-center gap-2 mx-auto"
+ >
+
+ Initialize TaskMaster AI
+
+
+ ) : (
+ // TaskMaster configured but no tasks - show Getting Started guide
+
+
+
+
+
+
+
+
Getting Started with TaskMaster
+
TaskMaster is initialized! Here's what to do next:
+
+
+
+
+
+ {/* Step 1 */}
+
+
1
+
+
Create a Product Requirements Document (PRD)
+
Discuss your project idea and create a PRD that describes what you want to build.
+
{
+ onShowPRDEditor?.();
+ }}
+ className="inline-flex items-center gap-1 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-2 py-1 rounded hover:bg-purple-200 dark:hover:bg-purple-900/50 transition-colors"
+ >
+
+ Add PRD
+
+
+ {/* Show existing PRDs if any */}
+ {existingPRDs.length > 0 && (
+
+
Existing PRDs:
+
+ {existingPRDs.map((prd) => (
+ {
+ try {
+ // Load the PRD content from the API
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}/${encodeURIComponent(prd.name)}`);
+ if (response.ok) {
+ const prdData = await response.json();
+ onShowPRDEditor?.({
+ name: prd.name,
+ content: prdData.content,
+ isExisting: true
+ });
+ } else {
+ console.error('Failed to load PRD:', response.statusText);
+ }
+ } catch (error) {
+ console.error('Error loading PRD:', error);
+ }
+ }}
+ className="inline-flex items-center gap-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-2 py-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
+ >
+
+ {prd.name}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Step 2 */}
+
+
2
+
+
Generate Tasks from PRD
+
Once you have a PRD, ask your AI assistant to parse it and TaskMaster will automatically break it down into manageable tasks with implementation details.
+
+
+
+ {/* Step 3 */}
+
+
3
+
+
Analyze & Expand Tasks
+
Ask your AI assistant to analyze task complexity and expand them into detailed subtasks for easier implementation.
+
+
+
+ {/* Step 4 */}
+
+
4
+
+
Start Building
+
Ask your AI assistant to begin working on tasks, update their status, and add new tasks as your project evolves.
+
+
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ onShowPRDEditor?.();
+ }}
+ className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium transition-colors cursor-pointer"
+ style={{ zIndex: 10 }}
+ >
+
+ Add PRD
+
+
+
+
+
+
+
+ 💡 Tip: Start with a PRD to get the most out of TaskMaster's AI-powered task generation
+
+
+
+ )}
+
+ {/* TaskMaster CLI Setup Modal */}
+ {showCLI && (
+
+
+ {/* Modal Header */}
+
+
+
+
+
+
+
TaskMaster Setup
+
Interactive CLI for {currentProject?.displayName}
+
+
+
{
+ setShowCLI(false);
+ // Refresh project data after closing CLI to detect TaskMaster initialization
+ setTimeout(() => {
+ refreshProjects();
+ // Also refresh the current project's TaskMaster status
+ if (currentProject) {
+ setCurrentProject(currentProject);
+ }
+ }, 1000);
+ }}
+ className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
+ >
+
+
+
+
+ {/* Terminal Container */}
+
+
{
+ // Focus the terminal when clicked
+ const terminalElement = e.currentTarget.querySelector('.xterm-screen');
+ if (terminalElement) {
+ terminalElement.focus();
+ }
+ }}
+ >
+ {
+ setIsTaskMasterComplete(true);
+ if (exitCode === 0) {
+ // Auto-refresh after successful completion
+ setTimeout(() => {
+ refreshProjects();
+ if (currentProject) {
+ setCurrentProject(currentProject);
+ }
+ }, 1000);
+ }
+ }}
+ />
+
+
+
+ {/* Modal Footer */}
+
+
+
+ {isTaskMasterComplete ? (
+
+
+ TaskMaster setup completed! You can now close this window.
+
+ ) : (
+ "TaskMaster initialization will start automatically"
+ )}
+
+
{
+ setShowCLI(false);
+ setIsTaskMasterComplete(false); // Reset state
+ // Refresh project data after closing CLI to detect TaskMaster initialization
+ setTimeout(() => {
+ refreshProjects();
+ // Also refresh the current project's TaskMaster status
+ if (currentProject) {
+ setCurrentProject(currentProject);
+ }
+ }, 1000);
+ }}
+ className={cn(
+ "px-4 py-2 text-sm font-medium rounded-md transition-colors",
+ isTaskMasterComplete
+ ? "bg-green-600 hover:bg-green-700 text-white"
+ : "text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600"
+ )}
+ >
+ {isTaskMasterComplete ? "Close & Continue" : "Close"}
+
+
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {/* Header Controls */}
+
+ {/* Search Bar */}
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 pr-4 py-2 w-full border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+ {/* Controls */}
+
+ {/* View Toggle */}
+
+ setViewMode('kanban')}
+ className={cn(
+ 'p-2 rounded-md transition-colors',
+ viewMode === 'kanban'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
+ )}
+ title="Kanban view"
+ >
+
+
+ setViewMode('list')}
+ className={cn(
+ 'p-2 rounded-md transition-colors',
+ viewMode === 'list'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
+ )}
+ title="List view"
+ >
+
+
+ setViewMode('grid')}
+ className={cn(
+ 'p-2 rounded-md transition-colors',
+ viewMode === 'grid'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
+ )}
+ title="Grid view"
+ >
+
+
+
+
+ {/* Filters Toggle */}
+
setShowFilters(!showFilters)}
+ className={cn(
+ 'flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors',
+ showFilters
+ ? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700 text-blue-700 dark:text-blue-300'
+ : 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
+ )}
+ >
+
+ Filters
+
+
+
+ {/* Action Buttons */}
+ {currentProject && (
+ <>
+ {/* Help Button */}
+
setShowHelpGuide(true)}
+ className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors border border-gray-300 dark:border-gray-600"
+ title="TaskMaster Getting Started Guide"
+ >
+
+
+
+ {/* PRD Management */}
+
+ {existingPRDs.length > 0 ? (
+ // Dropdown when PRDs exist
+
+
setShowPRDDropdown(!showPRDDropdown)}
+ className="flex items-center gap-2 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
+ title={`${existingPRDs.length} PRD${existingPRDs.length > 1 ? 's' : ''} available`}
+ >
+
+ PRDs
+
+ {existingPRDs.length}
+
+
+
+
+ {showPRDDropdown && (
+
+
+
{
+ onShowPRDEditor?.();
+ setShowPRDDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 text-sm font-medium text-purple-700 dark:text-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded flex items-center gap-2"
+ >
+
+ Create New PRD
+
+
+
Existing PRDs:
+ {existingPRDs.map((prd) => (
+
{
+ try {
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}/${encodeURIComponent(prd.name)}`);
+ if (response.ok) {
+ const prdData = await response.json();
+ onShowPRDEditor?.({
+ name: prd.name,
+ content: prdData.content,
+ isExisting: true
+ });
+ setShowPRDDropdown(false);
+ }
+ } catch (error) {
+ console.error('Error loading PRD:', error);
+ }
+ }}
+ className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2"
+ title={`Modified: ${new Date(prd.modified).toLocaleDateString()}`}
+ >
+
+ {prd.name}
+
+ ))}
+
+
+ )}
+
+ ) : (
+ // Simple button when no PRDs exist
+
{
+ onShowPRDEditor?.();
+ }}
+ className="flex items-center gap-2 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
+ title="Create Product Requirements Document"
+ >
+
+ Add PRD
+
+ )}
+
+
+ {/* Add Task Button */}
+ {((currentProject?.taskMasterConfigured || currentProject?.taskmaster?.hasTaskmaster || projectTaskMaster?.hasTaskmaster) || tasks.length > 0) && (
+
setShowCreateModal(true)}
+ className="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
+ title="Add a new task"
+ >
+
+ Add Task
+
+ )}
+ >
+ )}
+
+
+
+ {/* Expanded Filters */}
+ {showFilters && (
+
+
+ {/* Status Filter */}
+
+
+ Status
+
+ setStatusFilter(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
+ >
+ All Statuses
+ {statuses.map(status => (
+
+ {status.charAt(0).toUpperCase() + status.slice(1).replace('-', ' ')}
+
+ ))}
+
+
+
+ {/* Priority Filter */}
+
+
+ Priority
+
+ setPriorityFilter(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
+ >
+ All Priorities
+ {priorities.map(priority => (
+
+ {priority.charAt(0).toUpperCase() + priority.slice(1)}
+
+ ))}
+
+
+
+ {/* Sort By */}
+
+
+ Sort By
+
+ {
+ const [field, order] = e.target.value.split('-');
+ setSortBy(field);
+ setSortOrder(order);
+ }}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
+ >
+ ID (Ascending)
+ ID (Descending)
+ Title (A-Z)
+ Title (Z-A)
+ Status (Pending First)
+ Status (Done First)
+ Priority (High First)
+ Priority (Low First)
+
+
+
+
+ {/* Filter Actions */}
+
+
+ Showing {filteredAndSortedTasks.length} of {tasks.length} tasks
+
+
+ Clear Filters
+
+
+
+ )}
+
+ {/* Quick Sort Buttons */}
+
+ handleSortChange('id')}
+ className={cn(
+ 'flex items-center gap-1 px-3 py-1.5 rounded-md text-sm transition-colors',
+ sortBy === 'id'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
+ )}
+ >
+ ID {getSortIcon('id')}
+
+ handleSortChange('status')}
+ className={cn(
+ 'flex items-center gap-1 px-3 py-1.5 rounded-md text-sm transition-colors',
+ sortBy === 'status'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
+ )}
+ >
+ Status {getSortIcon('status')}
+
+ handleSortChange('priority')}
+ className={cn(
+ 'flex items-center gap-1 px-3 py-1.5 rounded-md text-sm transition-colors',
+ sortBy === 'priority'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
+ )}
+ >
+ Priority {getSortIcon('priority')}
+
+
+
+ {/* Task Cards */}
+ {filteredAndSortedTasks.length === 0 ? (
+
+
+
+
No tasks match your filters
+
Try adjusting your search or filter criteria.
+
+
+ ) : viewMode === 'kanban' ? (
+ /* Kanban Board Layout - Dynamic grid based on column count */
+
= 6 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6"
+ )}>
+ {kanbanColumns.map((column) => (
+
+ {/* Column Header */}
+
+
+
+ {column.title}
+
+
+
+ {column.tasks.length}
+
+
+
+
+
+ {/* Column Tasks */}
+
+ {column.tasks.length === 0 ? (
+
+
+
+ No tasks yet
+
+
+ {column.status === 'pending' ? 'Tasks will appear here' :
+ column.status === 'in-progress' ? 'Move tasks here when started' :
+ column.status === 'done' ? 'Completed tasks appear here' :
+ 'Tasks with this status will appear here'}
+
+
+ ) : (
+ column.tasks.map((task) => (
+
+ onTaskClick?.(task)}
+ showParent={showParentTasks}
+ className="w-full shadow-sm hover:shadow-md transition-shadow cursor-pointer"
+ />
+
+ ))
+ )}
+
+
+ ))}
+
+ ) : (
+
+ {filteredAndSortedTasks.map((task) => (
+ onTaskClick?.(task)}
+ showParent={showParentTasks}
+ className={viewMode === 'grid' ? 'h-full' : ''}
+ />
+ ))}
+
+ )}
+
+ {/* Create Task Modal */}
+ {showCreateModal && (
+
setShowCreateModal(false)}
+ onTaskCreated={() => {
+ setShowCreateModal(false);
+ if (onTaskCreated) onTaskCreated();
+ }}
+ />
+ )}
+
+ {/* Help Guide Modal */}
+ {showHelpGuide && (
+
+
+ {/* Modal Header */}
+
+
+
+
+
+
+
Getting Started with TaskMaster
+
Your guide to productive task management
+
+
+
setShowHelpGuide(false)}
+ className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
+ >
+
+
+
+
+ {/* Modal Content */}
+
+
+ {/* Step 1 */}
+
+
1
+
+
Create a Product Requirements Document (PRD)
+
Discuss your project idea and create a PRD that describes what you want to build.
+
{
+ onShowPRDEditor?.();
+ setShowHelpGuide(false);
+ }}
+ className="inline-flex items-center gap-2 text-sm bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-3 py-1.5 rounded-lg hover:bg-purple-200 dark:hover:bg-purple-900/50 transition-colors"
+ >
+
+ Add PRD
+
+
+
+
+ {/* Step 2 */}
+
+
2
+
+
Generate Tasks from PRD
+
Once you have a PRD, ask your AI assistant to parse it and TaskMaster will automatically break it down into manageable tasks with implementation details.
+
+
💬 Example:
+
+ "I've just initialized a new project with Claude Task Master. I have a PRD at .taskmaster/docs/prd.txt. Can you help me parse it and set up the initial tasks?"
+
+
+
+
+
+ {/* Step 3 */}
+
+
3
+
+
Analyze & Expand Tasks
+
Ask your AI assistant to analyze task complexity and expand them into detailed subtasks for easier implementation.
+
+
💬 Example:
+
+ "Task 5 seems complex. Can you break it down into subtasks?"
+
+
+
+
+
+ {/* Step 4 */}
+
+
4
+
+
Start Building
+
Ask your AI assistant to begin working on tasks, update their status, and add new tasks as your project evolves.
+
+
💬 Example:
+
+ "Please add a new task to implement user profile image uploads using Cloudinary, research the best approach."
+
+
+
+ View more examples and usage patterns →
+
+
+
+
+ {/* Pro Tips */}
+
+
💡 Pro Tips
+
+
+
+ Use the search bar to quickly find specific tasks
+
+
+
+ Switch between Kanban, List, and Grid views using the view toggles
+
+
+
+ Use filters to focus on specific task statuses or priorities
+
+
+
+ Click on any task to view detailed information and manage subtasks
+
+
+
+
+ {/* Learn More Section */}
+
+
📚 Learn More
+
+ TaskMaster AI is an advanced task management system built for developers. Get documentation, examples, and contribute to the project.
+
+
+
+
+
+ View on GitHub
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default TaskList;
\ No newline at end of file
diff --git a/src/components/TaskMasterSetupWizard.jsx b/src/components/TaskMasterSetupWizard.jsx
new file mode 100644
index 0000000..813889b
--- /dev/null
+++ b/src/components/TaskMasterSetupWizard.jsx
@@ -0,0 +1,603 @@
+import React, { useState, useEffect } from 'react';
+import { X, ChevronRight, ChevronLeft, CheckCircle, AlertCircle, Settings, Server, FileText, Sparkles, ExternalLink, Copy } from 'lucide-react';
+import { cn } from '../lib/utils';
+import { api } from '../utils/api';
+
+const TaskMasterSetupWizard = ({
+ isOpen = true,
+ onClose,
+ onComplete,
+ currentProject,
+ className = ''
+}) => {
+ const [currentStep, setCurrentStep] = useState(1);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [setupData, setSetupData] = useState({
+ projectRoot: '',
+ initGit: true,
+ storeTasksInGit: true,
+ addAliases: true,
+ skipInstall: false,
+ rules: ['claude'],
+ mcpConfigured: false,
+ prdContent: ''
+ });
+
+ const totalSteps = 4;
+
+ useEffect(() => {
+ if (currentProject) {
+ setSetupData(prev => ({
+ ...prev,
+ projectRoot: currentProject.path || ''
+ }));
+ }
+ }, [currentProject]);
+
+ const steps = [
+ {
+ id: 1,
+ title: 'Project Configuration',
+ description: 'Configure basic TaskMaster settings for your project'
+ },
+ {
+ id: 2,
+ title: 'MCP Server Setup',
+ description: 'Ensure TaskMaster MCP server is properly configured'
+ },
+ {
+ id: 3,
+ title: 'PRD Creation',
+ description: 'Create or import a Product Requirements Document'
+ },
+ {
+ id: 4,
+ title: 'Complete Setup',
+ description: 'Initialize TaskMaster and generate initial tasks'
+ }
+ ];
+
+ const handleNext = async () => {
+ setError(null);
+
+ try {
+ if (currentStep === 1) {
+ // Validate project configuration
+ if (!setupData.projectRoot) {
+ setError('Project root path is required');
+ return;
+ }
+ setCurrentStep(2);
+ } else if (currentStep === 2) {
+ // Check MCP server status
+ setLoading(true);
+ try {
+ const mcpStatus = await api.get('/mcp-utils/taskmaster-server');
+ setSetupData(prev => ({
+ ...prev,
+ mcpConfigured: mcpStatus.hasMCPServer && mcpStatus.isConfigured
+ }));
+ setCurrentStep(3);
+ } catch (err) {
+ setError('Failed to check MCP server status. You can continue but some features may not work.');
+ setCurrentStep(3);
+ }
+ } else if (currentStep === 3) {
+ // Validate PRD step
+ if (!setupData.prdContent.trim()) {
+ setError('Please create or import a PRD to continue');
+ return;
+ }
+ setCurrentStep(4);
+ } else if (currentStep === 4) {
+ // Complete setup
+ await completeSetup();
+ }
+ } catch (err) {
+ setError(err.message || 'An error occurred');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handlePrevious = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1);
+ setError(null);
+ }
+ };
+
+ const completeSetup = async () => {
+ setLoading(true);
+ try {
+ // Initialize TaskMaster project
+ const initResponse = await api.post('/taskmaster/initialize', {
+ projectRoot: setupData.projectRoot,
+ initGit: setupData.initGit,
+ storeTasksInGit: setupData.storeTasksInGit,
+ addAliases: setupData.addAliases,
+ skipInstall: setupData.skipInstall,
+ rules: setupData.rules,
+ yes: true
+ });
+
+ if (!initResponse.ok) {
+ throw new Error('Failed to initialize TaskMaster project');
+ }
+
+ // Save PRD content if provided
+ if (setupData.prdContent.trim()) {
+ const prdResponse = await api.post('/taskmaster/save-prd', {
+ projectRoot: setupData.projectRoot,
+ content: setupData.prdContent
+ });
+
+ if (!prdResponse.ok) {
+ console.warn('Failed to save PRD content');
+ }
+ }
+
+ // Parse PRD to generate initial tasks
+ if (setupData.prdContent.trim()) {
+ const parseResponse = await api.post('/taskmaster/parse-prd', {
+ projectRoot: setupData.projectRoot,
+ input: '.taskmaster/docs/prd.txt',
+ numTasks: '10',
+ research: false,
+ force: false
+ });
+
+ if (!parseResponse.ok) {
+ console.warn('Failed to parse PRD and generate tasks');
+ }
+ }
+
+ onComplete?.();
+ onClose?.();
+ } catch (err) {
+ setError(err.message || 'Failed to complete TaskMaster setup');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const copyMCPConfig = () => {
+ const mcpConfig = `{
+ "mcpServers": {
+ "": {
+ "command": "npx",
+ "args": ["-y", "--package=task-master-ai", "task-master-ai"],
+ "env": {
+ "ANTHROPIC_API_KEY": "your_anthropic_key_here",
+ "PERPLEXITY_API_KEY": "your_perplexity_key_here"
+ }
+ }
+ }
+}`;
+ navigator.clipboard.writeText(mcpConfig);
+ };
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+
+
+
+ Project Configuration
+
+
+ Configure TaskMaster settings for your project
+
+
+
+
+
+
+ Project Root Path
+
+ setSetupData(prev => ({ ...prev, projectRoot: e.target.value }))}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
+ placeholder="/path/to/your/project"
+ />
+
+
+
+
Options
+
+
+ setSetupData(prev => ({ ...prev, initGit: e.target.checked }))}
+ className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
+ />
+ Initialize Git repository
+
+
+
+ setSetupData(prev => ({ ...prev, storeTasksInGit: e.target.checked }))}
+ className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
+ />
+ Store tasks in Git
+
+
+
+ setSetupData(prev => ({ ...prev, addAliases: e.target.checked }))}
+ className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
+ />
+ Add shell aliases (tm, taskmaster)
+
+
+
+
+
+ Rule Profiles
+
+
+ {['claude', 'cursor', 'vscode', 'roo', 'cline', 'windsurf'].map(rule => (
+
+ {
+ if (e.target.checked) {
+ setSetupData(prev => ({ ...prev, rules: [...prev.rules, rule] }));
+ } else {
+ setSetupData(prev => ({ ...prev, rules: prev.rules.filter(r => r !== rule) }));
+ }
+ }}
+ className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
+ />
+ {rule}
+
+ ))}
+
+
+
+
+ );
+
+ case 2:
+ return (
+
+
+
+
+ MCP Server Setup
+
+
+ TaskMaster works best with the MCP server configured
+
+
+
+
+
+
+
+
+ MCP Server Configuration
+
+
+ To enable full TaskMaster integration, add the MCP server configuration to your Claude settings.
+
+
+
+
+ .mcp.json
+
+
+ Copy
+
+
+
+{`{
+ "mcpServers": {
+ "task-master-ai": {
+ "command": "npx",
+ "args": ["-y", "--package=task-master-ai", "task-master-ai"],
+ "env": {
+ "ANTHROPIC_API_KEY": "your_anthropic_key_here",
+ "PERPLEXITY_API_KEY": "your_perplexity_key_here"
+ }
+ }
+ }
+}`}
+
+
+
+
+
+
+
+
+
+
Current Status
+
+ {setupData.mcpConfigured ? (
+ <>
+
+
MCP server is configured
+ >
+ ) : (
+ <>
+
+
MCP server not detected (optional)
+ >
+ )}
+
+
+
+ );
+
+ case 3:
+ return (
+
+
+
+
+ Product Requirements Document
+
+
+ Create or import a PRD to generate initial tasks
+
+
+
+
+
+
+ PRD Content
+
+ setSetupData(prev => ({ ...prev, prdContent: e.target.value }))}
+ rows={12}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white font-mono text-sm"
+ placeholder="# Product Requirements Document
+
+## 1. Overview
+Describe your project or feature...
+
+## 2. Objectives
+- Primary goal
+- Success metrics
+
+## 3. User Stories
+- As a user, I want...
+
+## 4. Requirements
+- Feature requirements
+- Technical requirements
+
+## 5. Implementation Plan
+- Phase 1: Core features
+- Phase 2: Enhancements"
+ />
+
+
+
+
+
+
+
+ AI Task Generation
+
+
+ TaskMaster will analyze your PRD and automatically generate a structured task list with dependencies, priorities, and implementation details.
+
+
+
+
+
+
+ );
+
+ case 4:
+ return (
+
+
+
+
+ Complete Setup
+
+
+ Ready to initialize TaskMaster for your project
+
+
+
+
+
+ Setup Summary
+
+
+
+
+ Project: {setupData.projectRoot}
+
+
+
+ Rules: {setupData.rules.join(', ')}
+
+ {setupData.mcpConfigured && (
+
+
+ MCP server configured
+
+ )}
+
+
+ PRD content ready ({setupData.prdContent.length} characters)
+
+
+
+
+
+
+ What happens next?
+
+
+ Initialize TaskMaster project structure
+ Save your PRD to .taskmaster/docs/prd.txt
+ Generate initial tasks from your PRD
+ Set up project configuration and rules
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ TaskMaster Setup Wizard
+
+
+ Step {currentStep} of {totalSteps}: {steps[currentStep - 1]?.description}
+
+
+
+
+
+
+
+
+
+ {/* Progress Bar */}
+
+
+ {steps.map((step, index) => (
+
+
step.id
+ ? 'bg-green-500 text-white'
+ : currentStep === step.id
+ ? 'bg-blue-500 text-white'
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
+ )}>
+ {currentStep > step.id ? (
+
+ ) : (
+ step.id
+ )}
+
+ {index < steps.length - 1 && (
+
step.id
+ ? 'bg-green-500'
+ : 'bg-gray-200 dark:bg-gray-700'
+ )} />
+ )}
+
+ ))}
+
+
+ {steps.map(step => (
+
+ {step.title}
+
+ ))}
+
+
+
+ {/* Content */}
+
+ {renderStepContent()}
+
+ {error && (
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+ Previous
+
+
+
+ {currentStep} of {totalSteps}
+
+
+
+ {loading ? (
+ <>
+
+ {currentStep === totalSteps ? 'Setting up...' : 'Processing...'}
+ >
+ ) : (
+ <>
+ {currentStep === totalSteps ? 'Complete Setup' : 'Next'}
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default TaskMasterSetupWizard;
\ No newline at end of file
diff --git a/src/components/TaskMasterStatus.jsx b/src/components/TaskMasterStatus.jsx
new file mode 100644
index 0000000..27c953d
--- /dev/null
+++ b/src/components/TaskMasterStatus.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+import TaskIndicator from './TaskIndicator';
+
+const TaskMasterStatus = () => {
+ const {
+ currentProject,
+ projectTaskMaster,
+ mcpServerStatus,
+ isLoading,
+ isLoadingMCP,
+ error
+ } = useTaskMaster();
+
+ if (isLoading || isLoadingMCP) {
+ return (
+
+
+ Loading TaskMaster status...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ TaskMaster Error
+
+ );
+ }
+
+ // Show MCP server status
+ const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
+
+ // Show project TaskMaster status
+ const projectConfigured = currentProject?.taskmaster?.hasTaskmaster;
+ const taskCount = currentProject?.taskmaster?.metadata?.taskCount || 0;
+ const completedCount = currentProject?.taskmaster?.metadata?.completed || 0;
+
+ if (!currentProject) {
+ return (
+
+
+ No project selected
+
+ );
+ }
+
+ // Determine overall status for TaskIndicator
+ let overallStatus = 'not-configured';
+ if (projectConfigured && mcpConfigured) {
+ overallStatus = 'fully-configured';
+ } else if (projectConfigured) {
+ overallStatus = 'taskmaster-only';
+ } else if (mcpConfigured) {
+ overallStatus = 'mcp-only';
+ }
+
+ return (
+
+ {/* TaskMaster Status Indicator */}
+
+
+ {/* Task Progress Info */}
+ {projectConfigured && (
+
+
+ {completedCount}/{taskCount} tasks
+
+ {taskCount > 0 && (
+
+ ({Math.round((completedCount / taskCount) * 100)}%)
+
+ )}
+
+ )}
+
+ );
+};
+
+export default TaskMasterStatus;
\ No newline at end of file
diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx
index 9ddd559..e9028f0 100644
--- a/src/components/TodoList.jsx
+++ b/src/components/TodoList.jsx
@@ -51,9 +51,9 @@ const TodoList = ({ todos, isResult = false }) => {
)}
- {todos.map((todo) => (
+ {todos.map((todo, index) => (
diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx
index b0939c3..2103abb 100644
--- a/src/components/ToolsSettings.jsx
+++ b/src/components/ToolsSettings.jsx
@@ -4,9 +4,18 @@ import { Input } from './ui/input';
import { Badge } from './ui/badge';
import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
+import { useTasksSettings } from '../contexts/TasksSettingsContext';
function ToolsSettings({ isOpen, onClose, projects = [] }) {
const { isDarkMode, toggleDarkMode } = useTheme();
+ const {
+ tasksEnabled,
+ setTasksEnabled,
+ isTaskMasterInstalled,
+ isTaskMasterReady,
+ installationStatus,
+ isCheckingInstallation
+ } = useTasksSettings();
const [allowedTools, setAllowedTools] = useState([]);
const [disallowedTools, setDisallowedTools] = useState([]);
const [newAllowedTool, setNewAllowedTool] = useState('');
@@ -630,6 +639,16 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
>
Appearance
+ setActiveTab('tasks')}
+ className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
+ activeTab === 'tasks'
+ ? 'border-blue-600 text-blue-600 dark:text-blue-400'
+ : 'border-transparent text-muted-foreground hover:text-foreground'
+ }`}
+ >
+ Tasks
+
@@ -1674,6 +1693,160 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
)}
)}
+
+ {/* Tasks Tab */}
+ {activeTab === 'tasks' && (
+
+ {/* Installation Status Check */}
+ {isCheckingInstallation ? (
+
+
+
+
Checking TaskMaster installation...
+
+
+ ) : (
+ <>
+ {/* TaskMaster Not Installed Warning */}
+ {!isTaskMasterInstalled && (
+
+
+
+
+
+ TaskMaster AI CLI Not Installed
+
+
+
TaskMaster CLI is required to use task management features. Install it to get started:
+
+
+
+
+
After installation:
+
+ Restart this application
+ TaskMaster features will automatically become available
+ Use task-master init in your project directory
+
+
+
+
+
+
+ )}
+
+ {/* TaskMaster Settings */}
+
+
+
+
+
+ Enable TaskMaster Integration
+
+
+ Show TaskMaster tasks, banners, and sidebar indicators across the interface
+
+ {!isTaskMasterInstalled && (
+
+ TaskMaster CLI must be installed first
+
+ )}
+
+
+ setTasksEnabled(e.target.checked)}
+ disabled={!isTaskMasterInstalled}
+ className="sr-only peer"
+ />
+
+
+
+
+
+ {/* TaskMaster Information */}
+
+
+
+
+
+
+
+ 🎯 About TaskMaster AI
+
+
+
AI-Powered Task Management: Break complex projects into manageable subtasks with AI assistance
+
PRD: Generate structured tasks from Product Requirements Documents
+
Dependency Tracking: Understand task relationships and execution order
+
Progress Visualization: Kanban boards, and detailed task views
+
+
+
+ {/* GitHub Link and Resources */}
+
+
+
+
+
+ 📚 Learn More & Tutorial
+
+
+
TaskMaster AI (aka claude-task-master ) is an advanced AI-powered task management system built for developers.
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ )}
diff --git a/src/components/Tooltip.jsx b/src/components/Tooltip.jsx
new file mode 100644
index 0000000..10421a8
--- /dev/null
+++ b/src/components/Tooltip.jsx
@@ -0,0 +1,91 @@
+import React, { useState } from 'react';
+import { cn } from '../lib/utils';
+
+const Tooltip = ({
+ children,
+ content,
+ position = 'top',
+ className = '',
+ delay = 500
+}) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [timeoutId, setTimeoutId] = useState(null);
+
+ const handleMouseEnter = () => {
+ const id = setTimeout(() => {
+ setIsVisible(true);
+ }, delay);
+ setTimeoutId(id);
+ };
+
+ const handleMouseLeave = () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ setTimeoutId(null);
+ }
+ setIsVisible(false);
+ };
+
+ const getPositionClasses = () => {
+ switch (position) {
+ case 'top':
+ return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
+ case 'bottom':
+ return 'top-full left-1/2 transform -translate-x-1/2 mt-2';
+ case 'left':
+ return 'right-full top-1/2 transform -translate-y-1/2 mr-2';
+ case 'right':
+ return 'left-full top-1/2 transform -translate-y-1/2 ml-2';
+ default:
+ return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
+ }
+ };
+
+ const getArrowClasses = () => {
+ switch (position) {
+ case 'top':
+ return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
+ case 'bottom':
+ return 'bottom-full left-1/2 transform -translate-x-1/2 border-b-gray-900 dark:border-b-gray-100';
+ case 'left':
+ return 'left-full top-1/2 transform -translate-y-1/2 border-l-gray-900 dark:border-l-gray-100';
+ case 'right':
+ return 'right-full top-1/2 transform -translate-y-1/2 border-r-gray-900 dark:border-r-gray-100';
+ default:
+ return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
+ }
+ };
+
+ if (!content) {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ {isVisible && (
+
+ {content}
+
+ {/* Arrow */}
+
+
+ )}
+
+ );
+};
+
+export default Tooltip;
\ No newline at end of file
diff --git a/src/contexts/TaskMasterContext.jsx b/src/contexts/TaskMasterContext.jsx
new file mode 100644
index 0000000..e9f0019
--- /dev/null
+++ b/src/contexts/TaskMasterContext.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export default TaskMasterContext;
\ No newline at end of file
diff --git a/src/contexts/TasksSettingsContext.jsx b/src/contexts/TasksSettingsContext.jsx
new file mode 100644
index 0000000..611d906
--- /dev/null
+++ b/src/contexts/TasksSettingsContext.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export default TasksSettingsContext;
\ No newline at end of file
diff --git a/src/contexts/WebSocketContext.jsx b/src/contexts/WebSocketContext.jsx
new file mode 100644
index 0000000..5c9fab5
--- /dev/null
+++ b/src/contexts/WebSocketContext.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export default WebSocketContext;
\ No newline at end of file
diff --git a/src/utils/api.js b/src/utils/api.js
index 2297851..c1ae66a 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -86,4 +86,48 @@ export const api = {
body: formData,
headers: {}, // Let browser set Content-Type for FormData
}),
+
+ // TaskMaster endpoints
+ taskmaster: {
+ // Initialize TaskMaster in a project
+ init: (projectName) =>
+ authenticatedFetch(`/api/taskmaster/init/${projectName}`, {
+ method: 'POST',
+ }),
+
+ // Add a new task
+ addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
+ authenticatedFetch(`/api/taskmaster/add-task/${projectName}`, {
+ method: 'POST',
+ body: JSON.stringify({ prompt, title, description, priority, dependencies }),
+ }),
+
+ // Parse PRD to generate tasks
+ parsePRD: (projectName, { fileName, numTasks, append }) =>
+ authenticatedFetch(`/api/taskmaster/parse-prd/${projectName}`, {
+ method: 'POST',
+ body: JSON.stringify({ fileName, numTasks, append }),
+ }),
+
+ // Get available PRD templates
+ getTemplates: () =>
+ authenticatedFetch('/api/taskmaster/prd-templates'),
+
+ // Apply a PRD template
+ applyTemplate: (projectName, { templateId, fileName, customizations }) =>
+ authenticatedFetch(`/api/taskmaster/apply-template/${projectName}`, {
+ method: 'POST',
+ body: JSON.stringify({ templateId, fileName, customizations }),
+ }),
+
+ // Update a task
+ updateTask: (projectName, taskId, updates) =>
+ authenticatedFetch(`/api/taskmaster/update-task/${projectName}/${taskId}`, {
+ method: 'PUT',
+ body: JSON.stringify(updates),
+ }),
+ },
+
+ // Generic GET method for any endpoint
+ get: (endpoint) => authenticatedFetch(`/api${endpoint}`),
};
\ No newline at end of file
diff --git a/src/utils/websocket.js b/src/utils/websocket.js
index f03fd00..826cb21 100755
--- a/src/utils/websocket.js
+++ b/src/utils/websocket.js
@@ -17,7 +17,7 @@ export function useWebSocket() {
ws.close();
}
};
- }, []);
+ }, []); // Keep dependency array but add proper cleanup
const connect = async () => {
try {
diff --git a/store.db-shm b/store.db-shm
deleted file mode 100644
index fe9ac28..0000000
Binary files a/store.db-shm and /dev/null differ
diff --git a/store.db-wal b/store.db-wal
deleted file mode 100644
index e69de29..0000000