mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-08 23:49:38 +00:00
Merge pull request #183 from siteboon/feature/taskmasterAI-integration
Integration with TaskMaster AI
This commit is contained in:
21
README.md
21
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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
48
server/routes/mcp-utils.js
Normal file
48
server/routes/mcp-utils.js
Normal file
@@ -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;
|
||||
1971
server/routes/taskmaster.js
Normal file
1971
server/routes/taskmaster.js
Normal file
File diff suppressed because it is too large
Load Diff
198
server/utils/mcp-detector.js
Normal file
198
server/utils/mcp-detector.js
Normal file
@@ -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<Object>} 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<Object>} 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: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
129
server/utils/taskmaster-websocket.js
Normal file
129
server/utils/taskmaster-websocket.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
28
src/App.jsx
28
src/App.jsx
@@ -26,9 +26,11 @@ import MobileNav from './components/MobileNav';
|
||||
import ToolsSettings from './components/ToolsSettings';
|
||||
import QuickSettingsPanel from './components/QuickSettingsPanel';
|
||||
|
||||
import { useWebSocket } from './utils/websocket';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { TaskMasterProvider } from './contexts/TaskMasterContext';
|
||||
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
|
||||
import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import { useVersionCheck } from './hooks/useVersionCheck';
|
||||
import { api, authenticatedFetch } from './utils/api';
|
||||
@@ -74,7 +76,7 @@ function AppContent() {
|
||||
// until the conversation completes or is aborted.
|
||||
const [activeSessions, setActiveSessions] = useState(new Set()); // Track sessions with active conversations
|
||||
|
||||
const { ws, sendMessage, messages } = useWebSocket();
|
||||
const { ws, sendMessage, messages } = useWebSocketContext();
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
@@ -690,14 +692,20 @@ function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ProtectedRoute>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContent />} />
|
||||
<Route path="/session/:sessionId" element={<AppContent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ProtectedRoute>
|
||||
<WebSocketProvider>
|
||||
<TasksSettingsProvider>
|
||||
<TaskMasterProvider>
|
||||
<ProtectedRoute>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContent />} />
|
||||
<Route path="/session/:sessionId" element={<AppContent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ProtectedRoute>
|
||||
</TaskMasterProvider>
|
||||
</TasksSettingsProvider>
|
||||
</WebSocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,8 @@ import { useDropzone } from 'react-dropzone';
|
||||
import TodoList from './TodoList';
|
||||
import ClaudeLogo from './ClaudeLogo.jsx';
|
||||
import CursorLogo from './CursorLogo.jsx';
|
||||
import NextTaskBanner from './NextTaskBanner.jsx';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
import ClaudeStatus from './ClaudeStatus';
|
||||
import { MicButton } from './MicButton.jsx';
|
||||
@@ -1162,7 +1164,8 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
|
||||
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
|
||||
//
|
||||
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
|
||||
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) {
|
||||
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter, onTaskClick, onShowAllTasks }) {
|
||||
const { tasksEnabled } = useTasksSettings();
|
||||
const [input, setInput] = useState(() => {
|
||||
if (typeof window !== 'undefined' && selectedProject) {
|
||||
return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
||||
@@ -3092,6 +3095,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
: 'Select a provider above to begin'
|
||||
}
|
||||
</p>
|
||||
|
||||
{/* Show NextTaskBanner when provider is selected and ready */}
|
||||
{provider && tasksEnabled && (
|
||||
<div className="mt-4 px-4 sm:px-0">
|
||||
<NextTaskBanner
|
||||
onStartTask={() => setInput('Start the next task')}
|
||||
onShowAllTasks={onShowAllTasks}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedSession && (
|
||||
@@ -3100,6 +3113,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
<p className="text-sm sm:text-base leading-relaxed">
|
||||
Ask questions about your code, request changes, or get help with development tasks
|
||||
</p>
|
||||
|
||||
{/* Show NextTaskBanner for existing sessions too */}
|
||||
{tasksEnabled && (
|
||||
<div className="mt-4 px-4 sm:px-0">
|
||||
<NextTaskBanner
|
||||
onStartTask={() => setInput('Start the next task')}
|
||||
onShowAllTasks={onShowAllTasks}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
88
src/components/CreateTaskModal.jsx
Normal file
88
src/components/CreateTaskModal.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { X, Sparkles } from 'lucide-react';
|
||||
|
||||
const CreateTaskModal = ({ currentProject, onClose, onTaskCreated }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-200 dark:border-gray-700">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
||||
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Create AI-Generated Task</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* AI-First Approach */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||
💡 Pro Tip: Ask Claude Code Directly!
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 mb-3">
|
||||
You can simply ask Claude Code in the chat to create tasks for you.
|
||||
The AI assistant will automatically generate detailed tasks with research-backed insights.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded border border-blue-200 dark:border-blue-700 p-3 mb-3">
|
||||
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Example:</p>
|
||||
<p className="text-sm text-gray-900 dark:text-white font-mono">
|
||||
"Please add a new task to implement user profile image uploads using Cloudinary, research the best approach."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
<strong>This runs:</strong> <code className="bg-blue-100 dark:bg-blue-900/50 px-1 rounded text-xs">
|
||||
task-master add-task --prompt="Implement user profile image uploads using Cloudinary" --research
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Learn More Link */}
|
||||
<div className="text-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
For more examples and advanced usage patterns:
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master/blob/main/docs/examples.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-medium"
|
||||
>
|
||||
View TaskMaster Documentation →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Got it, I'll ask Claude Code directly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTaskModal;
|
||||
@@ -20,6 +20,13 @@ import GitPanel from './GitPanel';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import TaskList from './TaskList';
|
||||
import TaskDetail from './TaskDetail';
|
||||
import PRDEditor from './PRDEditor';
|
||||
import Tooltip from './Tooltip';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
@@ -46,6 +53,60 @@ function MainContent({
|
||||
sendByCtrlEnter // Send by Ctrl+Enter mode for East Asian language input
|
||||
}) {
|
||||
const [editingFile, setEditingFile] = useState(null);
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||
|
||||
// PRD Editor state
|
||||
const [showPRDEditor, setShowPRDEditor] = useState(false);
|
||||
const [selectedPRD, setSelectedPRD] = useState(null);
|
||||
const [existingPRDs, setExistingPRDs] = useState([]);
|
||||
const [prdNotification, setPRDNotification] = useState(null);
|
||||
|
||||
// TaskMaster context
|
||||
const { tasks, currentProject, refreshTasks, setCurrentProject } = useTaskMaster();
|
||||
const { tasksEnabled, isTaskMasterInstalled, isTaskMasterReady } = useTasksSettings();
|
||||
|
||||
// Only show tasks tab if TaskMaster is installed and enabled
|
||||
const shouldShowTasksTab = tasksEnabled && isTaskMasterInstalled;
|
||||
|
||||
// Sync selectedProject with TaskMaster context
|
||||
useEffect(() => {
|
||||
if (selectedProject && selectedProject !== currentProject) {
|
||||
setCurrentProject(selectedProject);
|
||||
}
|
||||
}, [selectedProject, currentProject, setCurrentProject]);
|
||||
|
||||
// Switch away from tasks tab when tasks are disabled or TaskMaster is not installed
|
||||
useEffect(() => {
|
||||
if (!shouldShowTasksTab && activeTab === 'tasks') {
|
||||
setActiveTab('chat');
|
||||
}
|
||||
}, [shouldShowTasksTab, activeTab, setActiveTab]);
|
||||
|
||||
// Load existing PRDs when current project changes
|
||||
useEffect(() => {
|
||||
const loadExistingPRDs = async () => {
|
||||
if (!currentProject?.name) {
|
||||
setExistingPRDs([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setExistingPRDs(data.prdFiles || []);
|
||||
} else {
|
||||
setExistingPRDs([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load existing PRDs:', error);
|
||||
setExistingPRDs([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadExistingPRDs();
|
||||
}, [currentProject?.name]);
|
||||
|
||||
const handleFileOpen = (filePath, diffInfo = null) => {
|
||||
// Create a file object that CodeEditor expects
|
||||
@@ -61,6 +122,31 @@ function MainContent({
|
||||
const handleCloseEditor = () => {
|
||||
setEditingFile(null);
|
||||
};
|
||||
|
||||
const handleTaskClick = (task) => {
|
||||
// If task is just an ID (from dependency click), find the full task object
|
||||
if (typeof task === 'object' && task.id && !task.title) {
|
||||
const fullTask = tasks?.find(t => t.id === task.id);
|
||||
if (fullTask) {
|
||||
setSelectedTask(fullTask);
|
||||
setShowTaskDetail(true);
|
||||
}
|
||||
} else {
|
||||
setSelectedTask(task);
|
||||
setShowTaskDetail(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskDetailClose = () => {
|
||||
setShowTaskDetail(false);
|
||||
setSelectedTask(null);
|
||||
};
|
||||
|
||||
const handleTaskStatusChange = (taskId, newStatus) => {
|
||||
// This would integrate with TaskMaster API to update task status
|
||||
console.log('Update task status:', taskId, newStatus);
|
||||
refreshTasks?.();
|
||||
};
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -187,7 +273,10 @@ function MainContent({
|
||||
) : (
|
||||
<div>
|
||||
<h2 className="text-base sm:text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
|
||||
{activeTab === 'files' ? 'Project Files' :
|
||||
activeTab === 'git' ? 'Source Control' :
|
||||
(activeTab === 'tasks' && shouldShowTasksTab) ? 'TaskMaster' :
|
||||
'Project'}
|
||||
</h2>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{selectedProject.displayName}
|
||||
@@ -201,66 +290,93 @@ function MainContent({
|
||||
{/* Modern Tab Navigation - Right Side */}
|
||||
<div className="flex-shrink-0 hidden sm:block">
|
||||
<div className="relative flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
|
||||
activeTab === 'chat'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Chat</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('shell')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'shell'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Shell</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('files')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'files'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Files</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('git')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'git'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Source Control</span>
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip content="Chat" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
|
||||
activeTab === 'chat'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Chat</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Shell" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('shell')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'shell'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Shell</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Files" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('files')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'files'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Files</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Source Control" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('git')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'git'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Source Control</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{shouldShowTasksTab && (
|
||||
<Tooltip content="Tasks" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('tasks')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'tasks'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Tasks</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* <button
|
||||
onClick={() => setActiveTab('preview')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
@@ -302,6 +418,7 @@ function MainContent({
|
||||
showRawParameters={showRawParameters}
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
sendByCtrlEnter={sendByCtrlEnter}
|
||||
onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@@ -318,6 +435,40 @@ function MainContent({
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'git' ? 'block' : 'hidden'}`}>
|
||||
<GitPanel selectedProject={selectedProject} isMobile={isMobile} />
|
||||
</div>
|
||||
{shouldShowTasksTab && (
|
||||
<div className={`h-full ${activeTab === 'tasks' ? 'block' : 'hidden'}`}>
|
||||
<div className="h-full flex flex-col overflow-hidden">
|
||||
<TaskList
|
||||
tasks={tasks || []}
|
||||
onTaskClick={handleTaskClick}
|
||||
showParentTasks={true}
|
||||
className="flex-1 overflow-y-auto p-4"
|
||||
currentProject={currentProject}
|
||||
onTaskCreated={refreshTasks}
|
||||
onShowPRDEditor={(prd = null) => {
|
||||
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));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'preview' ? 'block' : 'hidden'}`}>
|
||||
{/* <LivePreviewPanel
|
||||
selectedProject={selectedProject}
|
||||
@@ -354,6 +505,63 @@ function MainContent({
|
||||
projectPath={selectedProject?.path}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Task Detail Modal */}
|
||||
{shouldShowTasksTab && showTaskDetail && selectedTask && (
|
||||
<TaskDetail
|
||||
task={selectedTask}
|
||||
isOpen={showTaskDetail}
|
||||
onClose={handleTaskDetailClose}
|
||||
onStatusChange={handleTaskStatusChange}
|
||||
onTaskClick={handleTaskClick}
|
||||
/>
|
||||
)}
|
||||
{/* PRD Editor Modal */}
|
||||
{showPRDEditor && (
|
||||
<PRDEditor
|
||||
project={currentProject}
|
||||
projectPath={currentProject?.fullPath || currentProject?.path}
|
||||
onClose={() => {
|
||||
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 && (
|
||||
<div className="fixed bottom-4 right-4 z-50 animate-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="bg-green-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="font-medium">{prdNotification}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
695
src/components/NextTaskBanner.jsx
Normal file
695
src/components/NextTaskBanner.jsx
Normal file
@@ -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 = (
|
||||
<div className={cn(
|
||||
'bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-3 mb-4',
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
TaskMaster AI is not configured
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setShowTaskOptions(!showTaskOptions)}
|
||||
className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Settings className="w-3 h-3" />
|
||||
Initialize TaskMaster AI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showTaskOptions && (
|
||||
<div className="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
|
||||
{!projectTaskMaster?.hasTaskmaster && (
|
||||
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/50 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
🎯 What is TaskMaster?
|
||||
</h4>
|
||||
<div className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
|
||||
<p>• <strong>AI-Powered Task Management:</strong> Break complex projects into manageable subtasks</p>
|
||||
<p>• <strong>PRD Templates:</strong> Generate tasks from Product Requirements Documents</p>
|
||||
<p>• <strong>Dependency Tracking:</strong> Understand task relationships and execution order</p>
|
||||
<p>• <strong>Progress Visualization:</strong> Kanban boards and detailed task analytics</p>
|
||||
<p>• <strong>CLI Integration:</strong> Use taskmaster commands for advanced workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{!projectTaskMaster?.hasTaskmaster ? (
|
||||
<button
|
||||
className="text-xs px-3 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 rounded transition-colors text-left flex items-center gap-2"
|
||||
onClick={() => setShowCLI(true)}
|
||||
>
|
||||
<Terminal className="w-3 h-3" />
|
||||
Initialize TaskMaster
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-2 p-2 bg-green-50 dark:bg-green-900/30 rounded text-xs text-green-800 dark:text-green-200">
|
||||
<strong>Add more tasks:</strong> Create additional tasks manually or generate them from a PRD template
|
||||
</div>
|
||||
<button
|
||||
className="text-xs px-3 py-2 bg-green-100 dark:bg-green-900 hover:bg-green-200 dark:hover:bg-green-800 text-green-800 dark:text-green-200 rounded transition-colors text-left flex items-center gap-2 disabled:opacity-50"
|
||||
onClick={handleCreateManualTask}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
Create a new task manually
|
||||
</button>
|
||||
<button
|
||||
className="text-xs px-3 py-2 bg-purple-100 dark:bg-purple-900 hover:bg-purple-200 dark:hover:bg-purple-800 text-purple-800 dark:text-purple-200 rounded transition-colors text-left flex items-center gap-2 disabled:opacity-50"
|
||||
onClick={handleParsePRD}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
{isLoading ? 'Parsing...' : 'Generate tasks from PRD template'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (nextTask) {
|
||||
// Show next task if available
|
||||
bannerContent = (
|
||||
<div className={cn(
|
||||
'bg-slate-50 dark:bg-slate-900/30 border border-slate-200 dark:border-slate-700 rounded-lg p-3 mb-4',
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-5 h-5 bg-blue-100 dark:bg-blue-900/50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Target className="w-3 h-3 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400 font-medium">Task {nextTask.id}</span>
|
||||
{nextTask.priority === 'high' && (
|
||||
<div className="w-4 h-4 rounded bg-red-100 dark:bg-red-900/50 flex items-center justify-center" title="High Priority">
|
||||
<Zap className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
)}
|
||||
{nextTask.priority === 'medium' && (
|
||||
<div className="w-4 h-4 rounded bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center" title="Medium Priority">
|
||||
<Flag className="w-2.5 h-2.5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
)}
|
||||
{nextTask.priority === 'low' && (
|
||||
<div className="w-4 h-4 rounded bg-gray-100 dark:bg-gray-800 flex items-center justify-center" title="Low Priority">
|
||||
<Circle className="w-2.5 h-2.5 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 line-clamp-1">
|
||||
{nextTask.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onStartTask?.()}
|
||||
className="text-xs px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium transition-colors shadow-sm flex items-center gap-1"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
Start Task
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowTaskDetail(true)}
|
||||
className="text-xs px-2 py-1.5 border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-md transition-colors flex items-center gap-1"
|
||||
title="View task details"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
</button>
|
||||
{onShowAllTasks && (
|
||||
<button
|
||||
onClick={onShowAllTasks}
|
||||
className="text-xs px-2 py-1.5 border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-md transition-colors flex items-center gap-1"
|
||||
title="View all tasks"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
} 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 = (
|
||||
<div className={cn(
|
||||
'bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800 rounded-lg p-3 mb-4',
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{completedTasks === totalTasks ? "All done! 🎉" : "No pending tasks"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{completedTasks}/{totalTasks}
|
||||
</span>
|
||||
<button
|
||||
onClick={onShowAllTasks}
|
||||
className="text-xs px-2 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded transition-colors"
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// TaskMaster is configured but no tasks exist - don't show anything in chat
|
||||
bannerContent = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{bannerContent}
|
||||
|
||||
{/* Create Task Modal */}
|
||||
{showCreateTaskModal && (
|
||||
<CreateTaskModal
|
||||
currentProject={currentProject}
|
||||
onClose={() => setShowCreateTaskModal(false)}
|
||||
onTaskCreated={() => {
|
||||
refreshTasks();
|
||||
setShowCreateTaskModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Template Selector Modal */}
|
||||
{showTemplateSelector && (
|
||||
<TemplateSelector
|
||||
currentProject={currentProject}
|
||||
onClose={() => setShowTemplateSelector(false)}
|
||||
onTemplateApplied={() => {
|
||||
refreshTasks();
|
||||
setShowTemplateSelector(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TaskMaster CLI Setup Modal */}
|
||||
{showCLI && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-4xl h-[600px] flex flex-col">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
||||
<Terminal className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">TaskMaster Setup</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Interactive CLI for {currentProject?.displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCLI(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Terminal Container */}
|
||||
<div className="flex-1 p-4">
|
||||
<div className="h-full bg-black rounded-lg overflow-hidden">
|
||||
<Shell
|
||||
selectedProject={currentProject}
|
||||
selectedSession={null}
|
||||
isActive={true}
|
||||
initialCommand="npx task-master init"
|
||||
isPlainShell={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
TaskMaster initialization will start automatically
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCLI(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task Detail Modal */}
|
||||
{showTaskDetail && nextTask && (
|
||||
<TaskDetail
|
||||
task={nextTask}
|
||||
isOpen={showTaskDetail}
|
||||
onClose={() => 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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Create New Task</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.useAI}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, useAI: e.target.checked }))}
|
||||
/>
|
||||
Use AI to generate task details
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{formData.useAI ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Task Description (AI will generate details)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.prompt}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, prompt: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
rows="3"
|
||||
placeholder="Describe what you want to accomplish..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Task Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Enter task title..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
rows="3"
|
||||
placeholder="Describe the task..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded disabled:opacity-50"
|
||||
disabled={isSubmitting || (formData.useAI && !formData.prompt.trim()) || (!formData.useAI && (!formData.title.trim() || !formData.description.trim()))}
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create Task'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Template Selector Modal Component
|
||||
const TemplateSelector = ({ currentProject, onClose, onTemplateApplied }) => {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
||||
const [customizations, setCustomizations] = useState({});
|
||||
const [fileName, setFileName] = useState('prd.txt');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [step, setStep] = useState('select'); // 'select', 'customize', 'generate'
|
||||
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const response = await api.taskmaster.getTemplates();
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTemplates(data.templates);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
}, []);
|
||||
|
||||
const handleSelectTemplate = (template) => {
|
||||
setSelectedTemplate(template);
|
||||
// Find placeholders in template content
|
||||
const placeholders = template.content.match(/\[([^\]]+)\]/g) || [];
|
||||
const uniquePlaceholders = [...new Set(placeholders.map(p => p.slice(1, -1)))];
|
||||
|
||||
const initialCustomizations = {};
|
||||
uniquePlaceholders.forEach(placeholder => {
|
||||
initialCustomizations[placeholder] = '';
|
||||
});
|
||||
|
||||
setCustomizations(initialCustomizations);
|
||||
setStep('customize');
|
||||
};
|
||||
|
||||
const handleApplyTemplate = async () => {
|
||||
if (!selectedTemplate || !currentProject) return;
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
// Apply template
|
||||
const applyResponse = await api.taskmaster.applyTemplate(currentProject.name, {
|
||||
templateId: selectedTemplate.id,
|
||||
fileName,
|
||||
customizations
|
||||
});
|
||||
|
||||
if (!applyResponse.ok) {
|
||||
const error = await applyResponse.json();
|
||||
throw new Error(error.message || 'Failed to apply template');
|
||||
}
|
||||
|
||||
// Parse PRD to generate tasks
|
||||
const parseResponse = await api.taskmaster.parsePRD(currentProject.name, {
|
||||
fileName,
|
||||
numTasks: 10
|
||||
});
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
const error = await parseResponse.json();
|
||||
throw new Error(error.message || 'Failed to generate tasks');
|
||||
}
|
||||
|
||||
setStep('generate');
|
||||
setTimeout(() => {
|
||||
onTemplateApplied();
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error applying template:', error);
|
||||
alert(`Error: ${error.message}`);
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading templates...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{step === 'select' ? 'Select PRD Template' :
|
||||
step === 'customize' ? 'Customize Template' :
|
||||
'Generating Tasks'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{step === 'select' && (
|
||||
<div className="space-y-3">
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className="p-4 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
onClick={() => handleSelectTemplate(template)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">{template.name}</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{template.description}</p>
|
||||
<span className="inline-block text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded mt-2">
|
||||
{template.category}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-gray-400 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'customize' && selectedTemplate && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
File Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="prd.txt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{Object.keys(customizations).length > 0 && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Customize Template
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(customizations).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<label className="block text-xs text-gray-600 dark:text-gray-400 mb-1">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setCustomizations(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder={`Enter ${key.toLowerCase()}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button
|
||||
onClick={() => setStep('select')}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleApplyTemplate}
|
||||
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded disabled:opacity-50"
|
||||
disabled={isApplying}
|
||||
>
|
||||
{isApplying ? 'Applying...' : 'Apply & Generate Tasks'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'generate' && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Template Applied Successfully!
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Your PRD has been created and tasks are being generated...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NextTaskBanner;
|
||||
871
src/components/PRDEditor.jsx
Normal file
871
src/components/PRDEditor.jsx
Normal file
@@ -0,0 +1,871 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { X, Save, Download, Maximize2, Minimize2, Eye, FileText, Sparkles, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { api, authenticatedFetch } from '../utils/api';
|
||||
|
||||
const PRDEditor = ({
|
||||
file,
|
||||
onClose,
|
||||
projectPath,
|
||||
project, // Add project object
|
||||
initialContent = '',
|
||||
isNewFile = false,
|
||||
onSave
|
||||
}) => {
|
||||
const [content, setContent] = useState(initialContent);
|
||||
const [loading, setLoading] = useState(!isNewFile);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(true);
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [previewMode, setPreviewMode] = useState(false);
|
||||
const [wordWrap, setWordWrap] = useState(true); // Default to true for markdown
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [showGenerateModal, setShowGenerateModal] = useState(false);
|
||||
const [showOverwriteConfirm, setShowOverwriteConfirm] = useState(false);
|
||||
const [existingPRDs, setExistingPRDs] = useState([]);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const PRD_TEMPLATE = `# Product Requirements Document - Example Project
|
||||
|
||||
## 1. Overview
|
||||
**Product Name:** AI-Powered Task Manager
|
||||
**Version:** 1.0
|
||||
**Date:** 2024-12-27
|
||||
**Author:** Development Team
|
||||
|
||||
This document outlines the requirements for building an AI-powered task management application that integrates with development workflows and provides intelligent task breakdown and prioritization.
|
||||
|
||||
## 2. Objectives
|
||||
- Create an intuitive task management system that works seamlessly with developer tools
|
||||
- Provide AI-powered task generation from high-level requirements
|
||||
- Enable real-time collaboration and progress tracking
|
||||
- Integrate with popular development environments (VS Code, Cursor, etc.)
|
||||
|
||||
### Success Metrics
|
||||
- User adoption rate > 80% within development teams
|
||||
- Task completion rate improvement of 25%
|
||||
- Time-to-delivery reduction of 15%
|
||||
|
||||
## 3. User Stories
|
||||
|
||||
### Core Functionality
|
||||
- As a project manager, I want to create PRDs that automatically generate detailed tasks so I can save time on project planning
|
||||
- As a developer, I want to see my next task clearly highlighted so I can maintain focus
|
||||
- As a team lead, I want to track progress across multiple projects so I can provide accurate status updates
|
||||
- As a developer, I want tasks to be broken down into implementable subtasks so I can work more efficiently
|
||||
|
||||
### AI Integration
|
||||
- As a user, I want to describe a feature in natural language and get detailed implementation tasks so I can start working immediately
|
||||
- As a project manager, I want the AI to analyze task complexity and suggest appropriate time estimates
|
||||
- As a developer, I want intelligent task prioritization based on dependencies and deadlines
|
||||
|
||||
### Collaboration
|
||||
- As a team member, I want to see real-time updates when tasks are completed so I can coordinate my work
|
||||
- As a stakeholder, I want to view project progress through intuitive dashboards
|
||||
- As a developer, I want to add implementation notes to tasks for future reference
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### Task Management
|
||||
- Create, edit, and delete tasks with rich metadata (priority, status, dependencies, estimates)
|
||||
- Hierarchical task structure with subtasks and sub-subtasks
|
||||
- Real-time status updates and progress tracking
|
||||
- Dependency management with circular dependency detection
|
||||
- Bulk operations (move, update status, assign)
|
||||
|
||||
### AI Features
|
||||
- Natural language PRD parsing to generate structured tasks
|
||||
- Intelligent task breakdown with complexity analysis
|
||||
- Automated subtask generation with implementation details
|
||||
- Smart dependency suggestion
|
||||
- Progress prediction based on historical data
|
||||
|
||||
### Integration Features
|
||||
- VS Code/Cursor extension for in-editor task management
|
||||
- Git integration for linking commits to tasks
|
||||
- API for third-party tool integration
|
||||
- Webhook support for external notifications
|
||||
- CLI tool for command-line task management
|
||||
|
||||
### User Interface
|
||||
- Responsive web application (desktop and mobile)
|
||||
- Multiple view modes (Kanban, list, calendar)
|
||||
- Dark/light theme support
|
||||
- Drag-and-drop task organization
|
||||
- Advanced filtering and search capabilities
|
||||
- Keyboard shortcuts for power users
|
||||
|
||||
## 5. Technical Requirements
|
||||
|
||||
### Frontend
|
||||
- React.js with TypeScript for type safety
|
||||
- Modern UI framework (Tailwind CSS)
|
||||
- State management (Context API or Redux)
|
||||
- Real-time updates via WebSockets
|
||||
- Progressive Web App (PWA) support
|
||||
- Accessibility compliance (WCAG 2.1 AA)
|
||||
|
||||
### Backend
|
||||
- Node.js with Express.js framework
|
||||
- RESTful API design with OpenAPI documentation
|
||||
- Real-time communication via Socket.io
|
||||
- Background job processing
|
||||
- Rate limiting and security middleware
|
||||
|
||||
### AI Integration
|
||||
- Integration with multiple AI providers (OpenAI, Anthropic, etc.)
|
||||
- Fallback model support
|
||||
- Context-aware prompt engineering
|
||||
- Token usage optimization
|
||||
- Model response caching
|
||||
|
||||
### Database
|
||||
- Primary: PostgreSQL for relational data
|
||||
- Cache: Redis for session management and real-time features
|
||||
- Full-text search capabilities
|
||||
- Database migrations and seeding
|
||||
- Backup and recovery procedures
|
||||
|
||||
### Infrastructure
|
||||
- Docker containerization
|
||||
- Cloud deployment (AWS/GCP/Azure)
|
||||
- Auto-scaling capabilities
|
||||
- Monitoring and logging (structured logging)
|
||||
- CI/CD pipeline with automated testing
|
||||
|
||||
## 6. Non-Functional Requirements
|
||||
|
||||
### Performance
|
||||
- Page load time < 2 seconds
|
||||
- API response time < 500ms for 95% of requests
|
||||
- Support for 1000+ concurrent users
|
||||
- Efficient handling of large task lists (10,000+ tasks)
|
||||
|
||||
### Security
|
||||
- JWT-based authentication with refresh tokens
|
||||
- Role-based access control (RBAC)
|
||||
- Data encryption at rest and in transit
|
||||
- Regular security audits and penetration testing
|
||||
- GDPR and privacy compliance
|
||||
|
||||
### Reliability
|
||||
- 99.9% uptime SLA
|
||||
- Graceful error handling and recovery
|
||||
- Data backup every 6 hours with point-in-time recovery
|
||||
- Disaster recovery plan with RTO < 4 hours
|
||||
|
||||
### Scalability
|
||||
- Horizontal scaling for both frontend and backend
|
||||
- Database read replicas for query optimization
|
||||
- CDN for static asset delivery
|
||||
- Microservices architecture for future expansion
|
||||
|
||||
## 7. User Experience Design
|
||||
|
||||
### Information Architecture
|
||||
- Intuitive navigation with breadcrumbs
|
||||
- Context-aware menus and actions
|
||||
- Progressive disclosure of complex features
|
||||
- Consistent design patterns throughout
|
||||
|
||||
### Interaction Design
|
||||
- Smooth animations and transitions
|
||||
- Immediate feedback for user actions
|
||||
- Undo/redo functionality for critical operations
|
||||
- Smart defaults and auto-save features
|
||||
|
||||
### Visual Design
|
||||
- Modern, clean interface with plenty of whitespace
|
||||
- Consistent color scheme and typography
|
||||
- Clear visual hierarchy with proper contrast ratios
|
||||
- Iconography that supports comprehension
|
||||
|
||||
## 8. Integration Requirements
|
||||
|
||||
### Development Tools
|
||||
- VS Code extension with task panel and quick actions
|
||||
- Cursor IDE integration with AI task suggestions
|
||||
- Terminal CLI for command-line workflow
|
||||
- Browser extension for web-based tools
|
||||
|
||||
### Third-Party Services
|
||||
- GitHub/GitLab integration for issue sync
|
||||
- Slack/Discord notifications
|
||||
- Calendar integration (Google Calendar, Outlook)
|
||||
- Time tracking tools (Toggl, Harvest)
|
||||
|
||||
### APIs and Webhooks
|
||||
- RESTful API with comprehensive documentation
|
||||
- GraphQL endpoint for complex queries
|
||||
- Webhook system for external integrations
|
||||
- SDK development for major programming languages
|
||||
|
||||
## 9. Implementation Phases
|
||||
|
||||
### Phase 1: Core MVP (8-10 weeks)
|
||||
- Basic task management (CRUD operations)
|
||||
- Simple AI task generation
|
||||
- Web interface with essential features
|
||||
- User authentication and basic permissions
|
||||
|
||||
### Phase 2: Enhanced Features (6-8 weeks)
|
||||
- Advanced AI features (complexity analysis, subtask generation)
|
||||
- Real-time collaboration
|
||||
- Mobile-responsive design
|
||||
- Integration with one development tool (VS Code)
|
||||
|
||||
### Phase 3: Enterprise Features (4-6 weeks)
|
||||
- Advanced user management and permissions
|
||||
- API and webhook system
|
||||
- Performance optimization
|
||||
- Comprehensive testing and security audit
|
||||
|
||||
### Phase 4: Ecosystem Expansion (4-6 weeks)
|
||||
- Additional tool integrations
|
||||
- Mobile app development
|
||||
- Advanced analytics and reporting
|
||||
- Third-party marketplace preparation
|
||||
|
||||
## 10. Risk Assessment
|
||||
|
||||
### Technical Risks
|
||||
- AI model reliability and cost management
|
||||
- Real-time synchronization complexity
|
||||
- Database performance with large datasets
|
||||
- Integration complexity with multiple tools
|
||||
|
||||
### Business Risks
|
||||
- User adoption in competitive market
|
||||
- AI provider dependency
|
||||
- Data privacy and security concerns
|
||||
- Feature scope creep and timeline delays
|
||||
|
||||
### Mitigation Strategies
|
||||
- Implement robust error handling and fallback systems
|
||||
- Develop comprehensive testing strategy
|
||||
- Create detailed documentation and user guides
|
||||
- Establish clear project scope and change management process
|
||||
|
||||
## 11. Success Criteria
|
||||
|
||||
### Development Milestones
|
||||
- Alpha version with core features completed
|
||||
- Beta version with selected user group feedback
|
||||
- Production-ready version with full feature set
|
||||
- Post-launch iterations based on user feedback
|
||||
|
||||
### Business Metrics
|
||||
- User engagement and retention rates
|
||||
- Task completion and productivity metrics
|
||||
- Customer satisfaction scores (NPS > 50)
|
||||
- Revenue targets and subscription growth
|
||||
|
||||
## 12. Appendices
|
||||
|
||||
### Glossary
|
||||
- **PRD**: Product Requirements Document
|
||||
- **AI**: Artificial Intelligence
|
||||
- **CRUD**: Create, Read, Update, Delete
|
||||
- **API**: Application Programming Interface
|
||||
- **CI/CD**: Continuous Integration/Continuous Deployment
|
||||
|
||||
### References
|
||||
- Industry best practices for task management
|
||||
- AI integration patterns and examples
|
||||
- Security and compliance requirements
|
||||
- Performance benchmarking data
|
||||
|
||||
---
|
||||
|
||||
**Document Control:**
|
||||
- Version: 1.0
|
||||
- Last Updated: December 27, 2024
|
||||
- Next Review: January 15, 2025
|
||||
- Approved By: Product Owner, Technical Lead`;
|
||||
|
||||
// Initialize filename and load content
|
||||
useEffect(() => {
|
||||
const initializeEditor = async () => {
|
||||
// Set initial filename
|
||||
if (file?.name) {
|
||||
setFileName(file.name.replace(/\.(txt|md)$/, '')); // Remove extension for editing
|
||||
} else if (isNewFile) {
|
||||
// Generate default filename based on current date
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
setFileName(`prd-${dateStr}`);
|
||||
}
|
||||
|
||||
// Load content
|
||||
if (isNewFile) {
|
||||
setContent(PRD_TEMPLATE);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If content is directly provided (for existing PRDs loaded from API)
|
||||
if (file.content) {
|
||||
setContent(file.content);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to loading from file path (legacy support)
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await api.readFile(file.projectName, file.path);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setContent(data.content || PRD_TEMPLATE);
|
||||
} catch (error) {
|
||||
console.error('Error loading PRD file:', error);
|
||||
setContent(`# Error Loading PRD\n\nError: ${error.message}\n\nFile: ${file?.name || 'New PRD'}\nPath: ${file?.path || 'Not saved yet'}\n\n${PRD_TEMPLATE}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeEditor();
|
||||
}, [file, projectPath, isNewFile]);
|
||||
|
||||
// Fetch existing PRDs to check for conflicts
|
||||
useEffect(() => {
|
||||
const fetchExistingPRDs = async () => {
|
||||
if (!project?.name) {
|
||||
console.log('No project name available:', project);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching PRDs for project:', project.name);
|
||||
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Fetched existing PRDs:', data.prds);
|
||||
setExistingPRDs(data.prds || []);
|
||||
} else {
|
||||
console.log('Failed to fetch PRDs:', response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching existing PRDs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchExistingPRDs();
|
||||
}, [project?.name]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!content.trim()) {
|
||||
alert('Please add content before saving.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.trim()) {
|
||||
alert('Please provide a filename for the PRD.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file already exists
|
||||
const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
|
||||
const existingFile = existingPRDs.find(prd => prd.name === fullFileName);
|
||||
|
||||
console.log('Save check:', {
|
||||
fullFileName,
|
||||
existingPRDs,
|
||||
existingFile,
|
||||
isExisting: file?.isExisting,
|
||||
fileObject: file,
|
||||
shouldShowModal: existingFile && !file?.isExisting
|
||||
});
|
||||
|
||||
if (existingFile && !file?.isExisting) {
|
||||
console.log('Showing overwrite confirmation modal');
|
||||
// Show confirmation modal for overwrite
|
||||
setShowOverwriteConfirm(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await performSave();
|
||||
};
|
||||
|
||||
const performSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Ensure filename has .txt extension
|
||||
const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
|
||||
|
||||
const response = await authenticatedFetch(`/api/taskmaster/prd/${encodeURIComponent(project?.name)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
fileName: fullFileName,
|
||||
content
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Save failed: ${response.status}`);
|
||||
}
|
||||
|
||||
// Show success feedback
|
||||
setSaveSuccess(true);
|
||||
setTimeout(() => setSaveSuccess(false), 2000);
|
||||
|
||||
// Update existing PRDs list
|
||||
const response2 = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
|
||||
if (response2.ok) {
|
||||
const data = await response2.json();
|
||||
setExistingPRDs(data.prds || []);
|
||||
}
|
||||
|
||||
// Call the onSave callback if provided (for UI updates)
|
||||
if (onSave) {
|
||||
await onSave();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving PRD:', error);
|
||||
alert(`Error saving PRD: ${error.message}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([content], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const downloadFileName = fileName ? `${fileName}.txt` : 'prd.txt';
|
||||
a.download = downloadFileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleGenerateTasks = async () => {
|
||||
if (!content.trim()) {
|
||||
alert('Please add content to the PRD before generating tasks.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show AI-first modal instead of simple confirm
|
||||
setShowGenerateModal(true);
|
||||
};
|
||||
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
setIsFullscreen(!isFullscreen);
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 's') {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [content]);
|
||||
|
||||
// Simple markdown to HTML converter for preview
|
||||
const renderMarkdown = (markdown) => {
|
||||
return markdown
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
|
||||
.replace(/\*(.*)\*/gim, '<em>$1</em>')
|
||||
.replace(/^\- (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/gims, '<ul>$1</ul>')
|
||||
.replace(/\n\n/gim, '</p><p>')
|
||||
.replace(/^(?!<[h|u|l])(.*$)/gim, '<p>$1</p>')
|
||||
.replace(/<\/ul>\s*<ul>/gim, '');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] md:bg-black/50 md:flex md:items-center md:justify-center">
|
||||
<div className="w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center bg-white dark:bg-gray-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading PRD...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed inset-0 z-[200] ${
|
||||
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
|
||||
} ${isFullscreen ? 'md:p-0' : ''}`}>
|
||||
<div className={cn(
|
||||
'bg-white dark:bg-gray-900 shadow-2xl flex flex-col',
|
||||
'w-full h-full md:rounded-lg md:shadow-2xl',
|
||||
isFullscreen
|
||||
? 'md:w-full md:h-full md:rounded-none'
|
||||
: 'md:w-full md:max-w-6xl md:h-[85vh] md:max-h-[85vh]'
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 min-w-0">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="w-8 h-8 bg-purple-600 rounded flex items-center justify-center flex-shrink-0">
|
||||
<FileText className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Mobile: Stack filename and tags vertically for more space */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 min-w-0">
|
||||
{/* Filename input row - full width on mobile */}
|
||||
<div className="flex items-center gap-1 min-w-0 flex-1">
|
||||
<div className="flex items-center min-w-0 flex-1 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-md px-3 py-2 focus-within:ring-2 focus-within:ring-purple-500 focus-within:border-purple-500 dark:focus-within:ring-purple-400 dark:focus-within:border-purple-400">
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
onChange={(e) => {
|
||||
// Remove invalid filename characters
|
||||
const sanitizedValue = e.target.value.replace(/[<>:"/\\|?*]/g, '');
|
||||
setFileName(sanitizedValue);
|
||||
}}
|
||||
className="font-medium text-gray-900 dark:text-white bg-transparent border-none outline-none min-w-0 flex-1 text-base sm:text-sm placeholder-gray-400 dark:placeholder-gray-500"
|
||||
placeholder="Enter PRD filename"
|
||||
maxLength={100}
|
||||
/>
|
||||
<span className="text-sm sm:text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ml-1">.txt</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => document.querySelector('input[placeholder="Enter PRD filename"]')?.focus()}
|
||||
className="p-1 text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
|
||||
title="Click to edit filename"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tags row - moves to second line on mobile for more filename space */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className="text-xs bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-300 px-2 py-1 rounded whitespace-nowrap">
|
||||
📋 PRD
|
||||
</span>
|
||||
{isNewFile && (
|
||||
<span className="text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-300 px-2 py-1 rounded whitespace-nowrap">
|
||||
✨ New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description - smaller on mobile */}
|
||||
<p className="text-xs sm:text-sm text-gray-500 dark:text-gray-400 truncate mt-1">
|
||||
Product Requirements Document
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setPreviewMode(!previewMode)}
|
||||
className={cn(
|
||||
'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
|
||||
previewMode
|
||||
? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
)}
|
||||
title={previewMode ? 'Switch to edit mode' : 'Preview markdown'}
|
||||
>
|
||||
<Eye className="w-5 h-5 md:w-4 md:h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setWordWrap(!wordWrap)}
|
||||
className={cn(
|
||||
'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
|
||||
wordWrap
|
||||
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
)}
|
||||
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
||||
>
|
||||
<span className="text-sm md:text-xs font-mono font-bold">↵</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<span className="text-lg md:text-base">{isDarkMode ? '☀️' : '🌙'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Download PRD"
|
||||
>
|
||||
<Download className="w-5 h-5 md:w-4 md:h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleGenerateTasks}
|
||||
disabled={!content.trim()}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors text-sm font-medium',
|
||||
'bg-purple-600 hover:bg-purple-700 text-white',
|
||||
'min-h-[44px] md:min-h-0'
|
||||
)}
|
||||
title="Generate tasks from PRD content"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="hidden md:inline">Generate Tasks</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
'px-3 py-2 text-white rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors',
|
||||
'min-h-[44px] md:min-h-0',
|
||||
saveSuccess
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-purple-600 hover:bg-purple-700'
|
||||
)}
|
||||
>
|
||||
{saveSuccess ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Saved!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-5 h-5 md:w-4 md:h-4" />
|
||||
<span className="hidden sm:inline">{saving ? 'Saving...' : 'Save PRD'}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-6 h-6 md:w-4 md:h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor/Preview Content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{previewMode ? (
|
||||
<div className="h-full overflow-y-auto p-6 prose prose-gray dark:prose-invert max-w-none">
|
||||
<div
|
||||
className="markdown-preview"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
extensions={[
|
||||
markdown(),
|
||||
...(wordWrap ? [EditorView.lineWrapping] : [])
|
||||
]}
|
||||
theme={isDarkMode ? oneDark : undefined}
|
||||
height="100%"
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
height: '100%',
|
||||
}}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Lines: {content.split('\n').length}</span>
|
||||
<span>Characters: {content.length}</span>
|
||||
<span>Words: {content.split(/\s+/).filter(word => word.length > 0).length}</span>
|
||||
<span>Format: Markdown</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Press Ctrl+S to save • Esc to close
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Tasks Modal */}
|
||||
{showGenerateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-200 dark:border-gray-700">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center">
|
||||
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Generate Tasks from PRD</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowGenerateModal(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* AI-First Approach */}
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4 border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">
|
||||
💡 Pro Tip: Ask Claude Code Directly!
|
||||
</h4>
|
||||
<p className="text-sm text-purple-800 dark:text-purple-200 mb-3">
|
||||
You can simply ask Claude Code in the chat to parse your PRD and generate tasks.
|
||||
The AI assistant will automatically save your PRD and create detailed tasks with implementation details.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded border border-purple-200 dark:border-purple-700 p-3 mb-3">
|
||||
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">💬 Example:</p>
|
||||
<p className="text-xs text-gray-900 dark:text-white font-mono">
|
||||
"I've just initialized a new project with Claude Task Master. I have a PRD at .taskmaster/docs/{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}. Can you help me parse it and set up the initial tasks?"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-purple-700 dark:text-purple-300">
|
||||
<strong>This will:</strong> Save your PRD, analyze its content, and generate structured tasks with subtasks, dependencies, and implementation details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Learn More Link */}
|
||||
<div className="text-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
For more examples and advanced usage patterns:
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master/blob/main/docs/examples.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 underline font-medium"
|
||||
>
|
||||
View TaskMaster Documentation →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={() => setShowGenerateModal(false)}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Got it, I'll ask Claude Code directly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overwrite Confirmation Modal */}
|
||||
{showOverwriteConfirm && (
|
||||
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowOverwriteConfirm(false)} />
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="p-2 rounded-full mr-3 bg-yellow-100 dark:bg-yellow-900">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
File Already Exists
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
A PRD file named "{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}" already exists.
|
||||
Do you want to overwrite it with the current content?
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowOverwriteConfirm(false)}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setShowOverwriteConfirm(false);
|
||||
await performSave();
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-white bg-yellow-600 hover:bg-yellow-700 rounded-md flex items-center space-x-2 transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
<span>Overwrite</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PRDEditor;
|
||||
@@ -29,7 +29,7 @@ if (typeof document !== 'undefined') {
|
||||
// Global store for shell sessions to persist across tab switches
|
||||
const shellSessions = new Map();
|
||||
|
||||
function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
function Shell({ selectedProject, selectedSession, isActive, initialCommand, isPlainShell = false, onProcessComplete }) {
|
||||
const terminalRef = useRef(null);
|
||||
const terminal = useRef(null);
|
||||
const fitAddon = useRef(null);
|
||||
@@ -434,13 +434,17 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
const initPayload = {
|
||||
type: 'init',
|
||||
projectPath: selectedProject.fullPath || selectedProject.path,
|
||||
sessionId: selectedSession?.id,
|
||||
hasSession: !!selectedSession,
|
||||
provider: selectedSession?.__provider || 'claude',
|
||||
sessionId: isPlainShell ? null : selectedSession?.id,
|
||||
hasSession: isPlainShell ? false : !!selectedSession,
|
||||
provider: isPlainShell ? 'plain-shell' : (selectedSession?.__provider || 'claude'),
|
||||
cols: terminal.current.cols,
|
||||
rows: terminal.current.rows
|
||||
rows: terminal.current.rows,
|
||||
initialCommand: initialCommand,
|
||||
isPlainShell: isPlainShell
|
||||
};
|
||||
|
||||
console.log('Shell init payload:', initPayload);
|
||||
|
||||
ws.current.send(JSON.stringify(initPayload));
|
||||
|
||||
// Also send resize message immediately after init
|
||||
@@ -473,6 +477,18 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
urls.push(match[1]);
|
||||
}
|
||||
|
||||
if (isPlainShell && onProcessComplete) {
|
||||
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI codes
|
||||
if (cleanOutput.includes('Process exited with code 0')) {
|
||||
onProcessComplete(0); // Success
|
||||
} else if (cleanOutput.match(/Process exited with code (\d+)/)) {
|
||||
const exitCode = parseInt(cleanOutput.match(/Process exited with code (\d+)/)[1]);
|
||||
if (exitCode !== 0) {
|
||||
onProcessComplete(exitCode); // Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If URLs found, log them for potential opening
|
||||
|
||||
terminal.current.write(output);
|
||||
@@ -606,14 +622,16 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
<span>Continue in Shell</span>
|
||||
</button>
|
||||
<p className="text-gray-400 text-sm mt-3 px-2">
|
||||
{selectedSession ?
|
||||
(() => {
|
||||
const displaySessionName = selectedSession.__provider === 'cursor'
|
||||
? (selectedSession.name || 'Untitled Session')
|
||||
: (selectedSession.summary || 'New Session');
|
||||
return `Resume session: ${displaySessionName.slice(0, 50)}...`;
|
||||
})() :
|
||||
'Start a new Claude session'
|
||||
{isPlainShell ?
|
||||
`Run ${initialCommand || 'command'} in ${selectedProject.displayName}` :
|
||||
selectedSession ?
|
||||
(() => {
|
||||
const displaySessionName = selectedSession.__provider === 'cursor'
|
||||
? (selectedSession.name || 'Untitled Session')
|
||||
: (selectedSession.summary || 'New Session');
|
||||
return `Resume session: ${displaySessionName.slice(0, 50)}...`;
|
||||
})() :
|
||||
'Start a new Claude session'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
@@ -629,7 +647,10 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
<span className="text-base font-medium">Connecting to shell...</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mt-3 px-2">
|
||||
Starting Claude CLI in {selectedProject.displayName}
|
||||
{isPlainShell ?
|
||||
`Running ${initialCommand || 'command'} in ${selectedProject.displayName}` :
|
||||
`Starting Claude CLI in ${selectedProject.displayName}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,10 @@ import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRig
|
||||
import { cn } from '../lib/utils';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo.jsx';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
import { api } from '../utils/api';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
// Move formatTimeAgo outside component to avoid recreation on every render
|
||||
const formatTimeAgo = (dateString, currentTime) => {
|
||||
@@ -70,6 +73,10 @@ function Sidebar({
|
||||
const [generatingSummary, setGeneratingSummary] = useState({});
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
// TaskMaster context
|
||||
const { setCurrentProject, mcpServerStatus } = useTaskMaster();
|
||||
const { tasksEnabled } = useTasksSettings();
|
||||
|
||||
|
||||
// Starred projects state - persisted in localStorage
|
||||
const [starredProjects, setStarredProjects] = useState(() => {
|
||||
@@ -417,6 +424,15 @@ function Sidebar({
|
||||
return displayName.includes(searchLower) || projectName.includes(searchLower);
|
||||
});
|
||||
|
||||
// Enhanced project selection that updates both the main UI and TaskMaster context
|
||||
const handleProjectSelect = (project) => {
|
||||
// Call the original project select handler
|
||||
onProjectSelect(project);
|
||||
|
||||
// Update TaskMaster context with the selected project
|
||||
setCurrentProject(project);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-card md:select-none">
|
||||
{/* Header */}
|
||||
@@ -500,6 +516,7 @@ function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* New Project Form */}
|
||||
{showNewProject && (
|
||||
<div className="md:p-3 md:border-b md:border-border md:bg-muted/30">
|
||||
@@ -717,9 +734,25 @@ function Sidebar({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-sm font-medium text-foreground truncate">
|
||||
{project.displayName}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between min-w-0 flex-1">
|
||||
<h3 className="text-sm font-medium text-foreground truncate">
|
||||
{project.displayName}
|
||||
</h3>
|
||||
{tasksEnabled && (
|
||||
<TaskIndicator
|
||||
status={(() => {
|
||||
const projectConfigured = project.taskmaster?.hasTaskmaster;
|
||||
const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
|
||||
if (projectConfigured && mcpConfigured) return 'fully-configured';
|
||||
if (projectConfigured) return 'taskmaster-only';
|
||||
if (mcpConfigured) return 'mcp-only';
|
||||
return 'not-configured';
|
||||
})()}
|
||||
size="xs"
|
||||
className="flex-shrink-0 ml-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(() => {
|
||||
const sessionCount = getAllSessions(project).length;
|
||||
@@ -825,13 +858,13 @@ function Sidebar({
|
||||
onClick={() => {
|
||||
// Desktop behavior: select project and toggle
|
||||
if (selectedProject?.name !== project.name) {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
}
|
||||
toggleProject(project.name);
|
||||
}}
|
||||
onTouchEnd={handleTouchClick(() => {
|
||||
if (selectedProject?.name !== project.name) {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
}
|
||||
toggleProject(project.name);
|
||||
})}
|
||||
@@ -1013,11 +1046,11 @@ function Sidebar({
|
||||
isActive ? "border-green-500/30 bg-green-50/5 dark:bg-green-900/5" : "border-border/30"
|
||||
)}
|
||||
onClick={() => {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
onSessionSelect(session);
|
||||
}}
|
||||
onTouchEnd={handleTouchClick(() => {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
onSessionSelect(session);
|
||||
})}
|
||||
>
|
||||
@@ -1239,7 +1272,7 @@ function Sidebar({
|
||||
<button
|
||||
className="w-full h-8 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md flex items-center justify-center gap-2 font-medium text-xs active:scale-[0.98] transition-all duration-150"
|
||||
onClick={() => {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
onNewSession(project);
|
||||
}}
|
||||
>
|
||||
|
||||
210
src/components/TaskCard.jsx
Normal file
210
src/components/TaskCard.jsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React from 'react';
|
||||
import { Clock, CheckCircle, Circle, AlertCircle, Pause, X, ArrowRight, ChevronUp, Minus, Flag } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const TaskCard = ({
|
||||
task,
|
||||
onClick,
|
||||
showParent = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const getStatusConfig = (status) => {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
bgColor: 'bg-green-50 dark:bg-green-950',
|
||||
borderColor: 'border-green-200 dark:border-green-800',
|
||||
iconColor: 'text-green-600 dark:text-green-400',
|
||||
textColor: 'text-green-900 dark:text-green-100',
|
||||
statusText: 'Done'
|
||||
};
|
||||
|
||||
case 'in-progress':
|
||||
return {
|
||||
icon: Clock,
|
||||
bgColor: 'bg-blue-50 dark:bg-blue-950',
|
||||
borderColor: 'border-blue-200 dark:border-blue-800',
|
||||
iconColor: 'text-blue-600 dark:text-blue-400',
|
||||
textColor: 'text-blue-900 dark:text-blue-100',
|
||||
statusText: 'In Progress'
|
||||
};
|
||||
|
||||
case 'review':
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
bgColor: 'bg-amber-50 dark:bg-amber-950',
|
||||
borderColor: 'border-amber-200 dark:border-amber-800',
|
||||
iconColor: 'text-amber-600 dark:text-amber-400',
|
||||
textColor: 'text-amber-900 dark:text-amber-100',
|
||||
statusText: 'Review'
|
||||
};
|
||||
|
||||
case 'deferred':
|
||||
return {
|
||||
icon: Pause,
|
||||
bgColor: 'bg-gray-50 dark:bg-gray-800',
|
||||
borderColor: 'border-gray-200 dark:border-gray-700',
|
||||
iconColor: 'text-gray-500 dark:text-gray-400',
|
||||
textColor: 'text-gray-700 dark:text-gray-300',
|
||||
statusText: 'Deferred'
|
||||
};
|
||||
|
||||
case 'cancelled':
|
||||
return {
|
||||
icon: X,
|
||||
bgColor: 'bg-red-50 dark:bg-red-950',
|
||||
borderColor: 'border-red-200 dark:border-red-800',
|
||||
iconColor: 'text-red-600 dark:text-red-400',
|
||||
textColor: 'text-red-900 dark:text-red-100',
|
||||
statusText: 'Cancelled'
|
||||
};
|
||||
|
||||
case 'pending':
|
||||
default:
|
||||
return {
|
||||
icon: Circle,
|
||||
bgColor: 'bg-slate-50 dark:bg-slate-800',
|
||||
borderColor: 'border-slate-200 dark:border-slate-700',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400',
|
||||
textColor: 'text-slate-900 dark:text-slate-100',
|
||||
statusText: 'Pending'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig(task.status);
|
||||
const Icon = config.icon;
|
||||
|
||||
const getPriorityIcon = (priority) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return (
|
||||
<Tooltip content="High Priority">
|
||||
<div className="w-4 h-4 bg-red-100 dark:bg-red-900/30 rounded flex items-center justify-center">
|
||||
<ChevronUp className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'medium':
|
||||
return (
|
||||
<Tooltip content="Medium Priority">
|
||||
<div className="w-4 h-4 bg-amber-100 dark:bg-amber-900/30 rounded flex items-center justify-center">
|
||||
<Minus className="w-2.5 h-2.5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'low':
|
||||
return (
|
||||
<Tooltip content="Low Priority">
|
||||
<div className="w-4 h-4 bg-blue-100 dark:bg-blue-900/30 rounded flex items-center justify-center">
|
||||
<Circle className="w-1.5 h-1.5 text-blue-600 dark:text-blue-400 fill-current" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip content="No Priority Set">
|
||||
<div className="w-4 h-4 bg-gray-100 dark:bg-gray-800 rounded flex items-center justify-center">
|
||||
<Circle className="w-1.5 h-1.5 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700',
|
||||
'hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-200 cursor-pointer',
|
||||
'p-3 space-y-3',
|
||||
onClick && 'hover:-translate-y-0.5',
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Header with Task ID, Title, and Priority */}
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
{/* Task ID and Title */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Tooltip content={`Task ID: ${task.id}`}>
|
||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">
|
||||
{task.id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<h3 className="font-medium text-sm text-gray-900 dark:text-white line-clamp-2 leading-tight">
|
||||
{task.title}
|
||||
</h3>
|
||||
{showParent && task.parentId && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
Task {task.parentId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority Icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{getPriorityIcon(task.priority)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with Dependencies and Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Dependencies */}
|
||||
<div className="flex items-center">
|
||||
{task.dependencies && Array.isArray(task.dependencies) && task.dependencies.length > 0 && (
|
||||
<Tooltip content={`Depends on: ${task.dependencies.map(dep => `Task ${dep}`).join(', ')}`}>
|
||||
<div className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
<span>Depends on: {task.dependencies.join(', ')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<Tooltip content={`Status: ${config.statusText}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className={cn('w-2 h-2 rounded-full', config.iconColor.replace('text-', 'bg-'))} />
|
||||
<span className={cn('text-xs font-medium', config.textColor)}>
|
||||
{config.statusText}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Subtask Progress (if applicable) */}
|
||||
{task.subtasks && task.subtasks.length > 0 && (
|
||||
<div className="ml-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Progress:</span>
|
||||
<Tooltip content={`${task.subtasks.filter(st => st.status === 'done').length} of ${task.subtasks.length} subtasks completed`}>
|
||||
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-300',
|
||||
task.status === 'done' ? 'bg-green-500' : 'bg-blue-500'
|
||||
)}
|
||||
style={{
|
||||
width: `${Math.round((task.subtasks.filter(st => st.status === 'done').length / task.subtasks.length) * 100)}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip content={`${task.subtasks.filter(st => st.status === 'done').length} completed, ${task.subtasks.filter(st => st.status === 'pending').length} pending, ${task.subtasks.filter(st => st.status === 'in-progress').length} in progress`}>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{task.subtasks.filter(st => st.status === 'done').length}/{task.subtasks.length}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCard;
|
||||
406
src/components/TaskDetail.jsx
Normal file
406
src/components/TaskDetail.jsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Flag, User, ArrowRight, CheckCircle, Circle, AlertCircle, Pause, Edit, Save, Copy, ChevronDown, ChevronRight, Clock } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
import { api } from '../utils/api';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
|
||||
const TaskDetail = ({
|
||||
task,
|
||||
onClose,
|
||||
onEdit,
|
||||
onStatusChange,
|
||||
onTaskClick,
|
||||
isOpen = true,
|
||||
className = ''
|
||||
}) => {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editedTask, setEditedTask] = useState(task || {});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [showTestStrategy, setShowTestStrategy] = useState(false);
|
||||
const { currentProject, refreshTasks } = useTaskMaster();
|
||||
|
||||
if (!isOpen || !task) return null;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// Only include changed fields
|
||||
const updates = {};
|
||||
if (editedTask.title !== task.title) updates.title = editedTask.title;
|
||||
if (editedTask.description !== task.description) updates.description = editedTask.description;
|
||||
if (editedTask.details !== task.details) updates.details = editedTask.details;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const response = await api.taskmaster.updateTask(currentProject.name, task.id, updates);
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh tasks to get updated data
|
||||
refreshTasks?.();
|
||||
onEdit?.(editedTask);
|
||||
setEditMode(false);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to update task:', error);
|
||||
alert(`Failed to update task: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
setEditMode(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
alert('Error updating task. Please try again.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
try {
|
||||
const response = await api.taskmaster.updateTask(currentProject.name, task.id, { status: newStatus });
|
||||
|
||||
if (response.ok) {
|
||||
refreshTasks?.();
|
||||
onStatusChange?.(task.id, newStatus);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to update task status:', error);
|
||||
alert(`Failed to update task status: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task status:', error);
|
||||
alert('Error updating task status. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const copyTaskId = () => {
|
||||
navigator.clipboard.writeText(task.id.toString());
|
||||
};
|
||||
|
||||
const getStatusConfig = (status) => {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return { icon: CheckCircle, color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-950' };
|
||||
case 'in-progress':
|
||||
return { icon: Clock, color: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-950' };
|
||||
case 'review':
|
||||
return { icon: AlertCircle, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-950' };
|
||||
case 'deferred':
|
||||
return { icon: Pause, color: 'text-gray-500 dark:text-gray-400', bg: 'bg-gray-50 dark:bg-gray-800' };
|
||||
case 'cancelled':
|
||||
return { icon: X, color: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-950' };
|
||||
default:
|
||||
return { icon: Circle, color: 'text-slate-500 dark:text-slate-400', bg: 'bg-slate-50 dark:bg-slate-800' };
|
||||
}
|
||||
};
|
||||
|
||||
const statusConfig = getStatusConfig(task.status);
|
||||
const StatusIcon = statusConfig.icon;
|
||||
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950';
|
||||
case 'medium': return 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-950';
|
||||
case 'low': return 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950';
|
||||
default: return 'text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'in-progress', label: 'In Progress' },
|
||||
{ value: 'review', label: 'Review' },
|
||||
{ value: 'done', label: 'Done' },
|
||||
{ value: 'deferred', label: 'Deferred' },
|
||||
{ value: 'cancelled', label: 'Cancelled' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-black/50">
|
||||
<div className={cn(
|
||||
'bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 md:rounded-lg shadow-xl',
|
||||
'w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col',
|
||||
className
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<StatusIcon className={cn('w-6 h-6', statusConfig.color)} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<button
|
||||
onClick={copyTaskId}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
title="Click to copy task ID"
|
||||
>
|
||||
<span>Task {task.id}</span>
|
||||
<Copy className="w-3 h-3" />
|
||||
</button>
|
||||
{task.parentId && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Subtask of Task {task.parentId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{editMode ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editedTask.title || ''}
|
||||
onChange={(e) => setEditedTask({ ...editedTask, title: e.target.value })}
|
||||
className="w-full text-lg font-semibold bg-transparent border-b-2 border-blue-500 focus:outline-none text-gray-900 dark:text-white"
|
||||
placeholder="Task title"
|
||||
/>
|
||||
) : (
|
||||
<h1 className="text-lg md:text-xl font-semibold text-gray-900 dark:text-white line-clamp-2">
|
||||
{task.title}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{editMode ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="p-2 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title={isSaving ? "Saving..." : "Save changes"}
|
||||
>
|
||||
<Save className={cn("w-5 h-5", isSaving && "animate-spin")} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditMode(false);
|
||||
setEditedTask(task);
|
||||
}}
|
||||
disabled={isSaving}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Cancel editing"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditMode(true)}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
title="Edit task"
|
||||
>
|
||||
<Edit className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 min-h-0">
|
||||
{/* Status and Metadata Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
|
||||
<div className={cn(
|
||||
'w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600',
|
||||
statusConfig.bg,
|
||||
statusConfig.color
|
||||
)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon className="w-4 h-4" />
|
||||
<span className="font-medium capitalize">
|
||||
{statusOptions.find(option => option.value === task.status)?.label || task.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Priority</label>
|
||||
<div className={cn(
|
||||
'px-3 py-2 rounded-md text-sm font-medium capitalize',
|
||||
getPriorityColor(task.priority)
|
||||
)}>
|
||||
<Flag className="w-4 h-4 inline mr-2" />
|
||||
{task.priority || 'Not set'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dependencies */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Dependencies</label>
|
||||
{task.dependencies && task.dependencies.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{task.dependencies.map(depId => (
|
||||
<button
|
||||
key={depId}
|
||||
onClick={() => onTaskClick && onTaskClick({ id: depId })}
|
||||
className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded text-sm hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer disabled:cursor-default disabled:opacity-50"
|
||||
disabled={!onTaskClick}
|
||||
title={onTaskClick ? `Click to view Task ${depId}` : `Task ${depId}`}
|
||||
>
|
||||
<ArrowRight className="w-3 h-3 inline mr-1" />
|
||||
{depId}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400 text-sm">No dependencies</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={editedTask.description || ''}
|
||||
onChange={(e) => setEditedTask({ ...editedTask, description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Task description"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{task.description || 'No description provided'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Implementation Details */}
|
||||
{task.details && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Implementation Details
|
||||
</span>
|
||||
{showDetails ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
{showDetails && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={editedTask.details || ''}
|
||||
onChange={(e) => setEditedTask({ ...editedTask, details: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Implementation details"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-md p-4">
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{task.details}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test Strategy */}
|
||||
{task.testStrategy && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<button
|
||||
onClick={() => setShowTestStrategy(!showTestStrategy)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Test Strategy
|
||||
</span>
|
||||
{showTestStrategy ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
{showTestStrategy && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-950 rounded-md p-4">
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{task.testStrategy}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subtasks */}
|
||||
{task.subtasks && task.subtasks.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Subtasks ({task.subtasks.length})
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{task.subtasks.map(subtask => {
|
||||
const subtaskConfig = getStatusConfig(subtask.status);
|
||||
const SubtaskIcon = subtaskConfig.icon;
|
||||
return (
|
||||
<div key={subtask.id} className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
|
||||
<SubtaskIcon className={cn('w-4 h-4', subtaskConfig.color)} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{subtask.title}
|
||||
</h4>
|
||||
{subtask.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 truncate">
|
||||
{subtask.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{subtask.id}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Task ID: {task.id}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetail;
|
||||
108
src/components/TaskIndicator.jsx
Normal file
108
src/components/TaskIndicator.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { CheckCircle, Settings, X, AlertCircle } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
/**
|
||||
* TaskIndicator Component
|
||||
*
|
||||
* Displays TaskMaster status for projects in the sidebar with appropriate
|
||||
* icons and colors based on the project's TaskMaster configuration state.
|
||||
*/
|
||||
const TaskIndicator = ({
|
||||
status = 'not-configured',
|
||||
size = 'sm',
|
||||
className = '',
|
||||
showLabel = false
|
||||
}) => {
|
||||
const getIndicatorConfig = () => {
|
||||
switch (status) {
|
||||
case 'fully-configured':
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
color: 'text-green-500 dark:text-green-400',
|
||||
bgColor: 'bg-green-50 dark:bg-green-950',
|
||||
label: 'TaskMaster Ready',
|
||||
title: 'TaskMaster fully configured with MCP server'
|
||||
};
|
||||
|
||||
case 'taskmaster-only':
|
||||
return {
|
||||
icon: Settings,
|
||||
color: 'text-blue-500 dark:text-blue-400',
|
||||
bgColor: 'bg-blue-50 dark:bg-blue-950',
|
||||
label: 'TaskMaster Init',
|
||||
title: 'TaskMaster initialized, MCP server needs setup'
|
||||
};
|
||||
|
||||
case 'mcp-only':
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
color: 'text-amber-500 dark:text-amber-400',
|
||||
bgColor: 'bg-amber-50 dark:bg-amber-950',
|
||||
label: 'MCP Ready',
|
||||
title: 'MCP server configured, TaskMaster needs initialization'
|
||||
};
|
||||
|
||||
case 'not-configured':
|
||||
case 'error':
|
||||
default:
|
||||
return {
|
||||
icon: X,
|
||||
color: 'text-gray-400 dark:text-gray-500',
|
||||
bgColor: 'bg-gray-50 dark:bg-gray-900',
|
||||
label: 'No TaskMaster',
|
||||
title: 'TaskMaster not configured'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getIndicatorConfig();
|
||||
const Icon = config.icon;
|
||||
|
||||
const sizeClasses = {
|
||||
xs: 'w-3 h-3',
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6'
|
||||
};
|
||||
|
||||
const paddingClasses = {
|
||||
xs: 'p-0.5',
|
||||
sm: 'p-1',
|
||||
md: 'p-1.5',
|
||||
lg: 'p-2'
|
||||
};
|
||||
|
||||
if (showLabel) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 text-xs rounded-md px-2 py-1 transition-colors',
|
||||
config.bgColor,
|
||||
config.color,
|
||||
className
|
||||
)}
|
||||
title={config.title}
|
||||
>
|
||||
<Icon className={sizeClasses[size]} />
|
||||
<span className="font-medium">{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-full transition-colors',
|
||||
config.bgColor,
|
||||
paddingClasses[size],
|
||||
className
|
||||
)}
|
||||
title={config.title}
|
||||
>
|
||||
<Icon className={cn(sizeClasses[size], config.color)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskIndicator;
|
||||
1054
src/components/TaskList.jsx
Normal file
1054
src/components/TaskList.jsx
Normal file
File diff suppressed because it is too large
Load Diff
603
src/components/TaskMasterSetupWizard.jsx
Normal file
603
src/components/TaskMasterSetupWizard.jsx
Normal file
@@ -0,0 +1,603 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, ChevronRight, ChevronLeft, CheckCircle, AlertCircle, Settings, Server, FileText, Sparkles, ExternalLink, Copy } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
const TaskMasterSetupWizard = ({
|
||||
isOpen = true,
|
||||
onClose,
|
||||
onComplete,
|
||||
currentProject,
|
||||
className = ''
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [setupData, setSetupData] = useState({
|
||||
projectRoot: '',
|
||||
initGit: true,
|
||||
storeTasksInGit: true,
|
||||
addAliases: true,
|
||||
skipInstall: false,
|
||||
rules: ['claude'],
|
||||
mcpConfigured: false,
|
||||
prdContent: ''
|
||||
});
|
||||
|
||||
const totalSteps = 4;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProject) {
|
||||
setSetupData(prev => ({
|
||||
...prev,
|
||||
projectRoot: currentProject.path || ''
|
||||
}));
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Project Configuration',
|
||||
description: 'Configure basic TaskMaster settings for your project'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'MCP Server Setup',
|
||||
description: 'Ensure TaskMaster MCP server is properly configured'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'PRD Creation',
|
||||
description: 'Create or import a Product Requirements Document'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Complete Setup',
|
||||
description: 'Initialize TaskMaster and generate initial tasks'
|
||||
}
|
||||
];
|
||||
|
||||
const handleNext = async () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (currentStep === 1) {
|
||||
// Validate project configuration
|
||||
if (!setupData.projectRoot) {
|
||||
setError('Project root path is required');
|
||||
return;
|
||||
}
|
||||
setCurrentStep(2);
|
||||
} else if (currentStep === 2) {
|
||||
// Check MCP server status
|
||||
setLoading(true);
|
||||
try {
|
||||
const mcpStatus = await api.get('/mcp-utils/taskmaster-server');
|
||||
setSetupData(prev => ({
|
||||
...prev,
|
||||
mcpConfigured: mcpStatus.hasMCPServer && mcpStatus.isConfigured
|
||||
}));
|
||||
setCurrentStep(3);
|
||||
} catch (err) {
|
||||
setError('Failed to check MCP server status. You can continue but some features may not work.');
|
||||
setCurrentStep(3);
|
||||
}
|
||||
} else if (currentStep === 3) {
|
||||
// Validate PRD step
|
||||
if (!setupData.prdContent.trim()) {
|
||||
setError('Please create or import a PRD to continue');
|
||||
return;
|
||||
}
|
||||
setCurrentStep(4);
|
||||
} else if (currentStep === 4) {
|
||||
// Complete setup
|
||||
await completeSetup();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message || 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const completeSetup = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Initialize TaskMaster project
|
||||
const initResponse = await api.post('/taskmaster/initialize', {
|
||||
projectRoot: setupData.projectRoot,
|
||||
initGit: setupData.initGit,
|
||||
storeTasksInGit: setupData.storeTasksInGit,
|
||||
addAliases: setupData.addAliases,
|
||||
skipInstall: setupData.skipInstall,
|
||||
rules: setupData.rules,
|
||||
yes: true
|
||||
});
|
||||
|
||||
if (!initResponse.ok) {
|
||||
throw new Error('Failed to initialize TaskMaster project');
|
||||
}
|
||||
|
||||
// Save PRD content if provided
|
||||
if (setupData.prdContent.trim()) {
|
||||
const prdResponse = await api.post('/taskmaster/save-prd', {
|
||||
projectRoot: setupData.projectRoot,
|
||||
content: setupData.prdContent
|
||||
});
|
||||
|
||||
if (!prdResponse.ok) {
|
||||
console.warn('Failed to save PRD content');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse PRD to generate initial tasks
|
||||
if (setupData.prdContent.trim()) {
|
||||
const parseResponse = await api.post('/taskmaster/parse-prd', {
|
||||
projectRoot: setupData.projectRoot,
|
||||
input: '.taskmaster/docs/prd.txt',
|
||||
numTasks: '10',
|
||||
research: false,
|
||||
force: false
|
||||
});
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
console.warn('Failed to parse PRD and generate tasks');
|
||||
}
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
onClose?.();
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to complete TaskMaster setup');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyMCPConfig = () => {
|
||||
const mcpConfig = `{
|
||||
"mcpServers": {
|
||||
"": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_anthropic_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_perplexity_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
navigator.clipboard.writeText(mcpConfig);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<Settings className="w-12 h-12 text-blue-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Project Configuration
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Configure TaskMaster settings for your project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Project Root Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={setupData.projectRoot}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, projectRoot: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="/path/to/your/project"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Options</h4>
|
||||
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.initGit}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, initGit: e.target.checked }))}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Initialize Git repository</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.storeTasksInGit}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, storeTasksInGit: e.target.checked }))}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Store tasks in Git</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.addAliases}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, addAliases: e.target.checked }))}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Add shell aliases (tm, taskmaster)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Rule Profiles
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{['claude', 'cursor', 'vscode', 'roo', 'cline', 'windsurf'].map(rule => (
|
||||
<label key={rule} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.rules.includes(rule)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSetupData(prev => ({ ...prev, rules: [...prev.rules, rule] }));
|
||||
} else {
|
||||
setSetupData(prev => ({ ...prev, rules: prev.rules.filter(r => r !== rule) }));
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300 capitalize">{rule}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<Server className="w-12 h-12 text-purple-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
MCP Server Setup
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
TaskMaster works best with the MCP server configured
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
MCP Server Configuration
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 mb-3">
|
||||
To enable full TaskMaster integration, add the MCP server configuration to your Claude settings.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded border p-3 mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-mono text-gray-600 dark:text-gray-400">.mcp.json</span>
|
||||
<button
|
||||
onClick={copyMCPConfig}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_anthropic_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_perplexity_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<a
|
||||
href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use/mcp-servers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
|
||||
>
|
||||
Learn about MCP setup
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Current Status</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
{setupData.mcpConfigured ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm text-green-700 dark:text-green-300">MCP server is configured</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="w-4 h-4 text-amber-500" />
|
||||
<span className="text-sm text-amber-700 dark:text-amber-300">MCP server not detected (optional)</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<FileText className="w-12 h-12 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Product Requirements Document
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Create or import a PRD to generate initial tasks
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
PRD Content
|
||||
</label>
|
||||
<textarea
|
||||
value={setupData.prdContent}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, prdContent: e.target.value }))}
|
||||
rows={12}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white font-mono text-sm"
|
||||
placeholder="# Product Requirements Document
|
||||
|
||||
## 1. Overview
|
||||
Describe your project or feature...
|
||||
|
||||
## 2. Objectives
|
||||
- Primary goal
|
||||
- Success metrics
|
||||
|
||||
## 3. User Stories
|
||||
- As a user, I want...
|
||||
|
||||
## 4. Requirements
|
||||
- Feature requirements
|
||||
- Technical requirements
|
||||
|
||||
## 5. Implementation Plan
|
||||
- Phase 1: Core features
|
||||
- Phase 2: Enhancements"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
AI Task Generation
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
TaskMaster will analyze your PRD and automatically generate a structured task list with dependencies, priorities, and implementation details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 4:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Complete Setup
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Ready to initialize TaskMaster for your project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<h4 className="font-medium text-green-900 dark:text-green-100 mb-3">
|
||||
Setup Summary
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm text-green-800 dark:text-green-200">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Project: {setupData.projectRoot}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Rules: {setupData.rules.join(', ')}
|
||||
</li>
|
||||
{setupData.mcpConfigured && (
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
MCP server configured
|
||||
</li>
|
||||
)}
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
PRD content ready ({setupData.prdContent.length} characters)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
What happens next?
|
||||
</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm text-blue-800 dark:text-blue-200">
|
||||
<li>Initialize TaskMaster project structure</li>
|
||||
<li>Save your PRD to <code>.taskmaster/docs/prd.txt</code></li>
|
||||
<li>Generate initial tasks from your PRD</li>
|
||||
<li>Set up project configuration and rules</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-black/50">
|
||||
<div className={cn(
|
||||
'bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 md:rounded-lg shadow-xl',
|
||||
'w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col',
|
||||
className
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sparkles className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
TaskMaster Setup Wizard
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Step {currentStep} of {totalSteps}: {steps[currentStep - 1]?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="px-4 md:px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<div className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors',
|
||||
currentStep > step.id
|
||||
? 'bg-green-500 text-white'
|
||||
: currentStep === step.id
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
)}>
|
||||
{currentStep > step.id ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
) : (
|
||||
step.id
|
||||
)}
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={cn(
|
||||
'w-16 h-1 mx-2 rounded',
|
||||
currentStep > step.id
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400">
|
||||
{steps.map(step => (
|
||||
<span key={step.id} className="text-center">
|
||||
{step.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
{renderStepContent()}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-red-900 dark:text-red-100 mb-1">Error</h4>
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
disabled={currentStep === 1}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{currentStep} of {totalSteps}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
{currentStep === totalSteps ? 'Setting up...' : 'Processing...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{currentStep === totalSteps ? 'Complete Setup' : 'Next'}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskMasterSetupWizard;
|
||||
86
src/components/TaskMasterStatus.jsx
Normal file
86
src/components/TaskMasterStatus.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
|
||||
const TaskMasterStatus = () => {
|
||||
const {
|
||||
currentProject,
|
||||
projectTaskMaster,
|
||||
mcpServerStatus,
|
||||
isLoading,
|
||||
isLoadingMCP,
|
||||
error
|
||||
} = useTaskMaster();
|
||||
|
||||
if (isLoading || isLoadingMCP) {
|
||||
return (
|
||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="animate-spin w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full mr-2"></div>
|
||||
Loading TaskMaster status...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center text-sm text-red-500 dark:text-red-400">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
|
||||
TaskMaster Error
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show MCP server status
|
||||
const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
|
||||
|
||||
// Show project TaskMaster status
|
||||
const projectConfigured = currentProject?.taskmaster?.hasTaskmaster;
|
||||
const taskCount = currentProject?.taskmaster?.metadata?.taskCount || 0;
|
||||
const completedCount = currentProject?.taskmaster?.metadata?.completed || 0;
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="w-2 h-2 bg-gray-400 rounded-full mr-2"></span>
|
||||
No project selected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine overall status for TaskIndicator
|
||||
let overallStatus = 'not-configured';
|
||||
if (projectConfigured && mcpConfigured) {
|
||||
overallStatus = 'fully-configured';
|
||||
} else if (projectConfigured) {
|
||||
overallStatus = 'taskmaster-only';
|
||||
} else if (mcpConfigured) {
|
||||
overallStatus = 'mcp-only';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* TaskMaster Status Indicator */}
|
||||
<TaskIndicator
|
||||
status={overallStatus}
|
||||
size="md"
|
||||
showLabel={true}
|
||||
/>
|
||||
|
||||
{/* Task Progress Info */}
|
||||
{projectConfigured && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">
|
||||
{completedCount}/{taskCount} tasks
|
||||
</span>
|
||||
{taskCount > 0 && (
|
||||
<span className="ml-2 opacity-75">
|
||||
({Math.round((completedCount / taskCount) * 100)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskMasterStatus;
|
||||
@@ -51,9 +51,9 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{todos.map((todo) => (
|
||||
{todos.map((todo, index) => (
|
||||
<div
|
||||
key={todo.id}
|
||||
key={todo.id || `todo-${index}`}
|
||||
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md dark:shadow-gray-900/50 transition-shadow"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
|
||||
@@ -4,9 +4,18 @@ import { Input } from './ui/input';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
function ToolsSettings({ isOpen, onClose, projects = [] }) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
const {
|
||||
tasksEnabled,
|
||||
setTasksEnabled,
|
||||
isTaskMasterInstalled,
|
||||
isTaskMasterReady,
|
||||
installationStatus,
|
||||
isCheckingInstallation
|
||||
} = useTasksSettings();
|
||||
const [allowedTools, setAllowedTools] = useState([]);
|
||||
const [disallowedTools, setDisallowedTools] = useState([]);
|
||||
const [newAllowedTool, setNewAllowedTool] = useState('');
|
||||
@@ -630,6 +639,16 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
|
||||
>
|
||||
Appearance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('tasks')}
|
||||
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'tasks'
|
||||
? 'border-blue-600 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
Tasks
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1674,6 +1693,160 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tasks Tab */}
|
||||
{activeTab === 'tasks' && (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
{/* Installation Status Check */}
|
||||
{isCheckingInstallation ? (
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
||||
<span className="text-sm text-muted-foreground">Checking TaskMaster installation...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* TaskMaster Not Installed Warning */}
|
||||
{!isTaskMasterInstalled && (
|
||||
<div className="bg-orange-50 dark:bg-orange-950/50 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-4 h-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100 mb-2">
|
||||
TaskMaster AI CLI Not Installed
|
||||
</div>
|
||||
<div className="text-sm text-orange-800 dark:text-orange-200 space-y-3">
|
||||
<p>TaskMaster CLI is required to use task management features. Install it to get started:</p>
|
||||
|
||||
<div className="bg-orange-100 dark:bg-orange-900/50 rounded-lg p-3 font-mono text-sm">
|
||||
<code>npm install -g task-master-ai</code>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
View on GitHub
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">After installation:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Restart this application</li>
|
||||
<li>TaskMaster features will automatically become available</li>
|
||||
<li>Use <code className="bg-orange-100 dark:bg-orange-800 px-1 rounded">task-master init</code> in your project directory</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TaskMaster Settings */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Enable TaskMaster Integration
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
Show TaskMaster tasks, banners, and sidebar indicators across the interface
|
||||
</div>
|
||||
{!isTaskMasterInstalled && (
|
||||
<div className="text-xs text-orange-600 dark:text-orange-400 mt-1">
|
||||
TaskMaster CLI must be installed first
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tasksEnabled}
|
||||
onChange={(e) => setTasksEnabled(e.target.checked)}
|
||||
disabled={!isTaskMasterInstalled}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className={`w-11 h-6 ${!isTaskMasterInstalled ? 'bg-gray-300 dark:bg-gray-600' : 'bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800'} rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600`}></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TaskMaster Information */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Zap className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
🎯 About TaskMaster AI
|
||||
</div>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
|
||||
<p><strong>AI-Powered Task Management:</strong> Break complex projects into manageable subtasks with AI assistance</p>
|
||||
<p><strong>PRD:</strong> Generate structured tasks from Product Requirements Documents</p>
|
||||
<p><strong>Dependency Tracking:</strong> Understand task relationships and execution order</p>
|
||||
<p><strong>Progress Visualization:</strong> Kanban boards, and detailed task views</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Link and Resources */}
|
||||
<div className="mt-4 pt-4 border-t border-blue-200 dark:border-blue-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
📚 Learn More & Tutorial
|
||||
</div>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
|
||||
<p>TaskMaster AI (aka <strong>claude-task-master</strong> ) is an advanced AI-powered task management system built for developers.</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
View on GitHub
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
Find documentation, setup guides, and examples for advanced TaskMaster workflows
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
91
src/components/Tooltip.jsx
Normal file
91
src/components/Tooltip.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
position = 'top',
|
||||
className = '',
|
||||
delay = 500
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [timeoutId, setTimeoutId] = useState(null);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const id = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, delay);
|
||||
setTimeoutId(id);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
setTimeoutId(null);
|
||||
}
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const getPositionClasses = () => {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
|
||||
case 'bottom':
|
||||
return 'top-full left-1/2 transform -translate-x-1/2 mt-2';
|
||||
case 'left':
|
||||
return 'right-full top-1/2 transform -translate-y-1/2 mr-2';
|
||||
case 'right':
|
||||
return 'left-full top-1/2 transform -translate-y-1/2 ml-2';
|
||||
default:
|
||||
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
|
||||
}
|
||||
};
|
||||
|
||||
const getArrowClasses = () => {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
|
||||
case 'bottom':
|
||||
return 'bottom-full left-1/2 transform -translate-x-1/2 border-b-gray-900 dark:border-b-gray-100';
|
||||
case 'left':
|
||||
return 'left-full top-1/2 transform -translate-y-1/2 border-l-gray-900 dark:border-l-gray-100';
|
||||
case 'right':
|
||||
return 'right-full top-1/2 transform -translate-y-1/2 border-r-gray-900 dark:border-r-gray-100';
|
||||
default:
|
||||
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
if (!content) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative inline-block"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{children}
|
||||
|
||||
{isVisible && (
|
||||
<div className={cn(
|
||||
'absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-900 dark:bg-gray-100 dark:text-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none',
|
||||
'animate-in fade-in-0 zoom-in-95 duration-200',
|
||||
getPositionClasses(),
|
||||
className
|
||||
)}>
|
||||
{content}
|
||||
|
||||
{/* Arrow */}
|
||||
<div className={cn(
|
||||
'absolute w-0 h-0 border-4 border-transparent',
|
||||
getArrowClasses()
|
||||
)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
324
src/contexts/TaskMasterContext.jsx
Normal file
324
src/contexts/TaskMasterContext.jsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { api } from '../utils/api';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useWebSocketContext } from './WebSocketContext';
|
||||
|
||||
const TaskMasterContext = createContext({
|
||||
// TaskMaster project state
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
projectTaskMaster: null,
|
||||
|
||||
// MCP server state
|
||||
mcpServerStatus: null,
|
||||
|
||||
// Tasks state
|
||||
tasks: [],
|
||||
nextTask: null,
|
||||
|
||||
// Loading states
|
||||
isLoading: false,
|
||||
isLoadingTasks: false,
|
||||
isLoadingMCP: false,
|
||||
|
||||
// Error state
|
||||
error: null,
|
||||
|
||||
// Actions
|
||||
refreshProjects: () => {},
|
||||
setCurrentProject: () => {},
|
||||
refreshTasks: () => {},
|
||||
refreshMCPStatus: () => {},
|
||||
clearError: () => {}
|
||||
});
|
||||
|
||||
export const useTaskMaster = () => {
|
||||
const context = useContext(TaskMasterContext);
|
||||
if (!context) {
|
||||
throw new Error('useTaskMaster must be used within a TaskMasterProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const TaskMasterProvider = ({ children }) => {
|
||||
// Get WebSocket messages from shared context to avoid duplicate connections
|
||||
const { messages } = useWebSocketContext();
|
||||
|
||||
// Authentication context
|
||||
const { user, token, isLoading: authLoading } = useAuth();
|
||||
|
||||
// State
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [currentProject, setCurrentProjectState] = useState(null);
|
||||
const [projectTaskMaster, setProjectTaskMaster] = useState(null);
|
||||
const [mcpServerStatus, setMCPServerStatus] = useState(null);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [nextTask, setNextTask] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingTasks, setIsLoadingTasks] = useState(false);
|
||||
const [isLoadingMCP, setIsLoadingMCP] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Helper to handle API errors
|
||||
const handleError = (error, context) => {
|
||||
console.error(`TaskMaster ${context} error:`, error);
|
||||
setError({
|
||||
message: error.message || `Failed to ${context}`,
|
||||
context,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
// Clear error state
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// This will be defined after the functions are declared
|
||||
|
||||
// Refresh projects with TaskMaster metadata
|
||||
const refreshProjects = useCallback(async () => {
|
||||
// Only make API calls if user is authenticated
|
||||
if (!user || !token) {
|
||||
setProjects([]);
|
||||
setCurrentProjectState(null); // This might be the problem!
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
clearError();
|
||||
const response = await api.get('/projects');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch projects: ${response.status}`);
|
||||
}
|
||||
|
||||
const projectsData = await response.json();
|
||||
|
||||
// Check if projectsData is an array
|
||||
if (!Array.isArray(projectsData)) {
|
||||
console.error('Projects API returned non-array data:', projectsData);
|
||||
setProjects([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter and enrich projects with TaskMaster data
|
||||
const enrichedProjects = projectsData.map(project => ({
|
||||
...project,
|
||||
taskMasterConfigured: project.taskmaster?.hasTaskmaster || false,
|
||||
taskMasterStatus: project.taskmaster?.status || 'not-configured',
|
||||
taskCount: project.taskmaster?.metadata?.taskCount || 0,
|
||||
completedCount: project.taskmaster?.metadata?.completed || 0
|
||||
}));
|
||||
|
||||
setProjects(enrichedProjects);
|
||||
|
||||
// If current project is set, update its TaskMaster data
|
||||
if (currentProject) {
|
||||
const updatedCurrent = enrichedProjects.find(p => p.name === currentProject.name);
|
||||
if (updatedCurrent) {
|
||||
setCurrentProjectState(updatedCurrent);
|
||||
setProjectTaskMaster(updatedCurrent.taskmaster);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err, 'load projects');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user, token]); // Remove currentProject dependency to avoid infinite loops
|
||||
|
||||
// Set current project and load its TaskMaster details
|
||||
const setCurrentProject = useCallback(async (project) => {
|
||||
try {
|
||||
setCurrentProjectState(project);
|
||||
|
||||
// Clear previous project's data immediately when switching projects
|
||||
setTasks([]);
|
||||
setNextTask(null);
|
||||
setProjectTaskMaster(null); // Clear previous TaskMaster data
|
||||
|
||||
// Try to fetch fresh TaskMaster detection data for the project
|
||||
if (project?.name) {
|
||||
try {
|
||||
const response = await api.get(`/taskmaster/detect/${encodeURIComponent(project.name)}`);
|
||||
if (response.ok) {
|
||||
const detectionData = await response.json();
|
||||
setProjectTaskMaster(detectionData.taskmaster);
|
||||
} else {
|
||||
// If individual detection fails, fall back to project data from /api/projects
|
||||
console.warn('Individual TaskMaster detection failed, using project data:', response.status);
|
||||
setProjectTaskMaster(project.taskmaster || null);
|
||||
}
|
||||
} catch (detectionError) {
|
||||
// If individual detection fails, fall back to project data from /api/projects
|
||||
console.warn('TaskMaster detection error, using project data:', detectionError.message);
|
||||
setProjectTaskMaster(project.taskmaster || null);
|
||||
}
|
||||
} else {
|
||||
setProjectTaskMaster(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in setCurrentProject:', err);
|
||||
handleError(err, 'set current project');
|
||||
// Fall back to project data if available
|
||||
setProjectTaskMaster(project?.taskmaster || null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Refresh MCP server status
|
||||
const refreshMCPStatus = useCallback(async () => {
|
||||
// Only make API calls if user is authenticated
|
||||
if (!user || !token) {
|
||||
setMCPServerStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoadingMCP(true);
|
||||
clearError();
|
||||
const mcpStatus = await api.get('/mcp-utils/taskmaster-server');
|
||||
setMCPServerStatus(mcpStatus);
|
||||
} catch (err) {
|
||||
handleError(err, 'check MCP server status');
|
||||
} finally {
|
||||
setIsLoadingMCP(false);
|
||||
}
|
||||
}, [user, token]);
|
||||
|
||||
// Refresh tasks for current project - load real TaskMaster data
|
||||
const refreshTasks = useCallback(async () => {
|
||||
if (!currentProject) {
|
||||
setTasks([]);
|
||||
setNextTask(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only make API calls if user is authenticated
|
||||
if (!user || !token) {
|
||||
setTasks([]);
|
||||
setNextTask(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoadingTasks(true);
|
||||
clearError();
|
||||
|
||||
// Load tasks from the TaskMaster API endpoint
|
||||
const response = await api.get(`/taskmaster/tasks/${encodeURIComponent(currentProject.name)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to load tasks');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
setTasks(data.tasks || []);
|
||||
|
||||
// Find next task (pending or in-progress)
|
||||
const nextTask = data.tasks?.find(task =>
|
||||
task.status === 'pending' || task.status === 'in-progress'
|
||||
) || null;
|
||||
setNextTask(nextTask);
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading tasks:', err);
|
||||
handleError(err, 'load tasks');
|
||||
// Set empty state on error
|
||||
setTasks([]);
|
||||
setNextTask(null);
|
||||
} finally {
|
||||
setIsLoadingTasks(false);
|
||||
}
|
||||
}, [currentProject, user, token]);
|
||||
|
||||
// Load initial data on mount or when auth changes
|
||||
useEffect(() => {
|
||||
if (!authLoading && user && token) {
|
||||
refreshProjects();
|
||||
refreshMCPStatus();
|
||||
} else {
|
||||
console.log('Auth not ready or no user, skipping project load:', { authLoading, user: !!user, token: !!token });
|
||||
}
|
||||
}, [refreshProjects, refreshMCPStatus, authLoading, user, token]);
|
||||
|
||||
// Clear errors when authentication changes
|
||||
useEffect(() => {
|
||||
if (user && token) {
|
||||
clearError();
|
||||
}
|
||||
}, [user, token, clearError]);
|
||||
|
||||
// Refresh tasks when current project changes
|
||||
useEffect(() => {
|
||||
if (currentProject?.name && user && token) {
|
||||
refreshTasks();
|
||||
}
|
||||
}, [currentProject?.name, user, token, refreshTasks]);
|
||||
|
||||
// Handle WebSocket messages for TaskMaster updates
|
||||
useEffect(() => {
|
||||
const latestMessage = messages[messages.length - 1];
|
||||
if (!latestMessage) return;
|
||||
|
||||
|
||||
switch (latestMessage.type) {
|
||||
case 'taskmaster-project-updated':
|
||||
// Refresh projects when TaskMaster state changes
|
||||
if (latestMessage.projectName) {
|
||||
refreshProjects();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'taskmaster-tasks-updated':
|
||||
// Refresh tasks for the current project
|
||||
if (latestMessage.projectName === currentProject?.name) {
|
||||
refreshTasks();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'taskmaster-mcp-status-changed':
|
||||
// Refresh MCP server status
|
||||
refreshMCPStatus();
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore non-TaskMaster messages
|
||||
break;
|
||||
}
|
||||
}, [messages, refreshProjects, refreshTasks, refreshMCPStatus, currentProject]);
|
||||
|
||||
// Context value
|
||||
const contextValue = {
|
||||
// State
|
||||
projects,
|
||||
currentProject,
|
||||
projectTaskMaster,
|
||||
mcpServerStatus,
|
||||
tasks,
|
||||
nextTask,
|
||||
isLoading,
|
||||
isLoadingTasks,
|
||||
isLoadingMCP,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
refreshProjects,
|
||||
setCurrentProject,
|
||||
refreshTasks,
|
||||
refreshMCPStatus,
|
||||
clearError
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskMasterContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TaskMasterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskMasterContext;
|
||||
95
src/contexts/TasksSettingsContext.jsx
Normal file
95
src/contexts/TasksSettingsContext.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
const TasksSettingsContext = createContext({
|
||||
tasksEnabled: true,
|
||||
setTasksEnabled: () => {},
|
||||
toggleTasksEnabled: () => {},
|
||||
isTaskMasterInstalled: null,
|
||||
isTaskMasterReady: null,
|
||||
installationStatus: null,
|
||||
isCheckingInstallation: true
|
||||
});
|
||||
|
||||
export const useTasksSettings = () => {
|
||||
const context = useContext(TasksSettingsContext);
|
||||
if (!context) {
|
||||
throw new Error('useTasksSettings must be used within a TasksSettingsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const TasksSettingsProvider = ({ children }) => {
|
||||
const [tasksEnabled, setTasksEnabled] = useState(() => {
|
||||
// Load from localStorage on initialization
|
||||
const saved = localStorage.getItem('tasks-enabled');
|
||||
return saved !== null ? JSON.parse(saved) : true; // Default to true
|
||||
});
|
||||
|
||||
const [isTaskMasterInstalled, setIsTaskMasterInstalled] = useState(null);
|
||||
const [isTaskMasterReady, setIsTaskMasterReady] = useState(null);
|
||||
const [installationStatus, setInstallationStatus] = useState(null);
|
||||
const [isCheckingInstallation, setIsCheckingInstallation] = useState(true);
|
||||
|
||||
// Save to localStorage whenever tasksEnabled changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('tasks-enabled', JSON.stringify(tasksEnabled));
|
||||
}, [tasksEnabled]);
|
||||
|
||||
// Check TaskMaster installation status asynchronously on component mount
|
||||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
try {
|
||||
const response = await api.get('/taskmaster/installation-status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setInstallationStatus(data);
|
||||
setIsTaskMasterInstalled(data.installation?.isInstalled || false);
|
||||
setIsTaskMasterReady(data.isReady || false);
|
||||
|
||||
// If TaskMaster is not installed and user hasn't explicitly enabled tasks,
|
||||
// disable tasks automatically
|
||||
const userEnabledTasks = localStorage.getItem('tasks-enabled');
|
||||
if (!data.installation?.isInstalled && !userEnabledTasks) {
|
||||
setTasksEnabled(false);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to check TaskMaster installation status');
|
||||
setIsTaskMasterInstalled(false);
|
||||
setIsTaskMasterReady(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking TaskMaster installation:', error);
|
||||
setIsTaskMasterInstalled(false);
|
||||
setIsTaskMasterReady(false);
|
||||
} finally {
|
||||
setIsCheckingInstallation(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Run check asynchronously without blocking initial render
|
||||
setTimeout(checkInstallation, 0);
|
||||
}, []);
|
||||
|
||||
const toggleTasksEnabled = () => {
|
||||
setTasksEnabled(prev => !prev);
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
tasksEnabled,
|
||||
setTasksEnabled,
|
||||
toggleTasksEnabled,
|
||||
isTaskMasterInstalled,
|
||||
isTaskMasterReady,
|
||||
installationStatus,
|
||||
isCheckingInstallation
|
||||
};
|
||||
|
||||
return (
|
||||
<TasksSettingsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TasksSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TasksSettingsContext;
|
||||
29
src/contexts/WebSocketContext.jsx
Normal file
29
src/contexts/WebSocketContext.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useWebSocket } from '../utils/websocket';
|
||||
|
||||
const WebSocketContext = createContext({
|
||||
ws: null,
|
||||
sendMessage: () => {},
|
||||
messages: [],
|
||||
isConnected: false
|
||||
});
|
||||
|
||||
export const useWebSocketContext = () => {
|
||||
const context = useContext(WebSocketContext);
|
||||
if (!context) {
|
||||
throw new Error('useWebSocketContext must be used within a WebSocketProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const WebSocketProvider = ({ children }) => {
|
||||
const webSocketData = useWebSocket();
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={webSocketData}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketContext;
|
||||
@@ -86,4 +86,48 @@ export const api = {
|
||||
body: formData,
|
||||
headers: {}, // Let browser set Content-Type for FormData
|
||||
}),
|
||||
|
||||
// TaskMaster endpoints
|
||||
taskmaster: {
|
||||
// Initialize TaskMaster in a project
|
||||
init: (projectName) =>
|
||||
authenticatedFetch(`/api/taskmaster/init/${projectName}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Add a new task
|
||||
addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
|
||||
authenticatedFetch(`/api/taskmaster/add-task/${projectName}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
|
||||
}),
|
||||
|
||||
// Parse PRD to generate tasks
|
||||
parsePRD: (projectName, { fileName, numTasks, append }) =>
|
||||
authenticatedFetch(`/api/taskmaster/parse-prd/${projectName}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ fileName, numTasks, append }),
|
||||
}),
|
||||
|
||||
// Get available PRD templates
|
||||
getTemplates: () =>
|
||||
authenticatedFetch('/api/taskmaster/prd-templates'),
|
||||
|
||||
// Apply a PRD template
|
||||
applyTemplate: (projectName, { templateId, fileName, customizations }) =>
|
||||
authenticatedFetch(`/api/taskmaster/apply-template/${projectName}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ templateId, fileName, customizations }),
|
||||
}),
|
||||
|
||||
// Update a task
|
||||
updateTask: (projectName, taskId, updates) =>
|
||||
authenticatedFetch(`/api/taskmaster/update-task/${projectName}/${taskId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates),
|
||||
}),
|
||||
},
|
||||
|
||||
// Generic GET method for any endpoint
|
||||
get: (endpoint) => authenticatedFetch(`/api${endpoint}`),
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export function useWebSocket() {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, []); // Keep dependency array but add proper cleanup
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
|
||||
BIN
store.db-shm
BIN
store.db-shm
Binary file not shown.
Reference in New Issue
Block a user