diff --git a/README.md b/README.md index fcb505cb..2d0f14bc 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-lock.json b/package-lock.json index 769aeb5d..a1972d5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-ui", - "version": "1.7.0", + "version": "v1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-ui", - "version": "1.7.0", + "version": "v1.8.0", "license": "MIT", "dependencies": { "@codemirror/lang-css": "^6.3.1", diff --git a/package.json b/package.json index c5b308c2..43175f2d 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 ca451337..f074c578 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 88d0e51f..8d1c7b45 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 00000000..1db7542f --- /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 00000000..adc2c7d0 --- /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 00000000..0ac6fee2 --- /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 00000000..eba17cb1 --- /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 1cbd7eb8..85b79737 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,12 +26,15 @@ 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'; +import { t } from './lib/i18n'; // Main App component with routing @@ -74,7 +77,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 = () => { @@ -474,8 +477,8 @@ function AppContent() {
-

Update Available

-

A new version is ready

+

{t('Update Available')}

+

{t('A new version is ready')}

@@ -690,14 +693,20 @@ function App() { return ( - - - - } /> - } /> - - - + + + + + + + } /> + } /> + + + + + + ); diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 26820b23..0c43a039 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -22,10 +22,13 @@ 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'; import { api, authenticatedFetch } from '../utils/api'; +import { t } from '../lib/i18n'; // Format "Claude AI usage limit reached|" into a local time string @@ -64,7 +67,7 @@ function formatUsageLimitText(text) { const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const dateReadable = `${reset.getDate()} ${months[reset.getMonth()]} ${reset.getFullYear()}`; - return `Claude usage limit reached. Your limit will reset at **${timeStr} ${tzHuman}** - ${dateReadable}`; + return t('usageLimitReached', { timeStr, tzHuman, dateReadable }); }); } catch { return text; @@ -81,19 +84,19 @@ const safeLocalStorage = { const parsed = JSON.parse(value); // Limit to last 50 messages to prevent storage bloat if (Array.isArray(parsed) && parsed.length > 50) { - console.warn(`Truncating chat history for ${key} from ${parsed.length} to 50 messages`); + console.warn(t('truncatingChatHistory', { key, from: parsed.length, to: 50 })); const truncated = parsed.slice(-50); value = JSON.stringify(truncated); } } catch (parseError) { - console.warn('Could not parse chat messages for truncation:', parseError); + console.warn(t('couldNotParseChatMessages'), parseError); } } localStorage.setItem(key, value); } catch (error) { if (error.name === 'QuotaExceededError') { - console.warn('localStorage quota exceeded, clearing old data'); + console.warn(t('localStorageQuotaExceeded')); // Clear old chat messages to free up space const keys = Object.keys(localStorage); const chatKeys = keys.filter(k => k.startsWith('chat_messages_')).sort(); @@ -102,7 +105,7 @@ const safeLocalStorage = { if (chatKeys.length > 3) { chatKeys.slice(0, chatKeys.length - 3).forEach(k => { localStorage.removeItem(k); - console.log(`Removed old chat data: ${k}`); + console.log(t('removedOldChatData', { key: k })); }); } @@ -116,7 +119,7 @@ const safeLocalStorage = { try { localStorage.setItem(key, value); } catch (retryError) { - console.error('Failed to save to localStorage even after cleanup:', retryError); + console.error(t('failedToSaveToLocalStorage'), retryError); // Last resort: Try to save just the last 10 messages if (key.startsWith('chat_messages_') && typeof value === 'string') { try { @@ -124,15 +127,15 @@ const safeLocalStorage = { if (Array.isArray(parsed) && parsed.length > 10) { const minimal = parsed.slice(-10); localStorage.setItem(key, JSON.stringify(minimal)); - console.warn('Saved only last 10 messages due to quota constraints'); + console.warn(t('savedOnlyLastMessages', { count: 10 })); } } catch (finalError) { - console.error('Final save attempt failed:', finalError); + console.error(t('finalSaveAttemptFailed'), finalError); } } } } else { - console.error('localStorage error:', error); + console.error(t('localStorageError'), error); } } }, @@ -140,7 +143,7 @@ const safeLocalStorage = { try { return localStorage.getItem(key); } catch (error) { - console.error('localStorage getItem error:', error); + console.error(t('localStorageGetItemError'), error); return null; } }, @@ -148,7 +151,7 @@ const safeLocalStorage = { try { localStorage.removeItem(key); } catch (error) { - console.error('localStorage removeItem error:', error); + console.error(t('localStorageRemoveItemError'), error); } } }; @@ -246,7 +249,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile )}
- {message.type === 'error' ? 'Error' : message.type === 'tool' ? 'Tool' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')} + {message.type === 'error' ? t('Error') : message.type === 'tool' ? t('Tool') : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? t('Cursor') : t('Claude'))}
)} @@ -264,7 +267,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile - Using {message.toolName} + {t('usingTool', { toolName: message.toolName })} {message.toolId} @@ -277,7 +280,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile onShowSettings(); }} className="p-1 rounded hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors" - title="Tool Settings" + title={t('Tool Settings')} > @@ -296,7 +299,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile - 📝 View edit diff for + 📝 {t('viewEditDiffFor')} - Diff + {t('Diff')}
@@ -351,7 +354,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile {showRawParameters && (
- View raw parameters + {t('viewRawParameters')}
                                   {message.toolInput}
@@ -368,7 +371,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
                   return (
                     
- View input parameters + {t('viewInputParameters')}
                         {message.toolInput}
@@ -398,7 +401,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
                               
                                 
                               
-                              📄 Creating new file: 
+                              📄 {t('creatingNewFile')}
                               
                                   
-                                    New File
+                                    {t('New File')}
                                   
                                 
@@ -453,7 +456,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile {showRawParameters && (
- View raw parameters + {t('viewRawParameters')}
                                     {message.toolInput}
@@ -480,14 +483,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
                               
                                 
                               
-                              Updating Todo List
+                              {t('updatingTodoList')}
                             
                             
{showRawParameters && (
- View raw parameters + {t('viewRawParameters')}
                                     {message.toolInput}
@@ -513,7 +516,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
                             
                               
                             
-                            Running command
+                            {t('runningCommand')}
                           
                           
@@ -521,7 +524,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile - Terminal + {t('Terminal')}
$ {input.command} @@ -535,7 +538,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile {showRawParameters && (
- View raw parameters + {t('viewRawParameters')}
                                   {message.toolInput}
@@ -559,7 +562,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
                         
                         return (
                           
- Read{' '} + {t('Read')}{' '}

- The file content is displayed in the diff view above + {t('fileContentDisplayedAbove')}

); @@ -871,7 +874,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile - View file content + {t('viewFileContent')}
@@ -889,7 +892,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile - View full output ({content.length} chars) + {t('viewFullOutput', { count: content.length })}
{content} @@ -919,7 +922,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile

- Interactive Prompt + {t('Interactive Prompt')}

{(() => { const lines = message.content.split('\n').filter(line => line.trim()); @@ -979,10 +982,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile

- ⏳ Waiting for your response in the CLI + {t('waitingForResponse')}

- Please select an option in your terminal where Claude is running. + {t('cliHelp')}

@@ -1000,7 +1003,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile const filename = input.file_path.split('/').pop(); return (
- 📖 Read{' '} + 📖 {t('Read')}{' '} @@ -1226,11 +1227,11 @@ function GitPanel({ selectedProject, isMobile }) { }`} />

- {confirmAction.type === 'discard' ? 'Discard Changes' : - confirmAction.type === 'delete' ? 'Delete File' : - confirmAction.type === 'commit' ? 'Confirm Commit' : - confirmAction.type === 'pull' ? 'Confirm Pull' : - confirmAction.type === 'publish' ? 'Publish Branch' : 'Confirm Push'} + {confirmAction.type === 'discard' ? t('discardChanges') : + confirmAction.type === 'delete' ? t('deleteFile') : + confirmAction.type === 'commit' ? t('Confirm Commit') : + confirmAction.type === 'pull' ? t('Confirm Pull') : + confirmAction.type === 'publish' ? t('Publish Branch') : t('Confirm Push')}

@@ -1243,7 +1244,7 @@ function GitPanel({ selectedProject, isMobile }) { onClick={() => setConfirmAction(null)} className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md" > - Cancel + {t('Cancel')} diff --git a/src/components/LoginForm.jsx b/src/components/LoginForm.jsx index f2a490a1..cb9b465b 100644 --- a/src/components/LoginForm.jsx +++ b/src/components/LoginForm.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { MessageSquare } from 'lucide-react'; +import { t } from '../lib/i18n'; const LoginForm = () => { const [username, setUsername] = useState(''); @@ -41,9 +42,9 @@ const LoginForm = () => {
-

Welcome Back

+

{t('welcomeBack')}

- Sign in to your Claude Code UI account + {t('signInToYourClaudeCodeUIAccount')}

@@ -51,7 +52,7 @@ const LoginForm = () => {
{ value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your username" + placeholder={t('enterUsernamePlaceholder')} required disabled={isLoading} /> @@ -67,7 +68,7 @@ const LoginForm = () => {
{ value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your password" + placeholder={t('enterPasswordPlaceholder')} required disabled={isLoading} /> @@ -92,13 +93,13 @@ const LoginForm = () => { disabled={isLoading} className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200" > - {isLoading ? 'Signing in...' : 'Sign In'} + {isLoading ? t('signingIn') : t('signIn')}

- Enter your credentials to access Claude Code UI + {t('enterCredentialsToAccess')}

diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx index 09a210a7..f43003e5 100644 --- a/src/components/MainContent.jsx +++ b/src/components/MainContent.jsx @@ -20,6 +20,14 @@ 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'; +import { t } from '../lib/i18n'; function MainContent({ selectedProject, @@ -46,6 +54,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 +123,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 (
@@ -89,8 +176,8 @@ function MainContent({ }} />
-

Loading Claude Code UI

-

Setting up your workspace...

+

{t('claudeCodeUI')}

+

{t('settingUpWorkspace')}

@@ -120,13 +207,13 @@ function MainContent({ -

Choose Your Project

+

{t('chooseYourProject')}

- Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history. + {t('selectAProject')}

- 💡 Tip: {isMobile ? 'Tap the menu button above to access projects' : 'Create a new project by clicking the folder icon in the sidebar'} + 💡 {t('Tip')}: {isMobile ? t('tipMobile') : t('tipDesktop')}

@@ -169,7 +256,7 @@ function MainContent({ {activeTab === 'chat' && selectedSession ? (

- {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')} + {selectedSession.__provider === 'cursor' ? (selectedSession.name || t('untitledSession')) : (selectedSession.summary || t('New Session'))}

{selectedProject.displayName} • {selectedSession.id} @@ -178,7 +265,7 @@ function MainContent({ ) : activeTab === 'chat' && !selectedSession ? (

- New Session + {t('New Session')}

{selectedProject.displayName} @@ -187,7 +274,10 @@ function MainContent({ ) : (

- {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'} + {activeTab === 'files' ? t('projectFiles') : + activeTab === 'git' ? t('sourceControl') : + (activeTab === 'tasks' && shouldShowTasksTab) ? t('taskMaster') : + t('Project')}

{selectedProject.displayName} @@ -201,66 +291,93 @@ function MainContent({ {/* Modern Tab Navigation - Right Side */}
- - - - + + + + + + + + + + + + + {shouldShowTasksTab && ( + + + + )} {/* */}
@@ -302,6 +419,7 @@ function MainContent({ showRawParameters={showRawParameters} autoScrollToBottom={autoScrollToBottom} sendByCtrlEnter={sendByCtrlEnter} + onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null} />
@@ -318,6 +436,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 185a9f43..b985142c 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 00000000..d4571ec6 --- /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 +
+
+
+
+
+
+ +
+
+ + {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 ? ( + + ) : ( + <> +
+ Add more tasks: Create additional tasks manually or generate them from a 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} +

+
+ +
+ + + {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} + + +
+
+
+ ); + } 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}

+
+
+ +
+ + {/* Terminal Container */} +
+
+ +
+
+ + {/* Modal Footer */} +
+
+
+ TaskMaster initialization will start automatically +
+ +
+
+
+
+ )} + + {/* 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

+ +
+ +
+
+ +
+ + {formData.useAI ? ( +
+ +