Merge pull request #183 from siteboon/feature/taskmasterAI-integration

Integration with TaskMaster AI
This commit is contained in:
viper151
2025-08-28 12:12:47 +03:00
committed by GitHub
33 changed files with 7856 additions and 111 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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}`;
}
}
}

View File

@@ -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);
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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: {}
};
}
}

View 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);
}
}
});
}

View File

@@ -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>
);

View File

@@ -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>

View 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;

View File

@@ -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>
);
}

View File

@@ -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 (

View 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;

View 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;

View File

@@ -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>

View File

@@ -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
View 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;

View 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;

View 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

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View File

@@ -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">

View File

@@ -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>

View 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;

View File

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

View File

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

View File

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

View File

@@ -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}`),
};

View File

@@ -17,7 +17,7 @@ export function useWebSocket() {
ws.close();
}
};
}, []);
}, []); // Keep dependency array but add proper cleanup
const connect = async () => {
try {

Binary file not shown.

View File