diff --git a/.changeset/upset-places-take.md b/.changeset/upset-places-take.md new file mode 100644 index 00000000..fb03158e --- /dev/null +++ b/.changeset/upset-places-take.md @@ -0,0 +1,4 @@ +--- +"task-master-ai": patch +--- +Complete VS Code extension with React-based kanban board UI. MCP integration for real-time Task Master synchronization. Professional CI/CD workflows for marketplace publishing \ No newline at end of file diff --git a/apps/extension/components.json b/apps/extension/components.json index 35dfed75..b77ea20d 100644 --- a/apps/extension/components.json +++ b/apps/extension/components.json @@ -1,18 +1,18 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/webview/index.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib" - }, - "iconLibrary": "lucide-react" -} \ No newline at end of file + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/webview/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib" + }, + "iconLibrary": "lucide-react" +} diff --git a/apps/extension/esbuild.js b/apps/extension/esbuild.js index 7f49f33b..30cb1fdc 100644 --- a/apps/extension/esbuild.js +++ b/apps/extension/esbuild.js @@ -1,5 +1,5 @@ -const esbuild = require("esbuild"); -const path = require("path"); +const esbuild = require('esbuild'); +const path = require('path'); const production = process.argv.includes('--production'); const watch = process.argv.includes('--watch'); @@ -17,11 +17,13 @@ const esbuildProblemMatcherPlugin = { build.onEnd((result) => { result.errors.forEach(({ text, location }) => { console.error(`✘ [ERROR] ${text}`); - console.error(` ${location.file}:${location.line}:${location.column}:`); + console.error( + ` ${location.file}:${location.line}:${location.column}:` + ); }); console.log('[watch] build finished'); }); - }, + } }; /** @@ -31,13 +33,13 @@ const aliasPlugin = { name: 'alias', setup(build) { // Handle @/ aliases for shadcn/ui - build.onResolve({ filter: /^@\// }, args => { + build.onResolve({ filter: /^@\// }, (args) => { const resolvedPath = path.resolve(__dirname, 'src', args.path.slice(2)); - + // Try to resolve with common TypeScript extensions const fs = require('fs'); const extensions = ['.tsx', '.ts', '.jsx', '.js']; - + // Check if it's a file first for (const ext of extensions) { const fullPath = resolvedPath + ext; @@ -45,7 +47,7 @@ const aliasPlugin = { return { path: fullPath }; } } - + // Check if it's a directory with index file for (const ext of extensions) { const indexPath = path.join(resolvedPath, 'index' + ext); @@ -53,11 +55,11 @@ const aliasPlugin = { return { path: indexPath }; } } - + // Fallback to original behavior return { path: resolvedPath }; }); - }, + } }; async function main() { @@ -76,12 +78,9 @@ async function main() { // Add production optimizations ...(production && { drop: ['debugger'], - pure: ['console.log', 'console.debug', 'console.trace'], + pure: ['console.log', 'console.debug', 'console.trace'] }), - plugins: [ - esbuildProblemMatcherPlugin, - aliasPlugin, - ], + plugins: [esbuildProblemMatcherPlugin, aliasPlugin] }); // Build configuration for the React webview @@ -102,35 +101,26 @@ async function main() { external: ['*.css'], define: { 'process.env.NODE_ENV': production ? '"production"' : '"development"', - 'global': 'globalThis' + global: 'globalThis' }, // Add production optimizations for webview too ...(production && { drop: ['debugger'], - pure: ['console.log', 'console.debug', 'console.trace'], + pure: ['console.log', 'console.debug', 'console.trace'] }), - plugins: [ - esbuildProblemMatcherPlugin, - aliasPlugin, - ], + plugins: [esbuildProblemMatcherPlugin, aliasPlugin] }); if (watch) { - await Promise.all([ - extensionCtx.watch(), - webviewCtx.watch() - ]); + await Promise.all([extensionCtx.watch(), webviewCtx.watch()]); } else { - await Promise.all([ - extensionCtx.rebuild(), - webviewCtx.rebuild() - ]); + await Promise.all([extensionCtx.rebuild(), webviewCtx.rebuild()]); await extensionCtx.dispose(); await webviewCtx.dispose(); } } -main().catch(e => { +main().catch((e) => { console.error(e); process.exit(1); -}); \ No newline at end of file +}); diff --git a/apps/extension/eslint.config.mjs b/apps/extension/eslint.config.mjs index d5c0b53a..d4c8915c 100644 --- a/apps/extension/eslint.config.mjs +++ b/apps/extension/eslint.config.mjs @@ -1,28 +1,34 @@ -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import tsParser from "@typescript-eslint/parser"; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; -export default [{ - files: ["**/*.ts"], -}, { - plugins: { - "@typescript-eslint": typescriptEslint, - }, +export default [ + { + files: ['**/*.ts'] + }, + { + plugins: { + '@typescript-eslint': typescriptEslint + }, - languageOptions: { - parser: tsParser, - ecmaVersion: 2022, - sourceType: "module", - }, + languageOptions: { + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module' + }, - rules: { - "@typescript-eslint/naming-convention": ["warn", { - selector: "import", - format: ["camelCase", "PascalCase"], - }], + rules: { + '@typescript-eslint/naming-convention': [ + 'warn', + { + selector: 'import', + format: ['camelCase', 'PascalCase'] + } + ], - curly: "warn", - eqeqeq: "warn", - "no-throw-literal": "warn", - semi: "warn", - }, -}]; \ No newline at end of file + curly: 'warn', + eqeqeq: 'warn', + 'no-throw-literal': 'warn', + semi: 'warn' + } + } +]; diff --git a/apps/extension/package.json b/apps/extension/package.json index 60d35860..cf1ea2e4 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,268 +1,250 @@ { - "name": "taskr", - "displayName": "Task Master Kanban", - "description": "A visual Kanban board interface for Task Master projects in VS Code", - "version": "1.0.0", - "publisher": "DavidMaliglowka", - "icon": "assets/icon.png", - "engines": { - "vscode": "^1.93.0" - }, - "categories": [ - "AI", - "Visualization", - "Education", - "Other" - ], - "main": "./dist/extension.js", - "contributes": { - "commands": [ - { - "command": "taskr.showKanbanBoard", - "title": "Task Master Kanban: Show Board" - }, - { - "command": "taskr.checkConnection", - "title": "Task Master Kanban: Check Connection" - }, - { - "command": "taskr.reconnect", - "title": "Task Master Kanban: Reconnect" - }, - { - "command": "taskr.openSettings", - "title": "Task Master Kanban: Open Settings" - } - ], - "configuration": { - "title": "Task Master Kanban", - "properties": { - "taskmaster.mcp.command": { - "type": "string", - "default": "npx", - "description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')." - }, - "taskmaster.mcp.args": { - "type": "array", - "items": { - "type": "string" - }, - "default": [ - "-y", - "--package=task-master-ai", - "task-master-ai" - ], - "description": "An array of arguments to pass to the MCP server command." - }, - "taskmaster.mcp.cwd": { - "type": "string", - "description": "Working directory for the Task Master MCP server (defaults to workspace root)" - }, - "taskmaster.mcp.env": { - "type": "object", - "description": "Environment variables for the Task Master MCP server" - }, - "taskmaster.mcp.timeout": { - "type": "number", - "default": 30000, - "minimum": 1000, - "maximum": 300000, - "description": "Connection timeout in milliseconds" - }, - "taskmaster.mcp.maxReconnectAttempts": { - "type": "number", - "default": 5, - "minimum": 1, - "maximum": 20, - "description": "Maximum number of reconnection attempts" - }, - "taskmaster.mcp.reconnectBackoffMs": { - "type": "number", - "default": 1000, - "minimum": 100, - "maximum": 10000, - "description": "Initial reconnection backoff delay in milliseconds" - }, - "taskmaster.mcp.maxBackoffMs": { - "type": "number", - "default": 30000, - "minimum": 1000, - "maximum": 300000, - "description": "Maximum reconnection backoff delay in milliseconds" - }, - "taskmaster.mcp.healthCheckIntervalMs": { - "type": "number", - "default": 15000, - "minimum": 5000, - "maximum": 60000, - "description": "Health check interval in milliseconds" - }, - "taskmaster.ui.autoRefresh": { - "type": "boolean", - "default": true, - "description": "Automatically refresh tasks from the server" - }, - "taskmaster.ui.refreshIntervalMs": { - "type": "number", - "default": 10000, - "minimum": 1000, - "maximum": 300000, - "description": "Auto-refresh interval in milliseconds" - }, - "taskmaster.ui.theme": { - "type": "string", - "enum": [ - "auto", - "light", - "dark" - ], - "default": "auto", - "description": "UI theme preference" - }, - "taskmaster.ui.showCompletedTasks": { - "type": "boolean", - "default": true, - "description": "Show completed tasks in the Kanban board" - }, - "taskmaster.ui.taskDisplayLimit": { - "type": "number", - "default": 100, - "minimum": 1, - "maximum": 1000, - "description": "Maximum number of tasks to display" - }, - "taskmaster.ui.showPriority": { - "type": "boolean", - "default": true, - "description": "Show task priority indicators" - }, - "taskmaster.ui.showTaskIds": { - "type": "boolean", - "default": true, - "description": "Show task IDs in the interface" - }, - "taskmaster.performance.maxConcurrentRequests": { - "type": "number", - "default": 5, - "minimum": 1, - "maximum": 20, - "description": "Maximum number of concurrent MCP requests" - }, - "taskmaster.performance.requestTimeoutMs": { - "type": "number", - "default": 30000, - "minimum": 1000, - "maximum": 300000, - "description": "Request timeout in milliseconds" - }, - "taskmaster.performance.cacheTasksMs": { - "type": "number", - "default": 5000, - "minimum": 0, - "maximum": 60000, - "description": "Task cache duration in milliseconds" - }, - "taskmaster.performance.lazyLoadThreshold": { - "type": "number", - "default": 50, - "minimum": 10, - "maximum": 500, - "description": "Number of tasks before enabling lazy loading" - }, - "taskmaster.debug.enableLogging": { - "type": "boolean", - "default": true, - "description": "Enable debug logging" - }, - "taskmaster.debug.logLevel": { - "type": "string", - "enum": [ - "error", - "warn", - "info", - "debug" - ], - "default": "info", - "description": "Logging level" - }, - "taskmaster.debug.enableConnectionMetrics": { - "type": "boolean", - "default": true, - "description": "Enable connection performance metrics" - }, - "taskmaster.debug.saveEventLogs": { - "type": "boolean", - "default": false, - "description": "Save event logs to files" - }, - "taskmaster.debug.maxEventLogSize": { - "type": "number", - "default": 1000, - "minimum": 10, - "maximum": 10000, - "description": "Maximum number of events to keep in memory" - } - } - } - }, - "scripts": { - "vscode:prepublish": false, - "build": "pnpm run build:js && pnpm run build:css", - "build:js": "node ./esbuild.js --production", - "build:css": "npx @tailwindcss/cli -o ./dist/index.css --minify", - "package": "pnpm exec node ./package.mjs", - "package:direct": "node ./package.mjs", - "debug:env": "node ./debug-env.mjs", - "compile": "node ./esbuild.js", - "watch": "pnpm run watch:js & pnpm run watch:css", - "watch:js": "node ./esbuild.js --watch", - "watch:css": "npx @tailwindcss/cli -o ./dist/index.css --watch", - "lint": "eslint src --ext ts,tsx", - "test": "vscode-test", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0", - "@modelcontextprotocol/sdk": "1.13.3", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-portal": "^1.1.9", - "@radix-ui/react-scroll-area": "^1.2.9", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/postcss": "^4.1.11", - "@types/mocha": "^10.0.10", - "@types/node": "20.x", - "@types/react": "19.1.8", - "@types/react-dom": "19.1.6", - "@types/vscode": "^1.101.0", - "@typescript-eslint/eslint-plugin": "^8.31.1", - "@typescript-eslint/parser": "^8.31.1", - "@vscode/test-cli": "^0.0.11", - "@vscode/test-electron": "^2.5.2", - "@vscode/vsce": "^2.32.0", - "autoprefixer": "10.4.21", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "esbuild": "^0.25.3", - "esbuild-postcss": "^0.0.4", - "eslint": "^9.25.1", - "fs-extra": "^11.3.0", - "lucide-react": "^0.525.0", - "npm-run-all": "^4.1.5", - "postcss": "8.5.6", - "react": "19.1.0", - "react-dom": "19.1.0", - "tailwind-merge": "^3.3.1", - "tailwindcss": "4.1.11", - "typescript": "^5.8.3" - }, - "pnpm": { - "overrides": { - "glob@<8": "^10.4.5", - "inflight": "npm:@tootallnate/once@2" - } - } + "name": "taskr", + "displayName": "Task Master Kanban", + "description": "A visual Kanban board interface for Task Master projects in VS Code", + "version": "1.0.0", + "publisher": "DavidMaliglowka", + "icon": "assets/icon.png", + "engines": { + "vscode": "^1.93.0" + }, + "categories": ["AI", "Visualization", "Education", "Other"], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "taskr.showKanbanBoard", + "title": "Task Master Kanban: Show Board" + }, + { + "command": "taskr.checkConnection", + "title": "Task Master Kanban: Check Connection" + }, + { + "command": "taskr.reconnect", + "title": "Task Master Kanban: Reconnect" + }, + { + "command": "taskr.openSettings", + "title": "Task Master Kanban: Open Settings" + } + ], + "configuration": { + "title": "Task Master Kanban", + "properties": { + "taskmaster.mcp.command": { + "type": "string", + "default": "npx", + "description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')." + }, + "taskmaster.mcp.args": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["-y", "--package=task-master-ai", "task-master-ai"], + "description": "An array of arguments to pass to the MCP server command." + }, + "taskmaster.mcp.cwd": { + "type": "string", + "description": "Working directory for the Task Master MCP server (defaults to workspace root)" + }, + "taskmaster.mcp.env": { + "type": "object", + "description": "Environment variables for the Task Master MCP server" + }, + "taskmaster.mcp.timeout": { + "type": "number", + "default": 30000, + "minimum": 1000, + "maximum": 300000, + "description": "Connection timeout in milliseconds" + }, + "taskmaster.mcp.maxReconnectAttempts": { + "type": "number", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Maximum number of reconnection attempts" + }, + "taskmaster.mcp.reconnectBackoffMs": { + "type": "number", + "default": 1000, + "minimum": 100, + "maximum": 10000, + "description": "Initial reconnection backoff delay in milliseconds" + }, + "taskmaster.mcp.maxBackoffMs": { + "type": "number", + "default": 30000, + "minimum": 1000, + "maximum": 300000, + "description": "Maximum reconnection backoff delay in milliseconds" + }, + "taskmaster.mcp.healthCheckIntervalMs": { + "type": "number", + "default": 15000, + "minimum": 5000, + "maximum": 60000, + "description": "Health check interval in milliseconds" + }, + "taskmaster.ui.autoRefresh": { + "type": "boolean", + "default": true, + "description": "Automatically refresh tasks from the server" + }, + "taskmaster.ui.refreshIntervalMs": { + "type": "number", + "default": 10000, + "minimum": 1000, + "maximum": 300000, + "description": "Auto-refresh interval in milliseconds" + }, + "taskmaster.ui.theme": { + "type": "string", + "enum": ["auto", "light", "dark"], + "default": "auto", + "description": "UI theme preference" + }, + "taskmaster.ui.showCompletedTasks": { + "type": "boolean", + "default": true, + "description": "Show completed tasks in the Kanban board" + }, + "taskmaster.ui.taskDisplayLimit": { + "type": "number", + "default": 100, + "minimum": 1, + "maximum": 1000, + "description": "Maximum number of tasks to display" + }, + "taskmaster.ui.showPriority": { + "type": "boolean", + "default": true, + "description": "Show task priority indicators" + }, + "taskmaster.ui.showTaskIds": { + "type": "boolean", + "default": true, + "description": "Show task IDs in the interface" + }, + "taskmaster.performance.maxConcurrentRequests": { + "type": "number", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Maximum number of concurrent MCP requests" + }, + "taskmaster.performance.requestTimeoutMs": { + "type": "number", + "default": 30000, + "minimum": 1000, + "maximum": 300000, + "description": "Request timeout in milliseconds" + }, + "taskmaster.performance.cacheTasksMs": { + "type": "number", + "default": 5000, + "minimum": 0, + "maximum": 60000, + "description": "Task cache duration in milliseconds" + }, + "taskmaster.performance.lazyLoadThreshold": { + "type": "number", + "default": 50, + "minimum": 10, + "maximum": 500, + "description": "Number of tasks before enabling lazy loading" + }, + "taskmaster.debug.enableLogging": { + "type": "boolean", + "default": true, + "description": "Enable debug logging" + }, + "taskmaster.debug.logLevel": { + "type": "string", + "enum": ["error", "warn", "info", "debug"], + "default": "info", + "description": "Logging level" + }, + "taskmaster.debug.enableConnectionMetrics": { + "type": "boolean", + "default": true, + "description": "Enable connection performance metrics" + }, + "taskmaster.debug.saveEventLogs": { + "type": "boolean", + "default": false, + "description": "Save event logs to files" + }, + "taskmaster.debug.maxEventLogSize": { + "type": "number", + "default": 1000, + "minimum": 10, + "maximum": 10000, + "description": "Maximum number of events to keep in memory" + } + } + } + }, + "scripts": { + "vscode:prepublish": false, + "build": "pnpm run build:js && pnpm run build:css", + "build:js": "node ./esbuild.js --production", + "build:css": "npx @tailwindcss/cli -o ./dist/index.css --minify", + "package": "pnpm exec node ./package.mjs", + "package:direct": "node ./package.mjs", + "debug:env": "node ./debug-env.mjs", + "compile": "node ./esbuild.js", + "watch": "pnpm run watch:js & pnpm run watch:css", + "watch:js": "node ./esbuild.js --watch", + "watch:css": "npx @tailwindcss/cli -o ./dist/index.css --watch", + "lint": "eslint src --ext ts,tsx", + "test": "vscode-test", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@modelcontextprotocol/sdk": "1.13.3", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-portal": "^1.1.9", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/postcss": "^4.1.11", + "@types/mocha": "^10.0.10", + "@types/node": "20.x", + "@types/react": "19.1.8", + "@types/react-dom": "19.1.6", + "@types/vscode": "^1.101.0", + "@typescript-eslint/eslint-plugin": "^8.31.1", + "@typescript-eslint/parser": "^8.31.1", + "@vscode/test-cli": "^0.0.11", + "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "^2.32.0", + "autoprefixer": "10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "esbuild": "^0.25.3", + "esbuild-postcss": "^0.0.4", + "eslint": "^9.25.1", + "fs-extra": "^11.3.0", + "lucide-react": "^0.525.0", + "npm-run-all": "^4.1.5", + "postcss": "8.5.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "4.1.11", + "typescript": "^5.8.3" + }, + "pnpm": { + "overrides": { + "glob@<8": "^10.4.5", + "inflight": "npm:@tootallnate/once@2" + } + } } diff --git a/apps/extension/package.mjs b/apps/extension/package.mjs index 7a7ba752..1efd91fc 100644 --- a/apps/extension/package.mjs +++ b/apps/extension/package.mjs @@ -11,84 +11,102 @@ const packageDir = path.resolve(__dirname, 'vsix-build'); // --- End Configuration --- try { - console.log('🚀 Starting packaging process...'); + console.log('🚀 Starting packaging process...'); - // 1. Build Project - console.log('\nBuilding JavaScript...'); - execSync('pnpm run build:js', { stdio: 'inherit' }); - console.log('\nBuilding CSS...'); - execSync('pnpm run build:css', { stdio: 'inherit' }); + // 1. Build Project + console.log('\nBuilding JavaScript...'); + execSync('pnpm run build:js', { stdio: 'inherit' }); + console.log('\nBuilding CSS...'); + execSync('pnpm run build:css', { stdio: 'inherit' }); - // 2. Prepare Clean Directory - console.log(`\nPreparing clean directory at: ${packageDir}`); - fs.emptyDirSync(packageDir); + // 2. Prepare Clean Directory + console.log(`\nPreparing clean directory at: ${packageDir}`); + fs.emptyDirSync(packageDir); - // 3. Copy Build Artifacts (excluding source maps) - console.log('Copying build artifacts...'); - const distDir = path.resolve(__dirname, 'dist'); - const targetDistDir = path.resolve(packageDir, 'dist'); - fs.ensureDirSync(targetDistDir); - - // Only copy the files we need (exclude .map files) - const filesToCopy = ['extension.js', 'index.js', 'index.css']; - for (const file of filesToCopy) { - const srcFile = path.resolve(distDir, file); - const destFile = path.resolve(targetDistDir, file); - if (fs.existsSync(srcFile)) { - fs.copySync(srcFile, destFile); - console.log(` - Copied dist/${file}`); - } - } + // 3. Copy Build Artifacts (excluding source maps) + console.log('Copying build artifacts...'); + const distDir = path.resolve(__dirname, 'dist'); + const targetDistDir = path.resolve(packageDir, 'dist'); + fs.ensureDirSync(targetDistDir); - // 4. Copy additional files - const additionalFiles = ['README.md', 'CHANGELOG.md', 'AGENTS.md']; - for (const file of additionalFiles) { - if (fs.existsSync(path.resolve(__dirname, file))) { - fs.copySync(path.resolve(__dirname, file), path.resolve(packageDir, file)); - console.log(` - Copied ${file}`); - } - } + // Only copy the files we need (exclude .map files) + const filesToCopy = ['extension.js', 'index.js', 'index.css']; + for (const file of filesToCopy) { + const srcFile = path.resolve(distDir, file); + const destFile = path.resolve(targetDistDir, file); + if (fs.existsSync(srcFile)) { + fs.copySync(srcFile, destFile); + console.log(` - Copied dist/${file}`); + } + } - // 5. Copy and RENAME the clean manifest - console.log('Copying and preparing the final package.json...'); - fs.copySync(path.resolve(__dirname, 'package.publish.json'), path.resolve(packageDir, 'package.json')); - console.log(' - Copied package.publish.json as package.json'); + // 4. Copy additional files + const additionalFiles = ['README.md', 'CHANGELOG.md', 'AGENTS.md']; + for (const file of additionalFiles) { + if (fs.existsSync(path.resolve(__dirname, file))) { + fs.copySync( + path.resolve(__dirname, file), + path.resolve(packageDir, file) + ); + console.log(` - Copied ${file}`); + } + } - // 6. Copy .vscodeignore if it exists - if (fs.existsSync(path.resolve(__dirname, '.vscodeignore'))) { - fs.copySync(path.resolve(__dirname, '.vscodeignore'), path.resolve(packageDir, '.vscodeignore')); - console.log(' - Copied .vscodeignore'); - } + // 5. Copy and RENAME the clean manifest + console.log('Copying and preparing the final package.json...'); + fs.copySync( + path.resolve(__dirname, 'package.publish.json'), + path.resolve(packageDir, 'package.json') + ); + console.log(' - Copied package.publish.json as package.json'); - // 7. Copy LICENSE if it exists - if (fs.existsSync(path.resolve(__dirname, 'LICENSE'))) { - fs.copySync(path.resolve(__dirname, 'LICENSE'), path.resolve(packageDir, 'LICENSE')); - console.log(' - Copied LICENSE'); - } + // 6. Copy .vscodeignore if it exists + if (fs.existsSync(path.resolve(__dirname, '.vscodeignore'))) { + fs.copySync( + path.resolve(__dirname, '.vscodeignore'), + path.resolve(packageDir, '.vscodeignore') + ); + console.log(' - Copied .vscodeignore'); + } - // 7a. Copy assets directory if it exists - const assetsDir = path.resolve(__dirname, 'assets'); - if (fs.existsSync(assetsDir)) { - const targetAssetsDir = path.resolve(packageDir, 'assets'); - fs.copySync(assetsDir, targetAssetsDir); - console.log(' - Copied assets directory'); - } + // 7. Copy LICENSE if it exists + if (fs.existsSync(path.resolve(__dirname, 'LICENSE'))) { + fs.copySync( + path.resolve(__dirname, 'LICENSE'), + path.resolve(packageDir, 'LICENSE') + ); + console.log(' - Copied LICENSE'); + } - // Small delay to ensure file system operations complete - await new Promise(resolve => setTimeout(resolve, 100)); + // 7a. Copy assets directory if it exists + const assetsDir = path.resolve(__dirname, 'assets'); + if (fs.existsSync(assetsDir)) { + const targetAssetsDir = path.resolve(packageDir, 'assets'); + fs.copySync(assetsDir, targetAssetsDir); + console.log(' - Copied assets directory'); + } - // 8. Final step - manual packaging - console.log('\n✅ Build preparation complete!'); - console.log('\nTo create the VSIX package, run:'); - console.log('\x1b[36m%s\x1b[0m', `cd vsix-build && pnpm exec vsce package --no-dependencies`); + // Small delay to ensure file system operations complete + await new Promise((resolve) => setTimeout(resolve, 100)); - // Read version from package.publish.json - const publishPackage = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.publish.json'), 'utf8')); - const version = publishPackage.version; - console.log(`\nYour extension will be packaged to: vsix-build/taskr-${version}.vsix`); + // 8. Final step - manual packaging + console.log('\n✅ Build preparation complete!'); + console.log('\nTo create the VSIX package, run:'); + console.log( + '\x1b[36m%s\x1b[0m', + `cd vsix-build && pnpm exec vsce package --no-dependencies` + ); + // Read version from package.publish.json + const publishPackage = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'package.publish.json'), 'utf8') + ); + const version = publishPackage.version; + console.log( + `\nYour extension will be packaged to: vsix-build/taskr-${version}.vsix` + ); } catch (error) { - console.error('\n❌ Packaging failed!'); - console.error(error.message); - process.exit(1); -} \ No newline at end of file + console.error('\n❌ Packaging failed!'); + console.error(error.message); + process.exit(1); +} diff --git a/apps/extension/package.publish.json b/apps/extension/package.publish.json index f0564464..7bc7c71a 100644 --- a/apps/extension/package.publish.json +++ b/apps/extension/package.publish.json @@ -1,248 +1,230 @@ { - "name": "taskr-kanban", - "displayName": "taskr: Task Master Kanban", - "description": "A visual Kanban board interface for Task Master projects in VS Code", - "version": "1.0.0", - "publisher": "DavidMaliglowka", - "icon": "assets/icon.png", - "engines": { - "vscode": "^1.93.0" - }, - "categories": [ - "AI", - "Visualization", - "Education", - "Other" - ], - "keywords": [ - "kanban", - "kanban board", - "productivity", - "todo", - "task tracking", - "project management", - "task-master", - "task management", - "agile", - "scrum", - "ai", - "mcp", - "model context protocol", - "dashboard", - "chatgpt", - "claude", - "openai", - "anthropic", - "task", - "npm", - "intellicode", - "react", - "typescript", - "php", - "python", - "node", - "planner", - "organizer", - "workflow", - "boards", - "cards" - ], - "repository": "https://github.com/eyaltoledano/claude-task-master", - "activationEvents": [ - "onCommand:taskr.showKanbanBoard", - "onCommand:taskr.checkConnection", - "onCommand:taskr.reconnect", - "onCommand:taskr.openSettings" - ], - "main": "./dist/extension.js", - "contributes": { - "commands": [ - { - "command": "taskr.showKanbanBoard", - "title": "Task Master Kanban: Show Board" - }, - { - "command": "taskr.checkConnection", - "title": "Task Master Kanban: Check Connection" - }, - { - "command": "taskr.reconnect", - "title": "Task Master Kanban: Reconnect" - }, - { - "command": "taskr.openSettings", - "title": "Task Master Kanban: Open Settings" - } - ], - "configuration": { - "title": "Task Master Kanban", - "properties": { - "taskmaster.mcp.command": { - "type": "string", - "default": "npx", - "description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')." - }, - "taskmaster.mcp.args": { - "type": "array", - "items": { - "type": "string" - }, - "default": [ - "-y", - "--package=task-master-ai", - "task-master-ai" - ], - "description": "An array of arguments to pass to the MCP server command." - }, - "taskmaster.mcp.cwd": { - "type": "string", - "description": "Working directory for the Task Master MCP server (defaults to workspace root)" - }, - "taskmaster.mcp.env": { - "type": "object", - "description": "Environment variables for the Task Master MCP server" - }, - "taskmaster.mcp.timeout": { - "type": "number", - "default": 30000, - "minimum": 1000, - "maximum": 300000, - "description": "Connection timeout in milliseconds" - }, - "taskmaster.mcp.maxReconnectAttempts": { - "type": "number", - "default": 5, - "minimum": 1, - "maximum": 20, - "description": "Maximum number of reconnection attempts" - }, - "taskmaster.mcp.reconnectBackoffMs": { - "type": "number", - "default": 1000, - "minimum": 100, - "maximum": 10000, - "description": "Initial reconnection backoff delay in milliseconds" - }, - "taskmaster.mcp.maxBackoffMs": { - "type": "number", - "default": 30000, - "minimum": 1000, - "maximum": 300000, - "description": "Maximum reconnection backoff delay in milliseconds" - }, - "taskmaster.mcp.healthCheckIntervalMs": { - "type": "number", - "default": 15000, - "minimum": 5000, - "maximum": 60000, - "description": "Health check interval in milliseconds" - }, - "taskmaster.ui.autoRefresh": { - "type": "boolean", - "default": true, - "description": "Automatically refresh tasks from the server" - }, - "taskmaster.ui.refreshIntervalMs": { - "type": "number", - "default": 10000, - "minimum": 1000, - "maximum": 300000, - "description": "Auto-refresh interval in milliseconds" - }, - "taskmaster.ui.theme": { - "type": "string", - "enum": [ - "auto", - "light", - "dark" - ], - "default": "auto", - "description": "UI theme preference" - }, - "taskmaster.ui.showCompletedTasks": { - "type": "boolean", - "default": true, - "description": "Show completed tasks in the Kanban board" - }, - "taskmaster.ui.taskDisplayLimit": { - "type": "number", - "default": 100, - "minimum": 1, - "maximum": 1000, - "description": "Maximum number of tasks to display" - }, - "taskmaster.ui.showPriority": { - "type": "boolean", - "default": true, - "description": "Show task priority indicators" - }, - "taskmaster.ui.showTaskIds": { - "type": "boolean", - "default": true, - "description": "Show task IDs in the interface" - }, - "taskmaster.performance.maxConcurrentRequests": { - "type": "number", - "default": 5, - "minimum": 1, - "maximum": 20, - "description": "Maximum number of concurrent MCP requests" - }, - "taskmaster.performance.requestTimeoutMs": { - "type": "number", - "default": 30000, - "minimum": 1000, - "maximum": 300000, - "description": "Request timeout in milliseconds" - }, - "taskmaster.performance.cacheTasksMs": { - "type": "number", - "default": 5000, - "minimum": 0, - "maximum": 60000, - "description": "Task cache duration in milliseconds" - }, - "taskmaster.performance.lazyLoadThreshold": { - "type": "number", - "default": 50, - "minimum": 10, - "maximum": 500, - "description": "Number of tasks before enabling lazy loading" - }, - "taskmaster.debug.enableLogging": { - "type": "boolean", - "default": true, - "description": "Enable debug logging" - }, - "taskmaster.debug.logLevel": { - "type": "string", - "enum": [ - "error", - "warn", - "info", - "debug" - ], - "default": "info", - "description": "Logging level" - }, - "taskmaster.debug.enableConnectionMetrics": { - "type": "boolean", - "default": true, - "description": "Enable connection performance metrics" - }, - "taskmaster.debug.saveEventLogs": { - "type": "boolean", - "default": false, - "description": "Save event logs to files" - }, - "taskmaster.debug.maxEventLogSize": { - "type": "number", - "default": 1000, - "minimum": 10, - "maximum": 10000, - "description": "Maximum number of events to keep in memory" - } - } - } - } - } \ No newline at end of file + "name": "taskr-kanban", + "displayName": "taskr: Task Master Kanban", + "description": "A visual Kanban board interface for Task Master projects in VS Code", + "version": "1.0.0", + "publisher": "DavidMaliglowka", + "icon": "assets/icon.png", + "engines": { + "vscode": "^1.93.0" + }, + "categories": ["AI", "Visualization", "Education", "Other"], + "keywords": [ + "kanban", + "kanban board", + "productivity", + "todo", + "task tracking", + "project management", + "task-master", + "task management", + "agile", + "scrum", + "ai", + "mcp", + "model context protocol", + "dashboard", + "chatgpt", + "claude", + "openai", + "anthropic", + "task", + "npm", + "intellicode", + "react", + "typescript", + "php", + "python", + "node", + "planner", + "organizer", + "workflow", + "boards", + "cards" + ], + "repository": "https://github.com/eyaltoledano/claude-task-master", + "activationEvents": [ + "onCommand:taskr.showKanbanBoard", + "onCommand:taskr.checkConnection", + "onCommand:taskr.reconnect", + "onCommand:taskr.openSettings" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "taskr.showKanbanBoard", + "title": "Task Master Kanban: Show Board" + }, + { + "command": "taskr.checkConnection", + "title": "Task Master Kanban: Check Connection" + }, + { + "command": "taskr.reconnect", + "title": "Task Master Kanban: Reconnect" + }, + { + "command": "taskr.openSettings", + "title": "Task Master Kanban: Open Settings" + } + ], + "configuration": { + "title": "Task Master Kanban", + "properties": { + "taskmaster.mcp.command": { + "type": "string", + "default": "npx", + "description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')." + }, + "taskmaster.mcp.args": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["-y", "--package=task-master-ai", "task-master-ai"], + "description": "An array of arguments to pass to the MCP server command." + }, + "taskmaster.mcp.cwd": { + "type": "string", + "description": "Working directory for the Task Master MCP server (defaults to workspace root)" + }, + "taskmaster.mcp.env": { + "type": "object", + "description": "Environment variables for the Task Master MCP server" + }, + "taskmaster.mcp.timeout": { + "type": "number", + "default": 30000, + "minimum": 1000, + "maximum": 300000, + "description": "Connection timeout in milliseconds" + }, + "taskmaster.mcp.maxReconnectAttempts": { + "type": "number", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Maximum number of reconnection attempts" + }, + "taskmaster.mcp.reconnectBackoffMs": { + "type": "number", + "default": 1000, + "minimum": 100, + "maximum": 10000, + "description": "Initial reconnection backoff delay in milliseconds" + }, + "taskmaster.mcp.maxBackoffMs": { + "type": "number", + "default": 30000, + "minimum": 1000, + "maximum": 300000, + "description": "Maximum reconnection backoff delay in milliseconds" + }, + "taskmaster.mcp.healthCheckIntervalMs": { + "type": "number", + "default": 15000, + "minimum": 5000, + "maximum": 60000, + "description": "Health check interval in milliseconds" + }, + "taskmaster.ui.autoRefresh": { + "type": "boolean", + "default": true, + "description": "Automatically refresh tasks from the server" + }, + "taskmaster.ui.refreshIntervalMs": { + "type": "number", + "default": 10000, + "minimum": 1000, + "maximum": 300000, + "description": "Auto-refresh interval in milliseconds" + }, + "taskmaster.ui.theme": { + "type": "string", + "enum": ["auto", "light", "dark"], + "default": "auto", + "description": "UI theme preference" + }, + "taskmaster.ui.showCompletedTasks": { + "type": "boolean", + "default": true, + "description": "Show completed tasks in the Kanban board" + }, + "taskmaster.ui.taskDisplayLimit": { + "type": "number", + "default": 100, + "minimum": 1, + "maximum": 1000, + "description": "Maximum number of tasks to display" + }, + "taskmaster.ui.showPriority": { + "type": "boolean", + "default": true, + "description": "Show task priority indicators" + }, + "taskmaster.ui.showTaskIds": { + "type": "boolean", + "default": true, + "description": "Show task IDs in the interface" + }, + "taskmaster.performance.maxConcurrentRequests": { + "type": "number", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Maximum number of concurrent MCP requests" + }, + "taskmaster.performance.requestTimeoutMs": { + "type": "number", + "default": 30000, + "minimum": 1000, + "maximum": 300000, + "description": "Request timeout in milliseconds" + }, + "taskmaster.performance.cacheTasksMs": { + "type": "number", + "default": 5000, + "minimum": 0, + "maximum": 60000, + "description": "Task cache duration in milliseconds" + }, + "taskmaster.performance.lazyLoadThreshold": { + "type": "number", + "default": 50, + "minimum": 10, + "maximum": 500, + "description": "Number of tasks before enabling lazy loading" + }, + "taskmaster.debug.enableLogging": { + "type": "boolean", + "default": true, + "description": "Enable debug logging" + }, + "taskmaster.debug.logLevel": { + "type": "string", + "enum": ["error", "warn", "info", "debug"], + "default": "info", + "description": "Logging level" + }, + "taskmaster.debug.enableConnectionMetrics": { + "type": "boolean", + "default": true, + "description": "Enable connection performance metrics" + }, + "taskmaster.debug.saveEventLogs": { + "type": "boolean", + "default": false, + "description": "Save event logs to files" + }, + "taskmaster.debug.maxEventLogSize": { + "type": "number", + "default": 1000, + "minimum": 10, + "maximum": 10000, + "description": "Maximum number of events to keep in memory" + } + } + } + } +} diff --git a/apps/extension/src/components/TaskDetailsView.tsx b/apps/extension/src/components/TaskDetailsView.tsx index f843b7bd..72a71b5f 100644 --- a/apps/extension/src/components/TaskDetailsView.tsx +++ b/apps/extension/src/components/TaskDetailsView.tsx @@ -1,11 +1,11 @@ import React, { useState, useEffect, useContext, useCallback } from 'react'; import { VSCodeContext, TaskMasterTask } from '../webview/index'; import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator } from '@/components/ui/breadcrumb'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; @@ -14,1235 +14,1370 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, + Collapsible, + CollapsibleContent, + CollapsibleTrigger } from '@/components/ui/collapsible'; -import { ChevronRight, ChevronDown, Plus, Wand2, PlusCircle, Loader2 } from 'lucide-react'; +import { + ChevronRight, + ChevronDown, + Plus, + Wand2, + PlusCircle, + Loader2 +} from 'lucide-react'; interface TaskDetailsViewProps { - taskId: string; - onNavigateBack: () => void; - onNavigateToTask: (taskId: string) => void; + taskId: string; + onNavigateBack: () => void; + onNavigateToTask: (taskId: string) => void; } // Markdown renderer component to handle code blocks -const MarkdownRenderer: React.FC<{ content: string; className?: string }> = ({ content, className = '' }) => { - // Parse content to separate code blocks from regular text - const parseMarkdown = (text: string) => { - const parts = []; - const lines = text.split('\n'); - let currentBlock = []; - let inCodeBlock = false; - let codeLanguage = ''; +const MarkdownRenderer: React.FC<{ content: string; className?: string }> = ({ + content, + className = '' +}) => { + // Parse content to separate code blocks from regular text + const parseMarkdown = (text: string) => { + const parts = []; + const lines = text.split('\n'); + let currentBlock = []; + let inCodeBlock = false; + let codeLanguage = ''; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (line.startsWith('```')) { - if (inCodeBlock) { - // End of code block - if (currentBlock.length > 0) { - parts.push({ - type: 'code', - content: currentBlock.join('\n'), - language: codeLanguage - }); - currentBlock = []; - } - inCodeBlock = false; - codeLanguage = ''; - } else { - // Start of code block - if (currentBlock.length > 0) { - parts.push({ - type: 'text', - content: currentBlock.join('\n') - }); - currentBlock = []; - } - inCodeBlock = true; - codeLanguage = line.substring(3).trim(); // Get language after ``` - } - } else { - currentBlock.push(line); - } - } + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; - // Handle remaining content - if (currentBlock.length > 0) { - parts.push({ - type: inCodeBlock ? 'code' : 'text', - content: currentBlock.join('\n'), - language: codeLanguage - }); - } + if (line.startsWith('```')) { + if (inCodeBlock) { + // End of code block + if (currentBlock.length > 0) { + parts.push({ + type: 'code', + content: currentBlock.join('\n'), + language: codeLanguage + }); + currentBlock = []; + } + inCodeBlock = false; + codeLanguage = ''; + } else { + // Start of code block + if (currentBlock.length > 0) { + parts.push({ + type: 'text', + content: currentBlock.join('\n') + }); + currentBlock = []; + } + inCodeBlock = true; + codeLanguage = line.substring(3).trim(); // Get language after ``` + } + } else { + currentBlock.push(line); + } + } - return parts; - }; + // Handle remaining content + if (currentBlock.length > 0) { + parts.push({ + type: inCodeBlock ? 'code' : 'text', + content: currentBlock.join('\n'), + language: codeLanguage + }); + } - const parts = parseMarkdown(content); + return parts; + }; - return ( -
- {parts.map((part, index) => { - if (part.type === 'code') { - return ( -
-               {part.content}
-             
- ); - } else { - // Handle inline code (single backticks) in text blocks - const textWithInlineCode = part.content.split(/(`[^`]+`)/g).map((segment, segIndex) => { - if (segment.startsWith('`') && segment.endsWith('`')) { - const codeContent = segment.slice(1, -1); - return ( - - {codeContent} - - ); - } - return segment; - }); + const parts = parseMarkdown(content); - return ( -
- {textWithInlineCode} -
- ); - } - })} -
- ); + return ( +
+ {parts.map((part, index) => { + if (part.type === 'code') { + return ( +
+							{part.content}
+						
+ ); + } else { + // Handle inline code (single backticks) in text blocks + const textWithInlineCode = part.content + .split(/(`[^`]+`)/g) + .map((segment, segIndex) => { + if (segment.startsWith('`') && segment.endsWith('`')) { + const codeContent = segment.slice(1, -1); + return ( + + {codeContent} + + ); + } + return segment; + }); + + return ( +
+ {textWithInlineCode} +
+ ); + } + })} +
+ ); }; // Custom Priority Badge Component with theme-adaptive styling -const PriorityBadge: React.FC<{ priority: TaskMasterTask['priority'] }> = ({ priority }) => { - const getPriorityColors = (priority: string) => { - switch (priority) { - case 'high': - return { - backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity - color: '#dc2626', // red-600 - works in both themes - borderColor: 'rgba(239, 68, 68, 0.4)' - }; - case 'medium': - return { - backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity - color: '#d97706', // amber-600 - works in both themes - borderColor: 'rgba(245, 158, 11, 0.4)' - }; - case 'low': - return { - backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity - color: '#16a34a', // green-600 - works in both themes - borderColor: 'rgba(34, 197, 94, 0.4)' - }; - default: - return { - backgroundColor: 'rgba(156, 163, 175, 0.2)', - color: 'var(--vscode-foreground)', - borderColor: 'rgba(156, 163, 175, 0.4)' - }; - } - }; +const PriorityBadge: React.FC<{ priority: TaskMasterTask['priority'] }> = ({ + priority +}) => { + const getPriorityColors = (priority: string) => { + switch (priority) { + case 'high': + return { + backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity + color: '#dc2626', // red-600 - works in both themes + borderColor: 'rgba(239, 68, 68, 0.4)' + }; + case 'medium': + return { + backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity + color: '#d97706', // amber-600 - works in both themes + borderColor: 'rgba(245, 158, 11, 0.4)' + }; + case 'low': + return { + backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity + color: '#16a34a', // green-600 - works in both themes + borderColor: 'rgba(34, 197, 94, 0.4)' + }; + default: + return { + backgroundColor: 'rgba(156, 163, 175, 0.2)', + color: 'var(--vscode-foreground)', + borderColor: 'rgba(156, 163, 175, 0.4)' + }; + } + }; - const colors = getPriorityColors(priority); + const colors = getPriorityColors(priority); - return ( - - {priority} - - ); + return ( + + {priority} + + ); }; // Custom Status Badge Component with theme-adaptive styling -const StatusBadge: React.FC<{ status: TaskMasterTask['status'] }> = ({ status }) => { - const getStatusColors = (status: string) => { - // Use colors that work well in both light and dark themes - switch (status) { - case 'pending': - return { - backgroundColor: 'rgba(156, 163, 175, 0.2)', // gray-400 with opacity - color: 'var(--vscode-foreground)', - borderColor: 'rgba(156, 163, 175, 0.4)' - }; - case 'in-progress': - return { - backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity - color: '#d97706', // amber-600 - works in both themes - borderColor: 'rgba(245, 158, 11, 0.4)' - }; - case 'review': - return { - backgroundColor: 'rgba(59, 130, 246, 0.2)', // blue-500 with opacity - color: '#2563eb', // blue-600 - works in both themes - borderColor: 'rgba(59, 130, 246, 0.4)' - }; - case 'done': - return { - backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity - color: '#16a34a', // green-600 - works in both themes - borderColor: 'rgba(34, 197, 94, 0.4)' - }; - case 'deferred': - return { - backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity - color: '#dc2626', // red-600 - works in both themes - borderColor: 'rgba(239, 68, 68, 0.4)' - }; - default: - return { - backgroundColor: 'rgba(156, 163, 175, 0.2)', - color: 'var(--vscode-foreground)', - borderColor: 'rgba(156, 163, 175, 0.4)' - }; - } - }; +const StatusBadge: React.FC<{ status: TaskMasterTask['status'] }> = ({ + status +}) => { + const getStatusColors = (status: string) => { + // Use colors that work well in both light and dark themes + switch (status) { + case 'pending': + return { + backgroundColor: 'rgba(156, 163, 175, 0.2)', // gray-400 with opacity + color: 'var(--vscode-foreground)', + borderColor: 'rgba(156, 163, 175, 0.4)' + }; + case 'in-progress': + return { + backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity + color: '#d97706', // amber-600 - works in both themes + borderColor: 'rgba(245, 158, 11, 0.4)' + }; + case 'review': + return { + backgroundColor: 'rgba(59, 130, 246, 0.2)', // blue-500 with opacity + color: '#2563eb', // blue-600 - works in both themes + borderColor: 'rgba(59, 130, 246, 0.4)' + }; + case 'done': + return { + backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity + color: '#16a34a', // green-600 - works in both themes + borderColor: 'rgba(34, 197, 94, 0.4)' + }; + case 'deferred': + return { + backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity + color: '#dc2626', // red-600 - works in both themes + borderColor: 'rgba(239, 68, 68, 0.4)' + }; + default: + return { + backgroundColor: 'rgba(156, 163, 175, 0.2)', + color: 'var(--vscode-foreground)', + borderColor: 'rgba(156, 163, 175, 0.4)' + }; + } + }; - const colors = getStatusColors(status); + const colors = getStatusColors(status); - return ( - - {status === 'pending' ? 'todo' : status} - - ); + return ( + + {status === 'pending' ? 'todo' : status} + + ); }; // Define the TaskFileData interface here since we're no longer importing it interface TaskFileData { - details?: string; - testStrategy?: string; + details?: string; + testStrategy?: string; } interface CombinedTaskData { - details?: string; - testStrategy?: string; - complexityScore?: number; // Only from MCP API + details?: string; + testStrategy?: string; + complexityScore?: number; // Only from MCP API } export const TaskDetailsView: React.FC = ({ - taskId, - onNavigateBack, - onNavigateToTask, + taskId, + onNavigateBack, + onNavigateToTask }) => { - const context = useContext(VSCodeContext); - if (!context) throw new Error('TaskDetailsView must be used within VSCodeContext'); + const context = useContext(VSCodeContext); + if (!context) + throw new Error('TaskDetailsView must be used within VSCodeContext'); - const { state, sendMessage } = context; - const { tasks } = state; + const { state, sendMessage } = context; + const { tasks } = state; - const [currentTask, setCurrentTask] = useState(null); - const [isSubtask, setIsSubtask] = useState(false); - const [parentTask, setParentTask] = useState(null); - - // Collapsible section states - const [isAiActionsExpanded, setIsAiActionsExpanded] = useState(true); - const [isImplementationExpanded, setIsImplementationExpanded] = useState(false); - const [isTestStrategyExpanded, setIsTestStrategyExpanded] = useState(false); - const [isSubtasksExpanded, setIsSubtasksExpanded] = useState(true); - - // AI Actions states - const [prompt, setPrompt] = useState(''); - const [isRegenerating, setIsRegenerating] = useState(false); - const [isAppending, setIsAppending] = useState(false); - - // Add subtask states - const [isAddingSubtask, setIsAddingSubtask] = useState(false); - const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); - const [newSubtaskDescription, setNewSubtaskDescription] = useState(''); - const [isSubmittingSubtask, setIsSubmittingSubtask] = useState(false); - - // Task file data states (for implementation details, test strategy, and complexity score) - const [taskFileData, setTaskFileData] = useState({ - details: undefined, - testStrategy: undefined, - complexityScore: undefined - }); - const [isLoadingTaskFileData, setIsLoadingTaskFileData] = useState(false); - const [taskFileDataError, setTaskFileDataError] = useState(null); + const [currentTask, setCurrentTask] = useState(null); + const [isSubtask, setIsSubtask] = useState(false); + const [parentTask, setParentTask] = useState(null); - // Get complexity score from main task data immediately (no flash) - const currentComplexityScore = currentTask?.complexityScore; - - // State for complexity data from MCP (only used for updates) - const [mcpComplexityScore, setMcpComplexityScore] = useState(undefined); - const [isLoadingComplexity, setIsLoadingComplexity] = useState(false); - - // Use MCP complexity if available, otherwise use main task data - const displayComplexityScore = mcpComplexityScore !== undefined ? mcpComplexityScore : currentComplexityScore; + // Collapsible section states + const [isAiActionsExpanded, setIsAiActionsExpanded] = useState(true); + const [isImplementationExpanded, setIsImplementationExpanded] = + useState(false); + const [isTestStrategyExpanded, setIsTestStrategyExpanded] = useState(false); + const [isSubtasksExpanded, setIsSubtasksExpanded] = useState(true); - // Fetch complexity from MCP when needed - const fetchComplexityFromMCP = useCallback(async (force = false) => { - if (!currentTask || (!force && currentComplexityScore !== undefined)) { - return; // Don't fetch if we already have a score unless forced - } - - setIsLoadingComplexity(true); - try { - const complexityResult = await sendMessage({ - type: 'mcpRequest', - tool: 'complexity_report', - params: {} - }); - - if (complexityResult?.data?.report?.complexityAnalysis) { - const taskComplexity = complexityResult.data.report.complexityAnalysis.find( - (analysis: any) => analysis.taskId === currentTask.id - ); - - if (taskComplexity?.complexityScore !== undefined) { - setMcpComplexityScore(taskComplexity.complexityScore); - } - } - } catch (error) { - console.error('Failed to fetch complexity from MCP:', error); - } finally { - setIsLoadingComplexity(false); - } - }, [currentTask, currentComplexityScore, sendMessage]); + // AI Actions states + const [prompt, setPrompt] = useState(''); + const [isRegenerating, setIsRegenerating] = useState(false); + const [isAppending, setIsAppending] = useState(false); - // Refresh complexity after AI operations or when task changes - useEffect(() => { - if (currentTask) { - // Reset MCP complexity when task changes - setMcpComplexityScore(undefined); - - // Fetch from MCP if no complexity score in main data - if (currentComplexityScore === undefined) { - fetchComplexityFromMCP(); - } - } - }, [currentTask?.id, currentComplexityScore, fetchComplexityFromMCP]); + // Add subtask states + const [isAddingSubtask, setIsAddingSubtask] = useState(false); + const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); + const [newSubtaskDescription, setNewSubtaskDescription] = useState(''); + const [isSubmittingSubtask, setIsSubmittingSubtask] = useState(false); - // Refresh complexity after AI operations - const refreshComplexityAfterAI = useCallback(() => { - // Force refresh complexity after AI operations - setTimeout(() => { - fetchComplexityFromMCP(true); - }, 2000); // Wait for AI operation to complete - }, [fetchComplexityFromMCP]); + // Task file data states (for implementation details, test strategy, and complexity score) + const [taskFileData, setTaskFileData] = useState({ + details: undefined, + testStrategy: undefined, + complexityScore: undefined + }); + const [isLoadingTaskFileData, setIsLoadingTaskFileData] = useState(false); + const [taskFileDataError, setTaskFileDataError] = useState( + null + ); - // Handle running complexity analysis for a task - const handleRunComplexityAnalysis = useCallback(async () => { - if (!currentTask) return; - - setIsLoadingComplexity(true); - try { - // Run complexity analysis on this specific task - await sendMessage({ - type: 'mcpRequest', - tool: 'analyze_project_complexity', - params: { - ids: currentTask.id.toString(), - research: false - } - }); - - // After analysis, fetch the updated complexity report - setTimeout(() => { - fetchComplexityFromMCP(true); - }, 1000); // Wait for analysis to complete - - } catch (error) { - console.error('Failed to run complexity analysis:', error); - } finally { - setIsLoadingComplexity(false); - } - }, [currentTask, sendMessage, fetchComplexityFromMCP]); + // Get complexity score from main task data immediately (no flash) + const currentComplexityScore = currentTask?.complexityScore; - // Parse task ID to determine if it's a subtask (e.g., "13.2") - const parseTaskId = (id: string) => { - const parts = id.split('.'); - if (parts.length === 2) { - return { - isSubtask: true, - parentId: parts[0], - subtaskIndex: parseInt(parts[1]) - 1, // Convert to 0-based index - }; - } - return { - isSubtask: false, - parentId: id, - subtaskIndex: -1, - }; - }; + // State for complexity data from MCP (only used for updates) + const [mcpComplexityScore, setMcpComplexityScore] = useState< + number | undefined + >(undefined); + const [isLoadingComplexity, setIsLoadingComplexity] = useState(false); - // Function to fetch task file data (implementation details and test strategy only) - const fetchTaskFileData = async () => { - if (!currentTask?.id) return; - - setIsLoadingTaskFileData(true); - setTaskFileDataError(null); - - try { - // For subtasks, construct the full dotted ID (e.g., "1.2") - // For main tasks, use the task ID as-is - const fileTaskId = isSubtask && parentTask - ? `${parentTask.id}.${currentTask.id}` - : currentTask.id; - - console.log('📄 Fetching task file data for task:', fileTaskId); - - // Get implementation details and test strategy from file - const fileData = await sendMessage({ - type: 'readTaskFileData', - data: { - taskId: fileTaskId, - tag: 'master' // TODO: Make this configurable - } - }); - - console.log('📄 Task file data response:', fileData); - - // Combine file data with complexity score from task data (already loaded) - const combinedData = { - details: fileData.details, - testStrategy: fileData.testStrategy, - complexityScore: currentTask.complexityScore // Use complexity score from already-loaded task data - }; - - console.log('📊 Combined task data:', combinedData); - setTaskFileData(combinedData); - - } catch (error) { - console.error('❌ Error fetching task file data:', error); - setTaskFileDataError(error instanceof Error ? error.message : 'Failed to load task data'); - } finally { - setIsLoadingTaskFileData(false); - } - }; + // Use MCP complexity if available, otherwise use main task data + const displayComplexityScore = + mcpComplexityScore !== undefined + ? mcpComplexityScore + : currentComplexityScore; - // Find task or subtask by ID - useEffect(() => { - const { isSubtask: isSubtaskId, parentId, subtaskIndex } = parseTaskId(taskId); - setIsSubtask(isSubtaskId); + // Fetch complexity from MCP when needed + const fetchComplexityFromMCP = useCallback( + async (force = false) => { + if (!currentTask || (!force && currentComplexityScore !== undefined)) { + return; // Don't fetch if we already have a score unless forced + } - if (isSubtaskId) { - // Find parent task - const parent = tasks.find(task => task.id === parentId); - setParentTask(parent || null); - - // Find subtask - if (parent && parent.subtasks && subtaskIndex >= 0 && subtaskIndex < parent.subtasks.length) { - const subtask = parent.subtasks[subtaskIndex]; - setCurrentTask(subtask); - // Fetch file data for subtask - fetchTaskFileData(); - } else { - setCurrentTask(null); - } - } else { - // Find main task - const task = tasks.find(task => task.id === parentId); - setCurrentTask(task || null); - setParentTask(null); - // Fetch file data for main task - if (task) { - fetchTaskFileData(); - } - } - }, [taskId, tasks]); + setIsLoadingComplexity(true); + try { + const complexityResult = await sendMessage({ + type: 'mcpRequest', + tool: 'complexity_report', + params: {} + }); - // Enhanced refresh logic for task file data when tasks are updated from polling - useEffect(() => { - if (currentTask) { - // Create a comprehensive hash of task data to detect any changes - const taskHash = JSON.stringify({ - id: currentTask.id, - title: currentTask.title, - description: currentTask.description, - status: currentTask.status, - priority: currentTask.priority, - dependencies: currentTask.dependencies, - subtasksCount: currentTask.subtasks?.length || 0, - subtasksStatus: currentTask.subtasks?.map(st => st.status) || [], - lastUpdate: Date.now() // Include timestamp to ensure periodic refresh - }); + if (complexityResult?.data?.report?.complexityAnalysis) { + const taskComplexity = + complexityResult.data.report.complexityAnalysis.find( + (analysis: any) => analysis.taskId === currentTask.id + ); - // Small delay to ensure the tasks.json file has been updated - const timeoutId = setTimeout(() => { - console.log('🔄 TaskDetailsView: Refreshing task file data due to task changes'); - fetchTaskFileData(); - }, 500); - - return () => clearTimeout(timeoutId); - } - }, [currentTask, tasks, taskId]); // More comprehensive dependencies + if (taskComplexity?.complexityScore !== undefined) { + setMcpComplexityScore(taskComplexity.complexityScore); + } + } + } catch (error) { + console.error('Failed to fetch complexity from MCP:', error); + } finally { + setIsLoadingComplexity(false); + } + }, + [currentTask, currentComplexityScore, sendMessage] + ); - // Periodic refresh to ensure we have the latest data - useEffect(() => { - if (currentTask) { - const intervalId = setInterval(() => { - console.log('🔄 TaskDetailsView: Periodic refresh of task file data'); - fetchTaskFileData(); - }, 30000); // Refresh every 30 seconds + // Refresh complexity after AI operations or when task changes + useEffect(() => { + if (currentTask) { + // Reset MCP complexity when task changes + setMcpComplexityScore(undefined); - return () => clearInterval(intervalId); - } - }, [currentTask, taskId]); + // Fetch from MCP if no complexity score in main data + if (currentComplexityScore === undefined) { + fetchComplexityFromMCP(); + } + } + }, [currentTask?.id, currentComplexityScore, fetchComplexityFromMCP]); - // Handle AI Actions - const handleRegenerate = async () => { - if (!currentTask || !prompt.trim()) return; + // Refresh complexity after AI operations + const refreshComplexityAfterAI = useCallback(() => { + // Force refresh complexity after AI operations + setTimeout(() => { + fetchComplexityFromMCP(true); + }, 2000); // Wait for AI operation to complete + }, [fetchComplexityFromMCP]); - setIsRegenerating(true); - try { - if (isSubtask && parentTask) { - await sendMessage({ - type: 'updateSubtask', - data: { - taskId: `${parentTask.id}.${currentTask.id}`, - prompt: prompt, - options: { research: false } - } - }); - } else { - await sendMessage({ - type: 'updateTask', - data: { - taskId: currentTask.id, - updates: { description: prompt }, - options: { append: false, research: false } - } - }); - } - - // Refresh both task file data and complexity after AI operation - setTimeout(() => { - console.log('🔄 TaskDetailsView: Refreshing after AI regeneration'); - fetchTaskFileData(); - }, 2000); // Wait 2 seconds for AI to finish processing - - // Refresh complexity after AI operation - refreshComplexityAfterAI(); - - } catch (error) { - console.error('❌ TaskDetailsView: Failed to regenerate task:', error); - } finally { - setIsRegenerating(false); - setPrompt(''); - } - }; + // Handle running complexity analysis for a task + const handleRunComplexityAnalysis = useCallback(async () => { + if (!currentTask) return; - const handleAppend = async () => { - if (!currentTask || !prompt.trim()) return; + setIsLoadingComplexity(true); + try { + // Run complexity analysis on this specific task + await sendMessage({ + type: 'mcpRequest', + tool: 'analyze_project_complexity', + params: { + ids: currentTask.id.toString(), + research: false + } + }); - setIsAppending(true); - try { - if (isSubtask && parentTask) { - await sendMessage({ - type: 'updateSubtask', - data: { - taskId: `${parentTask.id}.${currentTask.id}`, - prompt: prompt, - options: { research: false } - } - }); - } else { - await sendMessage({ - type: 'updateTask', - data: { - taskId: currentTask.id, - updates: { description: prompt }, - options: { append: true, research: false } - } - }); - } - - // Refresh both task file data and complexity after AI operation - setTimeout(() => { - console.log('🔄 TaskDetailsView: Refreshing after AI append'); - fetchTaskFileData(); - }, 2000); // Wait 2 seconds for AI to finish processing - - // Refresh complexity after AI operation - refreshComplexityAfterAI(); - - } catch (error) { - console.error('❌ TaskDetailsView: Failed to append to task:', error); - } finally { - setIsAppending(false); - setPrompt(''); - } - }; + // After analysis, fetch the updated complexity report + setTimeout(() => { + fetchComplexityFromMCP(true); + }, 1000); // Wait for analysis to complete + } catch (error) { + console.error('Failed to run complexity analysis:', error); + } finally { + setIsLoadingComplexity(false); + } + }, [currentTask, sendMessage, fetchComplexityFromMCP]); - // Handle adding a new subtask - const handleAddSubtask = async () => { - if (!currentTask || !newSubtaskTitle.trim() || isSubtask) return; + // Parse task ID to determine if it's a subtask (e.g., "13.2") + const parseTaskId = (id: string) => { + const parts = id.split('.'); + if (parts.length === 2) { + return { + isSubtask: true, + parentId: parts[0], + subtaskIndex: parseInt(parts[1]) - 1 // Convert to 0-based index + }; + } + return { + isSubtask: false, + parentId: id, + subtaskIndex: -1 + }; + }; - setIsSubmittingSubtask(true); - try { - await sendMessage({ - type: 'addSubtask', - data: { - parentTaskId: currentTask.id, - subtaskData: { - title: newSubtaskTitle.trim(), - description: newSubtaskDescription.trim() || undefined, - status: 'pending' - } - } - }); + // Function to fetch task file data (implementation details and test strategy only) + const fetchTaskFileData = async () => { + if (!currentTask?.id) return; - // Reset form and close - setNewSubtaskTitle(''); - setNewSubtaskDescription(''); - setIsAddingSubtask(false); + setIsLoadingTaskFileData(true); + setTaskFileDataError(null); - // Refresh task data to show the new subtask - setTimeout(() => { - console.log('🔄 TaskDetailsView: Refreshing after adding subtask'); - fetchTaskFileData(); - }, 1000); - - } catch (error) { - console.error('❌ TaskDetailsView: Failed to add subtask:', error); - } finally { - setIsSubmittingSubtask(false); - } - }; + try { + // For subtasks, construct the full dotted ID (e.g., "1.2") + // For main tasks, use the task ID as-is + const fileTaskId = + isSubtask && parentTask + ? `${parentTask.id}.${currentTask.id}` + : currentTask.id; - const handleCancelAddSubtask = () => { - setIsAddingSubtask(false); - setNewSubtaskTitle(''); - setNewSubtaskDescription(''); - }; + console.log('📄 Fetching task file data for task:', fileTaskId); - // Handle dependency navigation - const handleDependencyClick = (depId: string) => { - onNavigateToTask(depId); - }; + // Get implementation details and test strategy from file + const fileData = await sendMessage({ + type: 'readTaskFileData', + data: { + taskId: fileTaskId, + tag: 'master' // TODO: Make this configurable + } + }); - // Handle status change - const handleStatusChange = async (newStatus: TaskMasterTask['status']) => { - if (!currentTask) return; + console.log('📄 Task file data response:', fileData); - try { - await sendMessage({ - type: 'updateTaskStatus', - data: { - taskId: isSubtask && parentTask ? `${parentTask.id}.${currentTask.id}` : currentTask.id, - newStatus: newStatus - } - }); - } catch (error) { - console.error('❌ TaskDetailsView: Failed to update task status:', error); - } - }; + // Combine file data with complexity score from task data (already loaded) + const combinedData = { + details: fileData.details, + testStrategy: fileData.testStrategy, + complexityScore: currentTask.complexityScore // Use complexity score from already-loaded task data + }; - if (!currentTask) { - return ( -
-
-

Task not found

- -
-
- ); - } + console.log('📊 Combined task data:', combinedData); + setTaskFileData(combinedData); + } catch (error) { + console.error('❌ Error fetching task file data:', error); + setTaskFileDataError( + error instanceof Error ? error.message : 'Failed to load task data' + ); + } finally { + setIsLoadingTaskFileData(false); + } + }; - return ( -
- {/* Main content area with two-column layout */} -
- {/* Left column - Main content (2/3 width) */} -
- {/* Breadcrumb navigation */} - - - - - Kanban Board - - - {isSubtask && parentTask && ( - <> - - - onNavigateToTask(parentTask.id)} - className="cursor-pointer hover:text-vscode-foreground" - > - {parentTask.title} - - - - )} - - - {currentTask.title} - - - + // Find task or subtask by ID + useEffect(() => { + const { + isSubtask: isSubtaskId, + parentId, + subtaskIndex + } = parseTaskId(taskId); + setIsSubtask(isSubtaskId); - {/* Task title */} -

- {currentTask.title} -

+ if (isSubtaskId) { + // Find parent task + const parent = tasks.find((task) => task.id === parentId); + setParentTask(parent || null); - {/* Description (non-editable) */} -
-

- {currentTask.description || 'No description available.'} -

-
+ // Find subtask + if ( + parent && + parent.subtasks && + subtaskIndex >= 0 && + subtaskIndex < parent.subtasks.length + ) { + const subtask = parent.subtasks[subtaskIndex]; + setCurrentTask(subtask); + // Fetch file data for subtask + fetchTaskFileData(); + } else { + setCurrentTask(null); + } + } else { + // Find main task + const task = tasks.find((task) => task.id === parentId); + setCurrentTask(task || null); + setParentTask(null); + // Fetch file data for main task + if (task) { + fetchTaskFileData(); + } + } + }, [taskId, tasks]); - {/* AI Actions */} -
-
- -
+ // Enhanced refresh logic for task file data when tasks are updated from polling + useEffect(() => { + if (currentTask) { + // Create a comprehensive hash of task data to detect any changes + const taskHash = JSON.stringify({ + id: currentTask.id, + title: currentTask.title, + description: currentTask.description, + status: currentTask.status, + priority: currentTask.priority, + dependencies: currentTask.dependencies, + subtasksCount: currentTask.subtasks?.length || 0, + subtasksStatus: currentTask.subtasks?.map((st) => st.status) || [], + lastUpdate: Date.now() // Include timestamp to ensure periodic refresh + }); - {isAiActionsExpanded && ( -
-
-
- -