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
-
- Back to Kanban Board
-
-
-
- );
- }
+ 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 */}
-
-
- setIsAiActionsExpanded(!isAiActionsExpanded)}
- >
- {isAiActionsExpanded ? (
-
- ) : (
-
- )}
-
- 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 && (
-
-
-
-
- Enter your prompt
-
-
+ // 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);
-
- {/* Show regenerate button only for main tasks, not subtasks */}
- {!isSubtask && (
-
- {isRegenerating ? (
- <>
-
- Regenerating...
- >
- ) : (
- <>
-
- Regenerate Task
- >
- )}
-
- )}
+ return () => clearTimeout(timeoutId);
+ }
+ }, [currentTask, tasks, taskId]); // More comprehensive dependencies
-
- {isAppending ? (
- <>
-
- {isSubtask ? "Updating..." : "Appending..."}
- >
- ) : (
- <>
-
- {isSubtask ? "Add Notes to Subtask" : "Append to Task"}
- >
- )}
-
-
+ // 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
-
- {isSubtask ? (
-
- Add Notes: Appends timestamped implementation notes, progress updates, or findings to this subtask's details
-
- ) : (
- <>
-
- Regenerate: Completely rewrites the task description and subtasks based on your prompt
-
-
- Append: Adds new content to the existing task description based on your prompt
-
- >
- )}
-
-
-
- )}
-
+ return () => clearInterval(intervalId);
+ }
+ }, [currentTask, taskId]);
- {/* Implementation Details */}
-
-
- setIsImplementationExpanded(!isImplementationExpanded)}
- >
- {isImplementationExpanded ? (
-
- ) : (
-
- )}
- Implementation Details
-
- {isLoadingTaskFileData && (
-
- )}
-
+ // Handle AI Actions
+ const handleRegenerate = async () => {
+ if (!currentTask || !prompt.trim()) return;
- {isImplementationExpanded && (
-
-
- {isLoadingTaskFileData ? (
-
-
- Loading details...
-
- ) : taskFileDataError ? (
-
- Error loading details: {taskFileDataError}
-
- ) : taskFileData.details ? (
-
- ) : (
-
- No implementation details available
-
- )}
-
-
- )}
-
+ 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 }
+ }
+ });
+ }
- {/* Test Strategy */}
-
-
- setIsTestStrategyExpanded(!isTestStrategyExpanded)}
- >
- {isTestStrategyExpanded ? (
-
- ) : (
-
- )}
- Test Strategy
-
- {isLoadingTaskFileData && (
-
- )}
-
+ // 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
- {isTestStrategyExpanded && (
-
-
- {isLoadingTaskFileData ? (
-
-
- Loading strategy...
-
- ) : taskFileDataError ? (
-
- Error loading strategy: {taskFileDataError}
-
- ) : taskFileData.testStrategy ? (
-
- ) : (
-
- No test strategy available
-
- )}
-
-
- )}
-
+ // Refresh complexity after AI operation
+ refreshComplexityAfterAI();
+ } catch (error) {
+ console.error('❌ TaskDetailsView: Failed to regenerate task:', error);
+ } finally {
+ setIsRegenerating(false);
+ setPrompt('');
+ }
+ };
- {/* Subtasks section */}
- {((currentTask.subtasks && currentTask.subtasks.length > 0) || !isSubtask) && (
-
-
-
setIsSubtasksExpanded(!isSubtasksExpanded)}
- >
- {isSubtasksExpanded ? (
-
- ) : (
-
- )}
- Sub-issues
-
- {currentTask.subtasks && currentTask.subtasks.length > 0 && (
-
- {currentTask.subtasks?.filter(st => st.status === 'done').length}/{currentTask.subtasks?.length}
-
- )}
- {/* Only show add button for main tasks, not subtasks */}
- {!isSubtask && (
-
setIsAddingSubtask(true)}
- title="Add subtask"
- >
-
-
- )}
-
+ const handleAppend = async () => {
+ if (!currentTask || !prompt.trim()) return;
- {isSubtasksExpanded && (
-
- {/* Add Subtask Form */}
- {isAddingSubtask && (
-
-
Add New Subtask
-
-
-
- Title*
-
- setNewSubtaskTitle(e.target.value)}
- className="w-full px-3 py-2 text-sm bg-vscode-input-background border border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 rounded focus:border-vscode-focusBorder focus:ring-1 focus:ring-vscode-focusBorder"
- disabled={isSubmittingSubtask}
- autoFocus
- />
-
-
-
-
- Description (Optional)
-
-
-
-
-
- {isSubmittingSubtask ? (
- <>
-
- Adding...
- >
- ) : (
- <>
-
- Add Subtask
- >
- )}
-
-
-
- Cancel
-
-
-
-
- )}
-
- {currentTask.subtasks?.map((subtask, index) => {
- const subtaskId = `${currentTask.id}.${index + 1}`;
- const getStatusDotColor = (status: string) => {
- switch (status) {
- case 'pending': return '#9ca3af'; // gray-400
- case 'in-progress': return '#f59e0b'; // amber-500
- case 'review': return '#3b82f6'; // blue-500
- case 'done': return '#22c55e'; // green-500
- case 'deferred': return '#ef4444'; // red-500
- default: return '#9ca3af';
- }
- };
- const getSubtaskStatusColors = (status: string) => {
- switch (status) {
- case 'pending':
- return {
- backgroundColor: 'rgba(156, 163, 175, 0.2)',
- color: 'var(--vscode-foreground)',
- borderColor: 'rgba(156, 163, 175, 0.4)'
- };
- case 'in-progress':
- return {
- backgroundColor: 'rgba(245, 158, 11, 0.2)',
- color: '#d97706',
- borderColor: 'rgba(245, 158, 11, 0.4)'
- };
- case 'review':
- return {
- backgroundColor: 'rgba(59, 130, 246, 0.2)',
- color: '#2563eb',
- borderColor: 'rgba(59, 130, 246, 0.4)'
- };
- case 'done':
- return {
- backgroundColor: 'rgba(34, 197, 94, 0.2)',
- color: '#16a34a',
- borderColor: 'rgba(34, 197, 94, 0.4)'
- };
- case 'deferred':
- return {
- backgroundColor: 'rgba(239, 68, 68, 0.2)',
- color: '#dc2626',
- 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)'
- };
- }
- };
+ 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 }
+ }
+ });
+ }
- return (
-
onNavigateToTask(subtaskId)}
- >
-
-
{subtask.title}
-
- {subtask.status === 'pending' ? 'todo' : subtask.status}
-
-
- );
- })}
-
- )}
-
- )}
-
+ // 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
- {/* Right column - Properties sidebar (1/3 width) */}
-
-
-
-
-
Properties
-
+ // Refresh complexity after AI operation
+ refreshComplexityAfterAI();
+ } catch (error) {
+ console.error('❌ TaskDetailsView: Failed to append to task:', error);
+ } finally {
+ setIsAppending(false);
+ setPrompt('');
+ }
+ };
-
- {/* Status */}
-
- Status
- handleStatusChange(e.target.value as TaskMasterTask['status'])}
- className="border rounded-md px-3 py-1 text-sm font-medium focus:ring-1 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
- style={{
- backgroundColor: currentTask.status === 'pending' ? 'rgba(156, 163, 175, 0.2)' :
- currentTask.status === 'in-progress' ? 'rgba(245, 158, 11, 0.2)' :
- currentTask.status === 'review' ? 'rgba(59, 130, 246, 0.2)' :
- currentTask.status === 'done' ? 'rgba(34, 197, 94, 0.2)' :
- currentTask.status === 'deferred' ? 'rgba(239, 68, 68, 0.2)' :
- 'var(--vscode-input-background)',
- color: currentTask.status === 'pending' ? 'var(--vscode-foreground)' :
- currentTask.status === 'in-progress' ? '#d97706' :
- currentTask.status === 'review' ? '#2563eb' :
- currentTask.status === 'done' ? '#16a34a' :
- currentTask.status === 'deferred' ? '#dc2626' :
- 'var(--vscode-foreground)',
- borderColor: currentTask.status === 'pending' ? 'rgba(156, 163, 175, 0.4)' :
- currentTask.status === 'in-progress' ? 'rgba(245, 158, 11, 0.4)' :
- currentTask.status === 'review' ? 'rgba(59, 130, 246, 0.4)' :
- currentTask.status === 'done' ? 'rgba(34, 197, 94, 0.4)' :
- currentTask.status === 'deferred' ? 'rgba(239, 68, 68, 0.4)' :
- 'var(--vscode-input-border)'
- }}
- >
- To do
- In Progress
- Review
- Done
- Deferred
-
-
+ // Handle adding a new subtask
+ const handleAddSubtask = async () => {
+ if (!currentTask || !newSubtaskTitle.trim() || isSubtask) return;
+ setIsSubmittingSubtask(true);
+ try {
+ await sendMessage({
+ type: 'addSubtask',
+ data: {
+ parentTaskId: currentTask.id,
+ subtaskData: {
+ title: newSubtaskTitle.trim(),
+ description: newSubtaskDescription.trim() || undefined,
+ status: 'pending'
+ }
+ }
+ });
- {/* Priority */}
-
+ // Reset form and close
+ setNewSubtaskTitle('');
+ setNewSubtaskDescription('');
+ setIsAddingSubtask(false);
- {/* Complexity Score */}
-
-
- Complexity Score
-
- {isLoadingComplexity ? (
-
-
-
- Loading...
-
-
- ) : displayComplexityScore !== undefined ? (
-
-
- {displayComplexityScore}/10
-
-
= 7
- ? 'bg-red-500/20'
- : displayComplexityScore >= 4
- ? 'bg-yellow-500/20'
- : 'bg-green-500/20'
- }`}>
-
= 7
- ? 'bg-red-500'
- : displayComplexityScore >= 4
- ? 'bg-yellow-500'
- : 'bg-green-500'
- }`}
- style={{
- width: `${(displayComplexityScore || 0) * 10}%`
- }}
- />
-
-
- ) : currentTask?.status === 'done' || currentTask?.status === 'deferred' || currentTask?.status === 'review' ? (
-
- N/A
-
- ) : (
- <>
-
- No complexity score available
-
-
- handleRunComplexityAnalysis()}
- variant="outline"
- size="sm"
- className="text-xs"
- disabled={isRegenerating || isAppending}
- >
- Run Complexity Analysis
-
-
- >
- )}
-
-
-
+ // 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);
+ }
+ };
- {/* Dependencies */}
- {currentTask.dependencies && currentTask.dependencies.length > 0 && (
- <>
-
-
-
Dependencies
-
- {currentTask.dependencies.map((depId) => {
- const depTask = tasks.find(t => t.id === depId);
- const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`;
- const truncatedTitle = fullTitle.length > 40 ? fullTitle.substring(0, 37) + '...' : fullTitle;
- return (
-
handleDependencyClick(depId)}
- title={fullTitle}
- >
- {truncatedTitle}
-
- );
- })}
-
-
- >
- )}
+ const handleCancelAddSubtask = () => {
+ setIsAddingSubtask(false);
+ setNewSubtaskTitle('');
+ setNewSubtaskDescription('');
+ };
- {/* Divider after Dependencies */}
- {currentTask.dependencies && currentTask.dependencies.length > 0 && (
-
- )}
-
-
-
-
-
- );
+ // Handle dependency navigation
+ const handleDependencyClick = (depId: string) => {
+ onNavigateToTask(depId);
+ };
+
+ // Handle status change
+ const handleStatusChange = async (newStatus: TaskMasterTask['status']) => {
+ if (!currentTask) return;
+
+ 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);
+ }
+ };
+
+ if (!currentTask) {
+ return (
+
+
+
+ Task not found
+
+
+ Back to Kanban Board
+
+
+
+ );
+ }
+
+ 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}
+
+
+
+
+
+ {/* Task title */}
+
+ {currentTask.title}
+
+
+ {/* Description (non-editable) */}
+
+
+ {currentTask.description || 'No description available.'}
+
+
+
+ {/* AI Actions */}
+
+
+ setIsAiActionsExpanded(!isAiActionsExpanded)}
+ >
+ {isAiActionsExpanded ? (
+
+ ) : (
+
+ )}
+
+ AI Actions
+
+
+
+ {isAiActionsExpanded && (
+
+
+
+
+ Enter your prompt
+
+
+
+
+ {/* Show regenerate button only for main tasks, not subtasks */}
+ {!isSubtask && (
+
+ {isRegenerating ? (
+ <>
+
+ Regenerating...
+ >
+ ) : (
+ <>
+
+ Regenerate Task
+ >
+ )}
+
+ )}
+
+
+ {isAppending ? (
+ <>
+
+ {isSubtask ? 'Updating...' : 'Appending...'}
+ >
+ ) : (
+ <>
+
+ {isSubtask
+ ? 'Add Notes to Subtask'
+ : 'Append to Task'}
+ >
+ )}
+
+
+
+
+ {isSubtask ? (
+
+ Add Notes: Appends timestamped
+ implementation notes, progress updates, or findings to
+ this subtask's details
+
+ ) : (
+ <>
+
+ Regenerate: Completely rewrites the
+ task description and subtasks based on your prompt
+
+
+ Append: Adds new content to the
+ existing task description based on your prompt
+
+ >
+ )}
+
+
+
+ )}
+
+
+ {/* Implementation Details */}
+
+
+
+ setIsImplementationExpanded(!isImplementationExpanded)
+ }
+ >
+ {isImplementationExpanded ? (
+
+ ) : (
+
+ )}
+ Implementation Details
+
+ {isLoadingTaskFileData && (
+
+ )}
+
+
+ {isImplementationExpanded && (
+
+
+ {isLoadingTaskFileData ? (
+
+
+
+ Loading details...
+
+
+ ) : taskFileDataError ? (
+
+ Error loading details: {taskFileDataError}
+
+ ) : taskFileData.details ? (
+
+ ) : (
+
+ No implementation details available
+
+ )}
+
+
+ )}
+
+
+ {/* Test Strategy */}
+
+
+
+ setIsTestStrategyExpanded(!isTestStrategyExpanded)
+ }
+ >
+ {isTestStrategyExpanded ? (
+
+ ) : (
+
+ )}
+ Test Strategy
+
+ {isLoadingTaskFileData && (
+
+ )}
+
+
+ {isTestStrategyExpanded && (
+
+
+ {isLoadingTaskFileData ? (
+
+
+
+ Loading strategy...
+
+
+ ) : taskFileDataError ? (
+
+ Error loading strategy: {taskFileDataError}
+
+ ) : taskFileData.testStrategy ? (
+
+ ) : (
+
+ No test strategy available
+
+ )}
+
+
+ )}
+
+
+ {/* Subtasks section */}
+ {((currentTask.subtasks && currentTask.subtasks.length > 0) ||
+ !isSubtask) && (
+
+
+
setIsSubtasksExpanded(!isSubtasksExpanded)}
+ >
+ {isSubtasksExpanded ? (
+
+ ) : (
+
+ )}
+ Sub-issues
+
+ {currentTask.subtasks && currentTask.subtasks.length > 0 && (
+
+ {
+ currentTask.subtasks?.filter((st) => st.status === 'done')
+ .length
+ }
+ /{currentTask.subtasks?.length}
+
+ )}
+ {/* Only show add button for main tasks, not subtasks */}
+ {!isSubtask && (
+
setIsAddingSubtask(true)}
+ title="Add subtask"
+ >
+
+
+ )}
+
+
+ {isSubtasksExpanded && (
+
+ {/* Add Subtask Form */}
+ {isAddingSubtask && (
+
+
+ Add New Subtask
+
+
+
+
+ Title*
+
+ setNewSubtaskTitle(e.target.value)}
+ className="w-full px-3 py-2 text-sm bg-vscode-input-background border border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 rounded focus:border-vscode-focusBorder focus:ring-1 focus:ring-vscode-focusBorder"
+ disabled={isSubmittingSubtask}
+ autoFocus
+ />
+
+
+
+
+ Description (Optional)
+
+
+
+
+
+ {isSubmittingSubtask ? (
+ <>
+
+ Adding...
+ >
+ ) : (
+ <>
+
+ Add Subtask
+ >
+ )}
+
+
+
+ Cancel
+
+
+
+
+ )}
+
+ {currentTask.subtasks?.map((subtask, index) => {
+ const subtaskId = `${currentTask.id}.${index + 1}`;
+ const getStatusDotColor = (status: string) => {
+ switch (status) {
+ case 'pending':
+ return '#9ca3af'; // gray-400
+ case 'in-progress':
+ return '#f59e0b'; // amber-500
+ case 'review':
+ return '#3b82f6'; // blue-500
+ case 'done':
+ return '#22c55e'; // green-500
+ case 'deferred':
+ return '#ef4444'; // red-500
+ default:
+ return '#9ca3af';
+ }
+ };
+ const getSubtaskStatusColors = (status: string) => {
+ switch (status) {
+ case 'pending':
+ return {
+ backgroundColor: 'rgba(156, 163, 175, 0.2)',
+ color: 'var(--vscode-foreground)',
+ borderColor: 'rgba(156, 163, 175, 0.4)'
+ };
+ case 'in-progress':
+ return {
+ backgroundColor: 'rgba(245, 158, 11, 0.2)',
+ color: '#d97706',
+ borderColor: 'rgba(245, 158, 11, 0.4)'
+ };
+ case 'review':
+ return {
+ backgroundColor: 'rgba(59, 130, 246, 0.2)',
+ color: '#2563eb',
+ borderColor: 'rgba(59, 130, 246, 0.4)'
+ };
+ case 'done':
+ return {
+ backgroundColor: 'rgba(34, 197, 94, 0.2)',
+ color: '#16a34a',
+ borderColor: 'rgba(34, 197, 94, 0.4)'
+ };
+ case 'deferred':
+ return {
+ backgroundColor: 'rgba(239, 68, 68, 0.2)',
+ color: '#dc2626',
+ 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)'
+ };
+ }
+ };
+
+ return (
+
onNavigateToTask(subtaskId)}
+ >
+
+
+ {subtask.title}
+
+
+ {subtask.status === 'pending'
+ ? 'todo'
+ : subtask.status}
+
+
+ );
+ })}
+
+ )}
+
+ )}
+
+
+ {/* Right column - Properties sidebar (1/3 width) */}
+
+
+
+
+
+ Properties
+
+
+
+
+ {/* Status */}
+
+
+ Status
+
+
+ handleStatusChange(
+ e.target.value as TaskMasterTask['status']
+ )
+ }
+ className="border rounded-md px-3 py-1 text-sm font-medium focus:ring-1 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
+ style={{
+ backgroundColor:
+ currentTask.status === 'pending'
+ ? 'rgba(156, 163, 175, 0.2)'
+ : currentTask.status === 'in-progress'
+ ? 'rgba(245, 158, 11, 0.2)'
+ : currentTask.status === 'review'
+ ? 'rgba(59, 130, 246, 0.2)'
+ : currentTask.status === 'done'
+ ? 'rgba(34, 197, 94, 0.2)'
+ : currentTask.status === 'deferred'
+ ? 'rgba(239, 68, 68, 0.2)'
+ : 'var(--vscode-input-background)',
+ color:
+ currentTask.status === 'pending'
+ ? 'var(--vscode-foreground)'
+ : currentTask.status === 'in-progress'
+ ? '#d97706'
+ : currentTask.status === 'review'
+ ? '#2563eb'
+ : currentTask.status === 'done'
+ ? '#16a34a'
+ : currentTask.status === 'deferred'
+ ? '#dc2626'
+ : 'var(--vscode-foreground)',
+ borderColor:
+ currentTask.status === 'pending'
+ ? 'rgba(156, 163, 175, 0.4)'
+ : currentTask.status === 'in-progress'
+ ? 'rgba(245, 158, 11, 0.4)'
+ : currentTask.status === 'review'
+ ? 'rgba(59, 130, 246, 0.4)'
+ : currentTask.status === 'done'
+ ? 'rgba(34, 197, 94, 0.4)'
+ : currentTask.status === 'deferred'
+ ? 'rgba(239, 68, 68, 0.4)'
+ : 'var(--vscode-input-border)'
+ }}
+ >
+ To do
+ In Progress
+ Review
+ Done
+ Deferred
+
+
+
+ {/* Priority */}
+
+
+ {/* Complexity Score */}
+
+
+ Complexity Score
+
+ {isLoadingComplexity ? (
+
+
+
+ Loading...
+
+
+ ) : displayComplexityScore !== undefined ? (
+
+
+ {displayComplexityScore}/10
+
+
= 7
+ ? 'bg-red-500/20'
+ : displayComplexityScore >= 4
+ ? 'bg-yellow-500/20'
+ : 'bg-green-500/20'
+ }`}
+ >
+
= 7
+ ? 'bg-red-500'
+ : displayComplexityScore >= 4
+ ? 'bg-yellow-500'
+ : 'bg-green-500'
+ }`}
+ style={{
+ width: `${(displayComplexityScore || 0) * 10}%`
+ }}
+ />
+
+
+ ) : currentTask?.status === 'done' ||
+ currentTask?.status === 'deferred' ||
+ currentTask?.status === 'review' ? (
+
+ N/A
+
+ ) : (
+ <>
+
+ No complexity score available
+
+
+ handleRunComplexityAnalysis()}
+ variant="outline"
+ size="sm"
+ className="text-xs"
+ disabled={isRegenerating || isAppending}
+ >
+ Run Complexity Analysis
+
+
+ >
+ )}
+
+
+
+
+ {/* Dependencies */}
+ {currentTask.dependencies &&
+ currentTask.dependencies.length > 0 && (
+ <>
+
+
+ Dependencies
+
+
+ {currentTask.dependencies.map((depId) => {
+ const depTask = tasks.find((t) => t.id === depId);
+ const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`;
+ const truncatedTitle =
+ fullTitle.length > 40
+ ? fullTitle.substring(0, 37) + '...'
+ : fullTitle;
+ return (
+
handleDependencyClick(depId)}
+ title={fullTitle}
+ >
+ {truncatedTitle}
+
+ );
+ })}
+
+
+ >
+ )}
+
+ {/* Divider after Dependencies */}
+ {currentTask.dependencies &&
+ currentTask.dependencies.length > 0 && (
+
+ )}
+
+
+
+
+
+ );
};
-export default TaskDetailsView;
\ No newline at end of file
+export default TaskDetailsView;
diff --git a/apps/extension/src/components/ui/badge.tsx b/apps/extension/src/components/ui/badge.tsx
index 02054139..ed514277 100644
--- a/apps/extension/src/components/ui/badge.tsx
+++ b/apps/extension/src/components/ui/badge.tsx
@@ -1,46 +1,46 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
const badgeVariants = cva(
- "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
- destructive:
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
+ 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
+ destructive:
+ 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
+ }
+ },
+ defaultVariants: {
+ variant: 'default'
+ }
+ }
+);
function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps
& { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span"
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'span'> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'span';
- return (
-
- )
+ return (
+
+ );
}
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };
diff --git a/apps/extension/src/components/ui/breadcrumb.tsx b/apps/extension/src/components/ui/breadcrumb.tsx
index eb88f321..bf29c79e 100644
--- a/apps/extension/src/components/ui/breadcrumb.tsx
+++ b/apps/extension/src/components/ui/breadcrumb.tsx
@@ -1,109 +1,109 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { ChevronRight, MoreHorizontal } from "lucide-react"
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { ChevronRight, MoreHorizontal } from 'lucide-react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
-function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
- return
+function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
+ return ;
}
-function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
- return (
-
- )
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ );
}
-function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
- return (
-
- )
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ );
}
function BreadcrumbLink({
- asChild,
- className,
- ...props
-}: React.ComponentProps<"a"> & {
- asChild?: boolean
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean;
}) {
- const Comp = asChild ? Slot : "a"
+ const Comp = asChild ? Slot : 'a';
- return (
-
- )
+ return (
+
+ );
}
-function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
- return (
-
- )
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ );
}
function BreadcrumbSeparator({
- children,
- className,
- ...props
-}: React.ComponentProps<"li">) {
- return (
- svg]:size-3.5", className)}
- {...props}
- >
- {children ?? }
-
- )
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ );
}
function BreadcrumbEllipsis({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
-
- More
-
- )
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+
+ More
+
+ );
}
export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis
+};
diff --git a/apps/extension/src/components/ui/button.tsx b/apps/extension/src/components/ui/button.tsx
index b1d4a574..1cbff004 100644
--- a/apps/extension/src/components/ui/button.tsx
+++ b/apps/extension/src/components/ui/button.tsx
@@ -1,59 +1,59 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
-import { cn } from "../../lib/utils"
+import { cn } from '../../lib/utils';
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
- destructive:
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
- ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
- icon: "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+ secondary:
+ 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
+ ghost:
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ link: 'text-primary underline-offset-4 hover:underline'
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9'
+ }
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default'
+ }
+ }
+);
function Button({
- className,
- variant,
- size,
- asChild = false,
- ...props
-}: React.ComponentProps<"button"> &
- VariantProps & {
- asChild?: boolean
- }) {
- const Comp = asChild ? Slot : "button"
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : 'button';
- return (
-
- )
+ return (
+
+ );
}
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/apps/extension/src/components/ui/card.tsx b/apps/extension/src/components/ui/card.tsx
index d05bbc6c..3f6b135a 100644
--- a/apps/extension/src/components/ui/card.tsx
+++ b/apps/extension/src/components/ui/card.tsx
@@ -1,92 +1,92 @@
-import * as React from "react"
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
-function Card({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function Card({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
}
export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent
+};
diff --git a/apps/extension/src/components/ui/collapsible.tsx b/apps/extension/src/components/ui/collapsible.tsx
index 77f86bed..7b08e663 100644
--- a/apps/extension/src/components/ui/collapsible.tsx
+++ b/apps/extension/src/components/ui/collapsible.tsx
@@ -1,31 +1,31 @@
-import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
function Collapsible({
- ...props
+ ...props
}: React.ComponentProps) {
- return
+ return ;
}
function CollapsibleTrigger({
- ...props
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
function CollapsibleContent({
- ...props
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
-export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/apps/extension/src/components/ui/dropdown-menu.tsx b/apps/extension/src/components/ui/dropdown-menu.tsx
index ec51e9cc..5ec3b852 100644
--- a/apps/extension/src/components/ui/dropdown-menu.tsx
+++ b/apps/extension/src/components/ui/dropdown-menu.tsx
@@ -1,257 +1,257 @@
-"use client"
+'use client';
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+import * as React from 'react';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
function DropdownMenu({
- ...props
+ ...props
}: React.ComponentProps) {
- return
+ return ;
}
function DropdownMenuPortal({
- ...props
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuTrigger({
- ...props
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuContent({
- className,
- sideOffset = 4,
- ...props
+ className,
+ sideOffset = 4,
+ ...props
}: React.ComponentProps) {
- return (
-
-
-
- )
+ return (
+
+
+
+ );
}
function DropdownMenuGroup({
- ...props
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuItem({
- className,
- inset,
- variant = "default",
- ...props
+ className,
+ inset,
+ variant = 'default',
+ ...props
}: React.ComponentProps & {
- inset?: boolean
- variant?: "default" | "destructive"
+ inset?: boolean;
+ variant?: 'default' | 'destructive';
}) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuCheckboxItem({
- className,
- children,
- checked,
- ...props
+ className,
+ children,
+ checked,
+ ...props
}: React.ComponentProps) {
- return (
-
-
-
-
-
-
- {children}
-
- )
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
}
function DropdownMenuRadioGroup({
- ...props
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuRadioItem({
- className,
- children,
- ...props
+ className,
+ children,
+ ...props
}: React.ComponentProps) {
- return (
-
-
-
-
-
-
- {children}
-
- )
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
}
function DropdownMenuLabel({
- className,
- inset,
- ...props
+ className,
+ inset,
+ ...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuSeparator({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
function DropdownMenuShortcut({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
- )
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+ );
}
function DropdownMenuSub({
- ...props
+ ...props
}: React.ComponentProps) {
- return
+ return ;
}
function DropdownMenuSubTrigger({
- className,
- inset,
- children,
- ...props
+ className,
+ inset,
+ children,
+ ...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
- return (
-
- {children}
-
-
- )
+ return (
+
+ {children}
+
+
+ );
}
function DropdownMenuSubContent({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
export {
- DropdownMenu,
- DropdownMenuPortal,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuLabel,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuSub,
- DropdownMenuSubTrigger,
- DropdownMenuSubContent,
-}
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent
+};
diff --git a/apps/extension/src/components/ui/label.tsx b/apps/extension/src/components/ui/label.tsx
index ef7133a7..0e5eca60 100644
--- a/apps/extension/src/components/ui/label.tsx
+++ b/apps/extension/src/components/ui/label.tsx
@@ -1,22 +1,22 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
function Label({
- className,
- ...props
+ className,
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
-export { Label }
+export { Label };
diff --git a/apps/extension/src/components/ui/scroll-area.tsx b/apps/extension/src/components/ui/scroll-area.tsx
index 9376f594..9bd8295c 100644
--- a/apps/extension/src/components/ui/scroll-area.tsx
+++ b/apps/extension/src/components/ui/scroll-area.tsx
@@ -1,56 +1,56 @@
-import * as React from "react"
-import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+import * as React from 'react';
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
function ScrollArea({
- className,
- children,
- ...props
+ className,
+ children,
+ ...props
}: React.ComponentProps) {
- return (
-
-
- {children}
-
-
-
-
- )
+ return (
+
+
+ {children}
+
+
+
+
+ );
}
function ScrollBar({
- className,
- orientation = "vertical",
- ...props
+ className,
+ orientation = 'vertical',
+ ...props
}: React.ComponentProps) {
- return (
-
-
-
- )
+ return (
+
+
+
+ );
}
-export { ScrollArea, ScrollBar }
+export { ScrollArea, ScrollBar };
diff --git a/apps/extension/src/components/ui/separator.tsx b/apps/extension/src/components/ui/separator.tsx
index 275381ca..87b57392 100644
--- a/apps/extension/src/components/ui/separator.tsx
+++ b/apps/extension/src/components/ui/separator.tsx
@@ -1,28 +1,28 @@
-"use client"
+'use client';
-import * as React from "react"
-import * as SeparatorPrimitive from "@radix-ui/react-separator"
+import * as React from 'react';
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
function Separator({
- className,
- orientation = "horizontal",
- decorative = true,
- ...props
+ className,
+ orientation = 'horizontal',
+ decorative = true,
+ ...props
}: React.ComponentProps) {
- return (
-
- )
+ return (
+
+ );
}
-export { Separator }
+export { Separator };
diff --git a/apps/extension/src/components/ui/shadcn-io/kanban/index.tsx b/apps/extension/src/components/ui/shadcn-io/kanban/index.tsx
index 4c1dfcfb..fb46deb6 100644
--- a/apps/extension/src/components/ui/shadcn-io/kanban/index.tsx
+++ b/apps/extension/src/components/ui/shadcn-io/kanban/index.tsx
@@ -4,15 +4,15 @@ import React from 'react';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import {
- DndContext,
- DragOverlay,
- MouseSensor,
- TouchSensor,
- rectIntersection,
- useDraggable,
- useDroppable,
- useSensor,
- useSensors,
+ DndContext,
+ DragOverlay,
+ MouseSensor,
+ TouchSensor,
+ rectIntersection,
+ useDraggable,
+ useDroppable,
+ useSensor,
+ useSensors
} from '@dnd-kit/core';
import type { DragEndEvent } from '@dnd-kit/core';
import type { ReactNode } from 'react';
@@ -20,167 +20,166 @@ import type { ReactNode } from 'react';
export type { DragEndEvent } from '@dnd-kit/core';
export type Status = {
- id: string;
- name: string;
- color: string;
+ id: string;
+ name: string;
+ color: string;
};
export type Feature = {
- id: string;
- name: string;
- startAt: Date;
- endAt: Date;
- status: Status;
+ id: string;
+ name: string;
+ startAt: Date;
+ endAt: Date;
+ status: Status;
};
export type KanbanBoardProps = {
- id: Status['id'];
- children: ReactNode;
- className?: string;
+ id: Status['id'];
+ children: ReactNode;
+ className?: string;
};
export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => {
- const { isOver, setNodeRef } = useDroppable({ id });
+ const { isOver, setNodeRef } = useDroppable({ id });
- return (
-
- {children}
-
- );
+ return (
+
+ {children}
+
+ );
};
export type KanbanCardProps = Pick & {
- index: number;
- parent: string;
- children?: ReactNode;
- className?: string;
- onClick?: (event: React.MouseEvent) => void;
- onDoubleClick?: (event: React.MouseEvent) => void;
+ index: number;
+ parent: string;
+ children?: ReactNode;
+ className?: string;
+ onClick?: (event: React.MouseEvent) => void;
+ onDoubleClick?: (event: React.MouseEvent) => void;
};
export const KanbanCard = ({
- id,
- name,
- index,
- parent,
- children,
- className,
- onClick,
- onDoubleClick,
+ id,
+ name,
+ index,
+ parent,
+ children,
+ className,
+ onClick,
+ onDoubleClick
}: KanbanCardProps) => {
- const { attributes, listeners, setNodeRef, transform, isDragging } =
- useDraggable({
- id,
- data: { index, parent },
- });
+ const { attributes, listeners, setNodeRef, transform, isDragging } =
+ useDraggable({
+ id,
+ data: { index, parent }
+ });
-
-
- return (
- !isDragging && onClick?.(e)}
- onDoubleClick={onDoubleClick}
- ref={setNodeRef}
- >
- {children ?? {name}
}
-
- );
+ return (
+ !isDragging && onClick?.(e)}
+ onDoubleClick={onDoubleClick}
+ ref={setNodeRef}
+ >
+ {children ?? {name}
}
+
+ );
};
export type KanbanCardsProps = {
- children: ReactNode;
- className?: string;
+ children: ReactNode;
+ className?: string;
};
export const KanbanCards = ({ children, className }: KanbanCardsProps) => (
- {children}
+ {children}
);
export type KanbanHeaderProps =
- | {
- children: ReactNode;
- }
- | {
- name: Status['name'];
- color: Status['color'];
- className?: string;
- };
+ | {
+ children: ReactNode;
+ }
+ | {
+ name: Status['name'];
+ color: Status['color'];
+ className?: string;
+ };
export const KanbanHeader = (props: KanbanHeaderProps) =>
- 'children' in props ? (
- props.children
- ) : (
-
- );
+ 'children' in props ? (
+ props.children
+ ) : (
+
+ );
export type KanbanProviderProps = {
- children: ReactNode;
- onDragEnd: (event: DragEndEvent) => void;
- onDragStart?: (event: DragEndEvent) => void;
- className?: string;
- dragOverlay?: ReactNode;
+ children: ReactNode;
+ onDragEnd: (event: DragEndEvent) => void;
+ onDragStart?: (event: DragEndEvent) => void;
+ className?: string;
+ dragOverlay?: ReactNode;
};
export const KanbanProvider = ({
- children,
- onDragEnd,
- onDragStart,
- className,
- dragOverlay,
+ children,
+ onDragEnd,
+ onDragStart,
+ className,
+ dragOverlay
}: KanbanProviderProps) => {
- // Configure sensors with activation constraints to prevent accidental drags
- const sensors = useSensors(
- // Only start a drag if you've moved more than 8px
- useSensor(MouseSensor, {
- activationConstraint: { distance: 8 },
- }),
- // On touch devices, require a short press + small move
- useSensor(TouchSensor, {
- activationConstraint: { delay: 150, tolerance: 5 },
- })
- );
+ // Configure sensors with activation constraints to prevent accidental drags
+ const sensors = useSensors(
+ // Only start a drag if you've moved more than 8px
+ useSensor(MouseSensor, {
+ activationConstraint: { distance: 8 }
+ }),
+ // On touch devices, require a short press + small move
+ useSensor(TouchSensor, {
+ activationConstraint: { delay: 150, tolerance: 5 }
+ })
+ );
- return (
-
-
- {children}
-
-
- {dragOverlay}
-
-
- );
+ return (
+
+
+ {children}
+
+ {dragOverlay}
+
+ );
};
diff --git a/apps/extension/src/components/ui/textarea.tsx b/apps/extension/src/components/ui/textarea.tsx
index 7f21b5e7..234446f2 100644
--- a/apps/extension/src/components/ui/textarea.tsx
+++ b/apps/extension/src/components/ui/textarea.tsx
@@ -1,18 +1,18 @@
-import * as React from "react"
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
-function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
- return (
-
- )
+function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+ return (
+
+ );
}
-export { Textarea }
+export { Textarea };
diff --git a/apps/extension/src/extension.ts b/apps/extension/src/extension.ts
index a9f3ce70..1fbf0d96 100644
--- a/apps/extension/src/extension.ts
+++ b/apps/extension/src/extension.ts
@@ -5,19 +5,22 @@ import * as path from 'path';
import * as fs from 'fs';
// Restore full imports for MCP and utilities
-import { MCPClientManager, createMCPConfigFromSettings } from './utils/mcpClient';
+import {
+ MCPClientManager,
+ createMCPConfigFromSettings
+} from './utils/mcpClient';
import { ConfigManager } from './utils/configManager';
import { TaskMasterApi } from './utils/taskMasterApi';
-import {
- ErrorHandler,
- getErrorHandler,
- MCPConnectionError,
- TaskLoadingError,
- NetworkError,
+import {
+ ErrorHandler,
+ getErrorHandler,
+ MCPConnectionError,
+ TaskLoadingError,
+ NetworkError,
UIRenderingError,
ErrorCategory,
ErrorSeverity,
- createErrorContext
+ createErrorContext
} from './utils/errorHandler';
import { getToastDuration } from './utils/notificationPreferences';
import { parseTaskFileData } from './utils/taskFileReader';
@@ -62,7 +65,7 @@ let pollingState: PollingState = {
maxErrors: 5,
// Adaptive frequency settings
baseInterval: 5000, // 5 seconds base
- minInterval: 2000, // 2 seconds minimum
+ minInterval: 2000, // 2 seconds minimum
maxInterval: 60000, // 1 minute maximum
consecutiveNoChanges: 0,
changeDetectionWindow: [], // Track recent change activity
@@ -79,18 +82,24 @@ let pollingState: PollingState = {
async function initializeMCPComponents(context: vscode.ExtensionContext) {
try {
console.log('🔄 Initializing MCP components...');
- console.log('🔍 DEBUGGING: initializeMCPComponents started at', new Date().toISOString());
-
+ console.log(
+ '🔍 DEBUGGING: initializeMCPComponents started at',
+ new Date().toISOString()
+ );
+
// Initialize ConfigManager singleton
configManager = ConfigManager.getInstance();
-
+
// Get MCP configuration from VS Code settings
const mcpConfig = createMCPConfigFromSettings();
-
+
// Initialize MCP client
- console.log('🔍 DEBUGGING: About to create MCPClientManager with config:', mcpConfig);
+ console.log(
+ '🔍 DEBUGGING: About to create MCPClientManager with config:',
+ mcpConfig
+ );
mcpClient = new MCPClientManager(mcpConfig);
-
+
// Initialize TaskMaster API first (even without connection)
taskMasterApi = new TaskMasterApi(mcpClient, {
timeout: 30000,
@@ -98,50 +107,67 @@ async function initializeMCPComponents(context: vscode.ExtensionContext) {
cacheDuration: 5 * 60 * 1000, // 5 minutes
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
-
+
// Try to connect to MCP server
console.log('🔗 Connecting to Task Master MCP server...');
try {
await mcpClient.connect();
-
+
// Test connection
const connectionTest = await taskMasterApi.testConnection();
if (connectionTest.success && connectionTest.data) {
console.log('✅ Task Master MCP connection established');
- vscode.window.showInformationMessage('Task Master connected successfully!');
+ vscode.window.showInformationMessage(
+ 'Task Master connected successfully!'
+ );
} else {
throw new Error(connectionTest.error || 'Connection test failed');
}
} catch (connectionError) {
console.error('❌ Task Master MCP connection failed:', connectionError);
- console.error('Connection error details:', {
- message: connectionError instanceof Error ? connectionError.message : 'Unknown error',
- stack: connectionError instanceof Error ? connectionError.stack : undefined,
+ console.error('Connection error details:', {
+ message:
+ connectionError instanceof Error
+ ? connectionError.message
+ : 'Unknown error',
+ stack:
+ connectionError instanceof Error ? connectionError.stack : undefined,
code: (connectionError as any)?.code,
errno: (connectionError as any)?.errno,
syscall: (connectionError as any)?.syscall
});
- const errorMessage = connectionError instanceof Error ? connectionError.message : 'Unknown connection error';
-
+ const errorMessage =
+ connectionError instanceof Error
+ ? connectionError.message
+ : 'Unknown connection error';
+
if (errorMessage.includes('ENOENT') && errorMessage.includes('npx')) {
- vscode.window.showWarningMessage(
- 'Task Master: npx not found. Please ensure Node.js is installed and accessible to VS Code. ' +
- 'You may need to restart VS Code after installing Node.js.',
- 'Open Settings'
- ).then((action) => {
- if (action === 'Open Settings') {
- vscode.commands.executeCommand('workbench.action.openSettings', '@ext:taskr taskmaster');
- }
- });
+ vscode.window
+ .showWarningMessage(
+ 'Task Master: npx not found. Please ensure Node.js is installed and accessible to VS Code. ' +
+ 'You may need to restart VS Code after installing Node.js.',
+ 'Open Settings'
+ )
+ .then((action) => {
+ if (action === 'Open Settings') {
+ vscode.commands.executeCommand(
+ 'workbench.action.openSettings',
+ '@ext:taskr taskmaster'
+ );
+ }
+ });
} else {
- vscode.window.showWarningMessage(`Task Master connection failed: ${errorMessage}`);
+ vscode.window.showWarningMessage(
+ `Task Master connection failed: ${errorMessage}`
+ );
}
-
+
// Initialize in offline mode
pollingState.isOfflineMode = true;
- console.log('📴 Starting in offline mode - some features will be unavailable');
+ console.log(
+ '📴 Starting in offline mode - some features will be unavailable'
+ );
}
-
} catch (error) {
// Use enhanced network error handling for polling
handleNetworkError(error);
@@ -186,7 +212,7 @@ async function pollForUpdates(): Promise {
try {
console.log('📡 Polling for task updates...');
-
+
const tasksResult = await taskMasterApi.getTasks({
withSubtasks: true,
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
@@ -194,20 +220,20 @@ async function pollForUpdates(): Promise {
if (tasksResult.success && tasksResult.data) {
const hasChanges = detectTaskChanges(tasksResult.data);
-
+
if (hasChanges) {
console.log('📋 Task changes detected, notifying webviews');
-
+
// Track change for adaptive frequency
pollingState.changeDetectionWindow.push(Date.now());
pollingState.consecutiveNoChanges = 0;
pollingState.lastUpdateTime = Date.now();
-
+
// Update cached data
pollingState.lastTaskData = tasksResult.data;
-
+
// Notify all active webviews
- activeWebviewPanels.forEach(panel => {
+ activeWebviewPanels.forEach((panel) => {
panel.webview.postMessage({
type: 'tasksUpdated',
data: tasksResult.data,
@@ -218,28 +244,26 @@ async function pollForUpdates(): Promise {
console.log('📋 No task changes detected');
pollingState.consecutiveNoChanges++;
}
-
+
// Adjust polling frequency based on activity
adjustPollingFrequency();
-
+
// Reset error count on success
pollingState.errorCount = 0;
-
+
// Track successful connection
pollingState.lastSuccessfulConnection = Date.now();
pollingState.reconnectAttempts = 0;
-
+
// If we were in offline mode, notify that we're back online
if (pollingState.isOfflineMode) {
pollingState.isOfflineMode = false;
notifyConnectionStatus('online', 'Connected');
console.log('✅ Reconnected successfully from offline mode');
}
-
} else {
throw new Error(tasksResult.error || 'Failed to fetch tasks');
}
-
} catch (error) {
// Use enhanced network error handling for polling
handleNetworkError(error);
@@ -261,7 +285,9 @@ function detectTaskChanges(newTasks: any[]): boolean {
// Deep comparison of task data
try {
const newTasksStr = JSON.stringify(sortTasksForComparison(newTasks));
- const oldTasksStr = JSON.stringify(sortTasksForComparison(pollingState.lastTaskData));
+ const oldTasksStr = JSON.stringify(
+ sortTasksForComparison(pollingState.lastTaskData)
+ );
return newTasksStr !== oldTasksStr;
} catch (error) {
console.warn('⚠️ Error comparing tasks, assuming changed:', error);
@@ -271,37 +297,51 @@ function detectTaskChanges(newTasks: any[]): boolean {
function sortTasksForComparison(tasks: any[]): any[] {
// Sort tasks by ID for consistent comparison
- return tasks.map(task => ({
- ...task,
- dependencies: task.dependencies ? [...task.dependencies].sort() : [],
- subtasks: task.subtasks ? task.subtasks.map((st: any) => ({
- ...st,
- dependencies: st.dependencies ? [...st.dependencies].sort() : []
- })).sort((a: any, b: any) => a.id - b.id) : []
- })).sort((a, b) => String(a.id).localeCompare(String(b.id)));
+ return tasks
+ .map((task) => ({
+ ...task,
+ dependencies: task.dependencies ? [...task.dependencies].sort() : [],
+ subtasks: task.subtasks
+ ? task.subtasks
+ .map((st: any) => ({
+ ...st,
+ dependencies: st.dependencies ? [...st.dependencies].sort() : []
+ }))
+ .sort((a: any, b: any) => a.id - b.id)
+ : []
+ }))
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)));
}
// Adaptive polling frequency management
function adjustPollingFrequency(): void {
const now = Date.now();
const windowSize = 5; // Track last 5 polling intervals
-
+
// Clean old entries from detection window (keep last 5 minutes)
- const fiveMinutesAgo = now - (5 * 60 * 1000);
- pollingState.changeDetectionWindow = pollingState.changeDetectionWindow.filter(
- timestamp => timestamp > fiveMinutesAgo
- );
-
+ const fiveMinutesAgo = now - 5 * 60 * 1000;
+ pollingState.changeDetectionWindow =
+ pollingState.changeDetectionWindow.filter(
+ (timestamp) => timestamp > fiveMinutesAgo
+ );
+
// Calculate change frequency in the recent window
const recentChanges = pollingState.changeDetectionWindow.length;
- const windowDuration = Math.min(5 * 60 * 1000, now - (pollingState.changeDetectionWindow[0] || now));
- const changesPerMinute = windowDuration > 0 ? (recentChanges / windowDuration) * 60 * 1000 : 0;
-
+ const windowDuration = Math.min(
+ 5 * 60 * 1000,
+ now - (pollingState.changeDetectionWindow[0] || now)
+ );
+ const changesPerMinute =
+ windowDuration > 0 ? (recentChanges / windowDuration) * 60 * 1000 : 0;
+
let newInterval = pollingState.baseInterval;
-
+
if (changesPerMinute > 2) {
// High activity: poll more frequently
- newInterval = Math.max(pollingState.minInterval, pollingState.baseInterval * 0.5);
+ newInterval = Math.max(
+ pollingState.minInterval,
+ pollingState.baseInterval * 0.5
+ );
console.log('📈 High activity detected, increasing polling frequency');
} else if (changesPerMinute > 0.5) {
// Moderate activity: use base interval
@@ -309,16 +349,29 @@ function adjustPollingFrequency(): void {
console.log('📊 Moderate activity, using base polling interval');
} else if (pollingState.consecutiveNoChanges > 3) {
// Low activity: reduce polling frequency with exponential backoff
- const backoffMultiplier = Math.min(4, Math.pow(1.5, pollingState.consecutiveNoChanges - 3));
- newInterval = Math.min(pollingState.maxInterval, pollingState.baseInterval * backoffMultiplier);
- console.log(`📉 Low activity detected (${pollingState.consecutiveNoChanges} no-change cycles), reducing polling frequency`);
+ const backoffMultiplier = Math.min(
+ 4,
+ Math.pow(1.5, pollingState.consecutiveNoChanges - 3)
+ );
+ newInterval = Math.min(
+ pollingState.maxInterval,
+ pollingState.baseInterval * backoffMultiplier
+ );
+ console.log(
+ `📉 Low activity detected (${pollingState.consecutiveNoChanges} no-change cycles), reducing polling frequency`
+ );
}
-
+
// Only restart polling if interval changed significantly (>500ms difference)
- if (Math.abs(newInterval - pollingState.interval) > 500 && pollingState.isPolling) {
- console.log(`🔄 Adjusting polling interval from ${pollingState.interval}ms to ${newInterval}ms`);
+ if (
+ Math.abs(newInterval - pollingState.interval) > 500 &&
+ pollingState.isPolling
+ ) {
+ console.log(
+ `🔄 Adjusting polling interval from ${pollingState.interval}ms to ${newInterval}ms`
+ );
pollingState.interval = newInterval;
-
+
// Restart polling with new interval
if (pollingState.timer) {
clearInterval(pollingState.timer);
@@ -333,31 +386,44 @@ function adjustPollingFrequency(): void {
function handleNetworkError(error: any): void {
pollingState.errorCount++;
pollingState.reconnectAttempts++;
-
- console.error(`❌ Network error (attempt ${pollingState.reconnectAttempts}/${pollingState.maxReconnectAttempts}):`, error);
-
+
+ console.error(
+ `❌ Network error (attempt ${pollingState.reconnectAttempts}/${pollingState.maxReconnectAttempts}):`,
+ error
+ );
+
// Check if we should enter offline mode
if (pollingState.reconnectAttempts >= pollingState.maxReconnectAttempts) {
enterOfflineMode();
return;
}
-
+
// Calculate exponential backoff delay
const baseDelay = pollingState.interval;
- const backoffDelay = baseDelay * Math.pow(pollingState.reconnectBackoffMultiplier, pollingState.reconnectAttempts);
+ const backoffDelay =
+ baseDelay *
+ Math.pow(
+ pollingState.reconnectBackoffMultiplier,
+ pollingState.reconnectAttempts
+ );
const maxBackoffDelay = pollingState.maxInterval;
const finalDelay = Math.min(backoffDelay, maxBackoffDelay);
-
- console.log(`🔄 Retrying connection in ${finalDelay}ms (attempt ${pollingState.reconnectAttempts})`);
-
+
+ console.log(
+ `🔄 Retrying connection in ${finalDelay}ms (attempt ${pollingState.reconnectAttempts})`
+ );
+
// Update UI with connection status
- notifyConnectionStatus('reconnecting', `Reconnecting... (${pollingState.reconnectAttempts}/${pollingState.maxReconnectAttempts})`);
-
+ notifyConnectionStatus(
+ 'reconnecting',
+ `Reconnecting... (${pollingState.reconnectAttempts}/${pollingState.maxReconnectAttempts})`
+ );
+
// Retry with exponential backoff
if (pollingState.timer) {
clearInterval(pollingState.timer);
}
-
+
pollingState.timer = setTimeout(() => {
// Try to resume normal polling
pollingState.timer = setInterval(pollForUpdates, pollingState.interval);
@@ -366,19 +432,19 @@ function handleNetworkError(error: any): void {
function enterOfflineMode(): void {
console.warn('⚠️ Entering offline mode due to persistent connection failures');
-
+
pollingState.isOfflineMode = true;
stopPolling();
-
+
// Cache current task data for offline viewing
if (pollingState.lastTaskData) {
pollingState.cachedTaskData = [...pollingState.lastTaskData];
}
-
+
// Notify webviews about offline mode
notifyConnectionStatus('offline', 'Offline - using cached data');
-
- activeWebviewPanels.forEach(panel => {
+
+ activeWebviewPanels.forEach((panel) => {
panel.webview.postMessage({
type: 'networkOffline',
data: {
@@ -394,26 +460,29 @@ function attemptReconnection(): void {
if (!pollingState.isOfflineMode) {
return;
}
-
+
console.log('🔄 Attempting to reconnect from offline mode...');
-
+
// Reset connection state
pollingState.isOfflineMode = false;
pollingState.reconnectAttempts = 0;
pollingState.errorCount = 0;
-
+
// Notify UI about reconnection attempt
notifyConnectionStatus('reconnecting', 'Attempting to reconnect...');
-
+
// Try to restart polling
- startPolling().catch(error => {
+ startPolling().catch((error) => {
console.error('Failed to reconnect:', error);
enterOfflineMode();
});
}
-function notifyConnectionStatus(status: 'online' | 'offline' | 'reconnecting', message: string): void {
- activeWebviewPanels.forEach(panel => {
+function notifyConnectionStatus(
+ status: 'online' | 'offline' | 'reconnecting',
+ message: string
+): void {
+ activeWebviewPanels.forEach((panel) => {
panel.webview.postMessage({
type: 'connectionStatusUpdate',
data: {
@@ -429,56 +498,79 @@ function notifyConnectionStatus(status: 'online' | 'offline' | 'reconnecting', m
}
// Error handling wrapper functions
-async function handleExtensionError(error: Error | unknown, operation: string, context?: Record): Promise {
+async function handleExtensionError(
+ error: Error | unknown,
+ operation: string,
+ context?: Record
+): Promise {
const errorContext = createErrorContext(error, operation, {
category: ErrorCategory.EXTENSION_HOST,
...context
});
-
+
console.error(`Extension Error [${operation}]:`, error);
- await errorHandler.handleError(error instanceof Error ? error : new Error(String(error)), context);
+ await errorHandler.handleError(
+ error instanceof Error ? error : new Error(String(error)),
+ context
+ );
}
-async function handleMCPError(error: Error | unknown, operation: string, context?: Record): Promise {
+async function handleMCPError(
+ error: Error | unknown,
+ operation: string,
+ context?: Record
+): Promise {
const mcpError = new MCPConnectionError(
error instanceof Error ? error.message : String(error),
'MCP_OPERATION_FAILED',
context
);
-
+
console.error(`MCP Error [${operation}]:`, error);
await errorHandler.handleError(mcpError, context);
}
-async function handleTaskLoadingError(error: Error | unknown, operation: string, context?: Record): Promise {
+async function handleTaskLoadingError(
+ error: Error | unknown,
+ operation: string,
+ context?: Record
+): Promise {
const taskError = new TaskLoadingError(
error instanceof Error ? error.message : String(error),
'TASK_OPERATION_FAILED',
context
);
-
+
console.error(`Task Loading Error [${operation}]:`, error);
await errorHandler.handleError(taskError, context);
}
-async function handleNetworkConnectionError(error: Error | unknown, operation: string, context?: Record): Promise {
+async function handleNetworkConnectionError(
+ error: Error | unknown,
+ operation: string,
+ context?: Record
+): Promise {
const networkError = new NetworkError(
error instanceof Error ? error.message : String(error),
'NETWORK_OPERATION_FAILED',
context
);
-
+
console.error(`Network Error [${operation}]:`, error);
await errorHandler.handleError(networkError, context);
}
-async function handleUIError(error: Error | unknown, operation: string, context?: Record): Promise {
+async function handleUIError(
+ error: Error | unknown,
+ operation: string,
+ context?: Record
+): Promise {
const uiError = new UIRenderingError(
error instanceof Error ? error.message : String(error),
'UI_OPERATION_FAILED',
context
);
-
+
console.error(`UI Error [${operation}]:`, error);
await errorHandler.handleError(uiError, context);
}
@@ -488,15 +580,18 @@ async function handleUIError(error: Error | unknown, operation: string, context?
export function activate(context: vscode.ExtensionContext) {
console.log('🎉 Task Master Kanban extension is now active!');
console.log('🎉 Extension context:', context);
- console.log('🔍 DEBUGGING: Extension activation started at', new Date().toISOString());
-
+ console.log(
+ '🔍 DEBUGGING: Extension activation started at',
+ new Date().toISOString()
+ );
+
// Initialize error handler
errorHandler = getErrorHandler();
-
+
// Set up error event listener for webview notifications
errorHandler.onError((errorDetails) => {
// Notify webviews about errors for toast notifications
- activeWebviewPanels.forEach(panel => {
+ activeWebviewPanels.forEach((panel) => {
panel.webview.postMessage({
type: 'errorNotification',
data: {
@@ -510,64 +605,70 @@ export function activate(context: vscode.ExtensionContext) {
});
});
});
-
+
// Initialize MCP components
initializeMCPComponents(context);
-
+
// Register command to show Kanban board with webview
- const showKanbanCommand = vscode.commands.registerCommand('taskr.showKanbanBoard', async () => {
- console.log('🎯 Show Kanban command executed!');
-
- // Check if panel already exists
- const existingPanel = activeWebviewPanels.find(panel => panel.title === 'Task Master Kanban');
- if (existingPanel) {
- existingPanel.reveal(vscode.ViewColumn.One);
- return;
- }
+ const showKanbanCommand = vscode.commands.registerCommand(
+ 'taskr.showKanbanBoard',
+ async () => {
+ console.log('🎯 Show Kanban command executed!');
- // Create webview panel
- const panel = vscode.window.createWebviewPanel(
- 'taskrKanban',
- 'Task Master Kanban',
- vscode.ViewColumn.One,
- {
- enableScripts: true,
- retainContextWhenHidden: true,
- localResourceRoots: [
- vscode.Uri.joinPath(context.extensionUri, 'dist')
- ]
+ // Check if panel already exists
+ const existingPanel = activeWebviewPanels.find(
+ (panel) => panel.title === 'Task Master Kanban'
+ );
+ if (existingPanel) {
+ existingPanel.reveal(vscode.ViewColumn.One);
+ return;
}
- );
- // Add to active panels
- activeWebviewPanels.push(panel);
+ // Create webview panel
+ const panel = vscode.window.createWebviewPanel(
+ 'taskrKanban',
+ 'Task Master Kanban',
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ localResourceRoots: [
+ vscode.Uri.joinPath(context.extensionUri, 'dist')
+ ]
+ }
+ );
- // Start polling if this is the first panel
- if (activeWebviewPanels.length === 1) {
- await startPolling();
- }
+ // Add to active panels
+ activeWebviewPanels.push(panel);
- // Handle panel disposal
- panel.onDidDispose(() => {
- const index = activeWebviewPanels.findIndex(p => p === panel);
- if (index !== -1) {
- activeWebviewPanels.splice(index, 1);
+ // Start polling if this is the first panel
+ if (activeWebviewPanels.length === 1) {
+ await startPolling();
}
-
- // Stop polling if no panels are active
- if (activeWebviewPanels.length === 0) {
- stopPolling();
- }
- });
- // Set webview HTML content
- panel.webview.html = getWebviewContent(panel.webview, context.extensionUri);
+ // Handle panel disposal
+ panel.onDidDispose(() => {
+ const index = activeWebviewPanels.findIndex((p) => p === panel);
+ if (index !== -1) {
+ activeWebviewPanels.splice(index, 1);
+ }
- // Handle messages from webview
- panel.webview.onDidReceiveMessage(
- async (message) => {
+ // Stop polling if no panels are active
+ if (activeWebviewPanels.length === 0) {
+ stopPolling();
+ }
+ });
+
+ // Set webview HTML content
+ panel.webview.html = getWebviewContent(
+ panel.webview,
+ context.extensionUri
+ );
+
+ // Handle messages from webview
+ panel.webview.onDidReceiveMessage(async (message) => {
console.log('📨 Received message from webview:', message);
-
+
switch (message.type) {
case 'ready':
console.log('🚀 Webview is ready!');
@@ -577,84 +678,110 @@ export function activate(context: vscode.ExtensionContext) {
data: { status: 'Extension connected!' }
});
break;
-
+
case 'getTasks':
console.log('📋 Getting tasks...');
try {
if (!taskMasterApi) {
- throw new Error('Task Master API not initialized - extension may be starting up');
+ throw new Error(
+ 'Task Master API not initialized - extension may be starting up'
+ );
}
-
+
// Check if we're in offline mode
if (pollingState.isOfflineMode) {
- throw new Error('Task Master is in offline mode - MCP server connection failed');
+ throw new Error(
+ 'Task Master is in offline mode - MCP server connection failed'
+ );
}
-
+
const tasksResult = await taskMasterApi.getTasks({
withSubtasks: true,
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
-
+
if (tasksResult.success) {
panel.webview.postMessage({
type: 'tasksData',
requestId: message.requestId,
data: tasksResult.data
});
- console.log(`✅ Retrieved ${tasksResult.data?.length || 0} tasks from Task Master`);
+ console.log(
+ `✅ Retrieved ${tasksResult.data?.length || 0} tasks from Task Master`
+ );
} else {
throw new Error(tasksResult.error || 'Failed to get tasks');
}
} catch (error) {
console.error('❌ Error getting tasks:', error);
-
+
// Send error to webview instead of falling back to sample data
panel.webview.postMessage({
type: 'error',
requestId: message.requestId,
- error: error instanceof Error ? error.message : 'Failed to get tasks',
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to get tasks',
errorType: 'connection'
});
-
+
// Enter offline mode if this is a connection error
if (!pollingState.isOfflineMode) {
handleNetworkError(error);
}
}
break;
-
+
case 'updateTaskStatus':
console.log('🔄 Updating task status:', message.data);
try {
- if (taskMasterApi && message.data?.taskId && message.data?.newStatus) {
+ if (
+ taskMasterApi &&
+ message.data?.taskId &&
+ message.data?.newStatus
+ ) {
const updateResult = await taskMasterApi.updateTaskStatus(
- message.data.taskId,
- message.data.newStatus,
+ message.data.taskId,
+ message.data.newStatus,
{
- projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
+ projectRoot:
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
}
);
-
+
if (updateResult.success) {
panel.webview.postMessage({
type: 'taskStatusUpdated',
requestId: message.requestId,
success: true,
- data: { taskId: message.data.taskId, newStatus: message.data.newStatus }
+ data: {
+ taskId: message.data.taskId,
+ newStatus: message.data.newStatus
+ }
});
- console.log(`✅ Updated task ${message.data.taskId} status to ${message.data.newStatus}`);
+ console.log(
+ `✅ Updated task ${message.data.taskId} status to ${message.data.newStatus}`
+ );
} else {
- throw new Error(updateResult.error || 'Failed to update task status');
+ throw new Error(
+ updateResult.error || 'Failed to update task status'
+ );
}
} else {
- throw new Error('Invalid task update data or Task Master API not initialized');
+ throw new Error(
+ 'Invalid task update data or Task Master API not initialized'
+ );
}
} catch (error) {
console.error('❌ Error updating task status:', error);
panel.webview.postMessage({
type: 'error',
requestId: message.requestId,
- error: error instanceof Error ? error.message : 'Failed to update task status'
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to update task status'
});
}
break;
@@ -662,40 +789,52 @@ export function activate(context: vscode.ExtensionContext) {
case 'updateTask':
console.log('📝 Updating task content:', message.data);
try {
- if (taskMasterApi && message.data?.taskId && message.data?.updates) {
+ if (
+ taskMasterApi &&
+ message.data?.taskId &&
+ message.data?.updates
+ ) {
const updateResult = await taskMasterApi.updateTask(
message.data.taskId,
message.data.updates,
{
- projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
+ projectRoot:
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
append: message.data.options?.append || false,
research: message.data.options?.research || false
}
);
-
+
if (updateResult.success) {
panel.webview.postMessage({
type: 'taskUpdated',
requestId: message.requestId,
success: true,
- data: {
- taskId: message.data.taskId,
- updates: message.data.updates
+ data: {
+ taskId: message.data.taskId,
+ updates: message.data.updates
}
});
console.log(`✅ Updated task ${message.data.taskId} content`);
} else {
- throw new Error(updateResult.error || 'Failed to update task');
+ throw new Error(
+ updateResult.error || 'Failed to update task'
+ );
}
} else {
- throw new Error('Invalid task update data or Task Master API not initialized');
+ throw new Error(
+ 'Invalid task update data or Task Master API not initialized'
+ );
}
} catch (error) {
console.error('❌ Error updating task:', error);
panel.webview.postMessage({
type: 'error',
requestId: message.requestId,
- error: error instanceof Error ? error.message : 'Failed to update task'
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to update task'
});
}
break;
@@ -703,39 +842,53 @@ export function activate(context: vscode.ExtensionContext) {
case 'updateSubtask':
console.log('📝 Updating subtask content:', message.data);
try {
- if (taskMasterApi && message.data?.taskId && message.data?.prompt) {
+ if (
+ taskMasterApi &&
+ message.data?.taskId &&
+ message.data?.prompt
+ ) {
const updateResult = await taskMasterApi.updateSubtask(
message.data.taskId,
message.data.prompt,
{
- projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
+ projectRoot:
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
research: message.data.options?.research || false
}
);
-
+
if (updateResult.success) {
panel.webview.postMessage({
type: 'subtaskUpdated',
requestId: message.requestId,
success: true,
- data: {
- taskId: message.data.taskId,
- prompt: message.data.prompt
+ data: {
+ taskId: message.data.taskId,
+ prompt: message.data.prompt
}
});
- console.log(`✅ Updated subtask ${message.data.taskId} content`);
+ console.log(
+ `✅ Updated subtask ${message.data.taskId} content`
+ );
} else {
- throw new Error(updateResult.error || 'Failed to update subtask');
+ throw new Error(
+ updateResult.error || 'Failed to update subtask'
+ );
}
} else {
- throw new Error('Invalid subtask update data or Task Master API not initialized');
+ throw new Error(
+ 'Invalid subtask update data or Task Master API not initialized'
+ );
}
} catch (error) {
console.error('❌ Error updating subtask:', error);
panel.webview.postMessage({
type: 'error',
requestId: message.requestId,
- error: error instanceof Error ? error.message : 'Failed to update subtask'
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to update subtask'
});
}
break;
@@ -743,42 +896,54 @@ export function activate(context: vscode.ExtensionContext) {
case 'addSubtask':
console.log('➕ Adding new subtask:', message.data);
try {
- if (taskMasterApi && message.data?.parentTaskId && message.data?.subtaskData) {
+ if (
+ taskMasterApi &&
+ message.data?.parentTaskId &&
+ message.data?.subtaskData
+ ) {
const addResult = await taskMasterApi.addSubtask(
message.data.parentTaskId,
message.data.subtaskData,
{
- projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
+ projectRoot:
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
}
);
-
+
if (addResult.success) {
panel.webview.postMessage({
type: 'subtaskAdded',
requestId: message.requestId,
success: true,
- data: {
- parentTaskId: message.data.parentTaskId,
- subtaskData: message.data.subtaskData
+ data: {
+ parentTaskId: message.data.parentTaskId,
+ subtaskData: message.data.subtaskData
}
});
- console.log(`✅ Added subtask to task ${message.data.parentTaskId}`);
+ console.log(
+ `✅ Added subtask to task ${message.data.parentTaskId}`
+ );
} else {
throw new Error(addResult.error || 'Failed to add subtask');
}
} else {
- throw new Error('Invalid subtask add data or Task Master API not initialized');
+ throw new Error(
+ 'Invalid subtask add data or Task Master API not initialized'
+ );
}
} catch (error) {
console.error('❌ Error adding subtask:', error);
panel.webview.postMessage({
type: 'error',
requestId: message.requestId,
- error: error instanceof Error ? error.message : 'Failed to add subtask'
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to add subtask'
});
}
break;
-
+
case 'startPolling':
console.log('🔄 Manual start polling requested');
await startPolling();
@@ -788,7 +953,7 @@ export function activate(context: vscode.ExtensionContext) {
success: true
});
break;
-
+
case 'stopPolling':
console.log('⏹️ Manual stop polling requested');
stopPolling();
@@ -798,7 +963,7 @@ export function activate(context: vscode.ExtensionContext) {
success: true
});
break;
-
+
case 'getPollingStatus':
console.log('📊 Polling status requested');
panel.webview.postMessage({
@@ -812,7 +977,7 @@ export function activate(context: vscode.ExtensionContext) {
}
});
break;
-
+
case 'attemptReconnection':
console.log('🔄 Manual reconnection requested');
if (pollingState.isOfflineMode) {
@@ -831,7 +996,7 @@ export function activate(context: vscode.ExtensionContext) {
});
}
break;
-
+
case 'getNetworkStatus':
console.log('📊 Network status requested');
panel.webview.postMessage({
@@ -846,12 +1011,12 @@ export function activate(context: vscode.ExtensionContext) {
}
});
break;
-
+
case 'reactError':
console.log('🔥 React error reported from webview:', message.data);
try {
await handleUIError(
- new Error(message.data.message),
+ new Error(message.data.message),
'React Component Error',
{
stack: message.data.stack,
@@ -863,35 +1028,54 @@ export function activate(context: vscode.ExtensionContext) {
console.error('Failed to handle React error:', error);
}
break;
-
+
case 'readTaskFileData':
console.log('📄 Reading task file data:', message.data);
const { requestId } = message;
try {
const { taskId, tag: tagName = 'master' } = message.data;
-
+
// Get workspace folder
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
throw new Error('No workspace folder found');
}
-
+
// Build path to tasks.json
- const tasksJsonPath = path.join(workspaceFolder.uri.fsPath, '.taskmaster', 'tasks', 'tasks.json');
+ const tasksJsonPath = path.join(
+ workspaceFolder.uri.fsPath,
+ '.taskmaster',
+ 'tasks',
+ 'tasks.json'
+ );
console.log('🔍 Looking for tasks.json at:', tasksJsonPath);
-
+
// Check if file exists
if (!fs.existsSync(tasksJsonPath)) {
// Try legacy location
- const legacyPath = path.join(workspaceFolder.uri.fsPath, 'tasks', 'tasks.json');
+ const legacyPath = path.join(
+ workspaceFolder.uri.fsPath,
+ 'tasks',
+ 'tasks.json'
+ );
console.log('🔍 Trying legacy path:', legacyPath);
if (!fs.existsSync(legacyPath)) {
- throw new Error('tasks.json not found in .taskmaster/tasks/ or tasks/ directory');
+ throw new Error(
+ 'tasks.json not found in .taskmaster/tasks/ or tasks/ directory'
+ );
}
// Use legacy path
const content = fs.readFileSync(legacyPath, 'utf8');
- console.log('📖 Read legacy tasks.json, content length:', content.length);
- const taskData = parseTaskFileData(content, taskId, tagName, workspaceFolder.uri.fsPath);
+ console.log(
+ '📖 Read legacy tasks.json, content length:',
+ content.length
+ );
+ const taskData = parseTaskFileData(
+ content,
+ taskId,
+ tagName,
+ workspaceFolder.uri.fsPath
+ );
console.log('✅ Parsed task data for legacy path:', taskData);
panel.webview.postMessage({
type: 'response',
@@ -900,31 +1084,41 @@ export function activate(context: vscode.ExtensionContext) {
});
return;
}
-
+
// Read and parse tasks.json
const content = fs.readFileSync(tasksJsonPath, 'utf8');
- console.log('📖 Read tasks.json, content length:', content.length);
- const taskData = parseTaskFileData(content, taskId, tagName, workspaceFolder.uri.fsPath);
+ console.log(
+ '📖 Read tasks.json, content length:',
+ content.length
+ );
+ const taskData = parseTaskFileData(
+ content,
+ taskId,
+ tagName,
+ workspaceFolder.uri.fsPath
+ );
console.log('✅ Parsed task data:', taskData);
-
+
panel.webview.postMessage({
type: 'response',
requestId,
data: taskData
});
-
+
console.log(`✅ Retrieved task file data for task ${taskId}`);
-
} catch (error) {
console.error('❌ Error reading task file data:', error);
panel.webview.postMessage({
type: 'error',
requestId,
- error: error instanceof Error ? error.message : 'Failed to read task file data'
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to read task file data'
});
}
break;
-
+
case 'mcpRequest':
console.log('📊 MCP Request:', message);
const { requestId: mcpRequestId, tool, parameters } = message;
@@ -932,52 +1126,68 @@ export function activate(context: vscode.ExtensionContext) {
if (!taskMasterApi) {
throw new Error('Task Master API not initialized');
}
-
+
if (pollingState.isOfflineMode) {
- throw new Error('Task Master is in offline mode - MCP server connection unavailable');
+ throw new Error(
+ 'Task Master is in offline mode - MCP server connection unavailable'
+ );
}
-
+
let result;
-
+
switch (tool) {
case 'complexity_report':
console.log('📊 Calling complexity_report MCP tool');
try {
// Use the private callMCPTool method via type assertion to access it
- const mcpResult = await (taskMasterApi as any).callMCPTool('complexity_report', {
- projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
- ...parameters
- });
+ const mcpResult = await (taskMasterApi as any).callMCPTool(
+ 'complexity_report',
+ {
+ projectRoot:
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
+ ...parameters
+ }
+ );
result = { success: true, data: mcpResult };
} catch (mcpError) {
- result = {
- success: false,
- error: mcpError instanceof Error ? mcpError.message : 'Failed to get complexity report'
+ result = {
+ success: false,
+ error:
+ mcpError instanceof Error
+ ? mcpError.message
+ : 'Failed to get complexity report'
};
}
break;
-
+
case 'analyze_project_complexity':
console.log('🧮 Calling analyze_project_complexity MCP tool');
try {
// Use the private callMCPTool method via type assertion to access it
- const mcpResult = await (taskMasterApi as any).callMCPTool('analyze_project_complexity', {
- projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
- ...parameters
- });
+ const mcpResult = await (taskMasterApi as any).callMCPTool(
+ 'analyze_project_complexity',
+ {
+ projectRoot:
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
+ ...parameters
+ }
+ );
result = { success: true, data: mcpResult };
} catch (mcpError) {
- result = {
- success: false,
- error: mcpError instanceof Error ? mcpError.message : 'Failed to analyze project complexity'
+ result = {
+ success: false,
+ error:
+ mcpError instanceof Error
+ ? mcpError.message
+ : 'Failed to analyze project complexity'
};
}
break;
-
+
default:
throw new Error(`Unsupported MCP tool: ${tool}`);
}
-
+
if (result.success) {
panel.webview.postMessage({
type: 'response',
@@ -986,53 +1196,81 @@ export function activate(context: vscode.ExtensionContext) {
});
console.log(`✅ MCP tool ${tool} executed successfully`);
} else {
- throw new Error(result.error || `Failed to execute MCP tool: ${tool}`);
+ throw new Error(
+ result.error || `Failed to execute MCP tool: ${tool}`
+ );
}
-
} catch (error) {
console.error(`❌ Error executing MCP tool ${tool}:`, error);
panel.webview.postMessage({
type: 'error',
requestId: mcpRequestId,
- error: error instanceof Error ? error.message : `Failed to execute MCP tool: ${tool}`
+ error:
+ error instanceof Error
+ ? error.message
+ : `Failed to execute MCP tool: ${tool}`
});
}
break;
-
+
default:
console.log('❓ Unknown message type:', message.type);
}
- }
- );
+ });
- vscode.window.showInformationMessage('Task Master Kanban Board opened!');
- });
+ vscode.window.showInformationMessage('Task Master Kanban Board opened!');
+ }
+ );
- const checkConnectionCommand = vscode.commands.registerCommand('taskr.checkConnection', async () => {
- console.log('🔗 Check connection command executed!');
- vscode.window.showInformationMessage('Check connection command works!');
- });
+ const checkConnectionCommand = vscode.commands.registerCommand(
+ 'taskr.checkConnection',
+ async () => {
+ console.log('🔗 Check connection command executed!');
+ vscode.window.showInformationMessage('Check connection command works!');
+ }
+ );
- const reconnectCommand = vscode.commands.registerCommand('taskr.reconnect', async () => {
- console.log('🔄 Reconnect command executed!');
- vscode.window.showInformationMessage('Reconnect command works!');
- });
+ const reconnectCommand = vscode.commands.registerCommand(
+ 'taskr.reconnect',
+ async () => {
+ console.log('🔄 Reconnect command executed!');
+ vscode.window.showInformationMessage('Reconnect command works!');
+ }
+ );
- const openSettingsCommand = vscode.commands.registerCommand('taskr.openSettings', () => {
- console.log('⚙️ Open settings command executed!');
- vscode.commands.executeCommand('workbench.action.openSettings', '@ext:taskr taskmaster');
- });
+ const openSettingsCommand = vscode.commands.registerCommand(
+ 'taskr.openSettings',
+ () => {
+ console.log('⚙️ Open settings command executed!');
+ vscode.commands.executeCommand(
+ 'workbench.action.openSettings',
+ '@ext:taskr taskmaster'
+ );
+ }
+ );
+
+ context.subscriptions.push(
+ showKanbanCommand,
+ checkConnectionCommand,
+ reconnectCommand,
+ openSettingsCommand
+ );
- context.subscriptions.push(showKanbanCommand, checkConnectionCommand, reconnectCommand, openSettingsCommand);
-
console.log('✅ All commands registered successfully!');
}
// Generate webview HTML content
-function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string {
+function getWebviewContent(
+ webview: vscode.Webview,
+ extensionUri: vscode.Uri
+): string {
// Get the local path to main script run in the webview
- const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'dist', 'index.js'));
- const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'dist', 'index.css'));
+ const scriptUri = webview.asWebviewUri(
+ vscode.Uri.joinPath(extensionUri, 'dist', 'index.js')
+ );
+ const styleUri = webview.asWebviewUri(
+ vscode.Uri.joinPath(extensionUri, 'dist', 'index.css')
+ );
// Use a nonce to only allow specific scripts to be run
const nonce = getNonce();
@@ -1055,7 +1293,8 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): s
function getNonce() {
let text = '';
- const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ const possible =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
@@ -1071,7 +1310,8 @@ function getSampleTasks() {
description: 'Create the basic VS Code extension structure',
status: 'done',
priority: 'high',
- details: 'Initialize package.json, create src folder, set up TypeScript configuration',
+ details:
+ 'Initialize package.json, create src folder, set up TypeScript configuration',
dependencies: []
},
{
@@ -1080,7 +1320,8 @@ function getSampleTasks() {
description: 'Create MCP client to communicate with task-master-ai',
status: 'done',
priority: 'high',
- details: 'Use @modelcontextprotocol/sdk to create a client that can connect to task-master-ai server',
+ details:
+ 'Use @modelcontextprotocol/sdk to create a client that can connect to task-master-ai server',
dependencies: ['1']
},
{
@@ -1089,7 +1330,8 @@ function getSampleTasks() {
description: 'Build configuration management for the extension',
status: 'done',
priority: 'medium',
- details: 'Create ConfigManager class to handle VS Code settings and configuration updates',
+ details:
+ 'Create ConfigManager class to handle VS Code settings and configuration updates',
dependencies: ['1']
},
{
@@ -1098,7 +1340,8 @@ function getSampleTasks() {
description: 'Set up the webview infrastructure with React',
status: 'done',
priority: 'high',
- details: 'Create webview panel, integrate React, set up bundling with esbuild',
+ details:
+ 'Create webview panel, integrate React, set up bundling with esbuild',
dependencies: ['1', '2', '3']
},
{
@@ -1107,16 +1350,19 @@ function getSampleTasks() {
description: 'Add the Kanban board UI using shadcn/ui components',
status: 'done',
priority: 'medium',
- details: 'Install and customize shadcn/ui Kanban component for VS Code theming',
+ details:
+ 'Install and customize shadcn/ui Kanban component for VS Code theming',
dependencies: ['4']
},
{
id: '6',
title: 'Implement get_tasks MCP tool integration',
- description: 'Use the MCP client to call the get_tasks tool and retrieve task data',
+ description:
+ 'Use the MCP client to call the get_tasks tool and retrieve task data',
status: 'in-progress',
priority: 'high',
- details: 'Connect to task-master-ai server and fetch real task data instead of using sample data',
+ details:
+ 'Connect to task-master-ai server and fetch real task data instead of using sample data',
dependencies: ['2']
},
{
@@ -1125,7 +1371,8 @@ function getSampleTasks() {
description: 'Implement drag-and-drop task status updates through MCP',
status: 'pending',
priority: 'high',
- details: 'When tasks are moved between columns, update status via set_task_status MCP tool',
+ details:
+ 'When tasks are moved between columns, update status via set_task_status MCP tool',
dependencies: ['6']
},
{
@@ -1134,7 +1381,8 @@ function getSampleTasks() {
description: 'Keep the Kanban board in sync with task file changes',
status: 'pending',
priority: 'medium',
- details: 'Implement file watching and real-time updates when tasks.json changes',
+ details:
+ 'Implement file watching and real-time updates when tasks.json changes',
dependencies: ['6', '7']
}
];
@@ -1143,24 +1391,24 @@ function getSampleTasks() {
// This method is called when your extension is deactivated
export function deactivate() {
console.log('👋 Task Master Kanban extension deactivated');
-
+
// Stop polling
stopPolling();
-
+
// Close all active webview panels
- activeWebviewPanels.forEach(panel => panel.dispose());
+ activeWebviewPanels.forEach((panel) => panel.dispose());
activeWebviewPanels = [];
-
+
// Clean up MCP components
if (taskMasterApi) {
taskMasterApi.destroy();
taskMasterApi = null;
}
-
+
if (mcpClient) {
mcpClient.disconnect();
mcpClient = null;
}
-
+
configManager = null;
}
diff --git a/apps/extension/src/lib/utils.ts b/apps/extension/src/lib/utils.ts
index 2164ec65..256f86ff 100644
--- a/apps/extension/src/lib/utils.ts
+++ b/apps/extension/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
\ No newline at end of file
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/extension/src/utils/configManager.ts b/apps/extension/src/utils/configManager.ts
index 4448a6a0..f005527a 100644
--- a/apps/extension/src/utils/configManager.ts
+++ b/apps/extension/src/utils/configManager.ts
@@ -2,398 +2,512 @@ import * as vscode from 'vscode';
import { MCPConfig } from './mcpClient';
export interface TaskMasterConfig {
- mcp: MCPServerConfig;
- ui: UIConfig;
- performance: PerformanceConfig;
- debug: DebugConfig;
+ mcp: MCPServerConfig;
+ ui: UIConfig;
+ performance: PerformanceConfig;
+ debug: DebugConfig;
}
export interface MCPServerConfig {
- command: string;
- args: string[];
- cwd?: string;
- env?: Record;
- timeout: number;
- maxReconnectAttempts: number;
- reconnectBackoffMs: number;
- maxBackoffMs: number;
- healthCheckIntervalMs: number;
+ command: string;
+ args: string[];
+ cwd?: string;
+ env?: Record;
+ timeout: number;
+ maxReconnectAttempts: number;
+ reconnectBackoffMs: number;
+ maxBackoffMs: number;
+ healthCheckIntervalMs: number;
}
export interface UIConfig {
- autoRefresh: boolean;
- refreshIntervalMs: number;
- theme: 'auto' | 'light' | 'dark';
- showCompletedTasks: boolean;
- taskDisplayLimit: number;
- showPriority: boolean;
- showTaskIds: boolean;
+ autoRefresh: boolean;
+ refreshIntervalMs: number;
+ theme: 'auto' | 'light' | 'dark';
+ showCompletedTasks: boolean;
+ taskDisplayLimit: number;
+ showPriority: boolean;
+ showTaskIds: boolean;
}
export interface PerformanceConfig {
- maxConcurrentRequests: number;
- requestTimeoutMs: number;
- cacheTasksMs: number;
- lazyLoadThreshold: number;
+ maxConcurrentRequests: number;
+ requestTimeoutMs: number;
+ cacheTasksMs: number;
+ lazyLoadThreshold: number;
}
export interface DebugConfig {
- enableLogging: boolean;
- logLevel: 'error' | 'warn' | 'info' | 'debug';
- enableConnectionMetrics: boolean;
- saveEventLogs: boolean;
- maxEventLogSize: number;
+ enableLogging: boolean;
+ logLevel: 'error' | 'warn' | 'info' | 'debug';
+ enableConnectionMetrics: boolean;
+ saveEventLogs: boolean;
+ maxEventLogSize: number;
}
export interface ConfigValidationResult {
- isValid: boolean;
- errors: string[];
- warnings: string[];
+ isValid: boolean;
+ errors: string[];
+ warnings: string[];
}
export class ConfigManager {
- private static instance: ConfigManager | null = null;
- private config: TaskMasterConfig;
- private configListeners: ((config: TaskMasterConfig) => void)[] = [];
+ private static instance: ConfigManager | null = null;
+ private config: TaskMasterConfig;
+ private configListeners: ((config: TaskMasterConfig) => void)[] = [];
- private constructor() {
- this.config = this.loadConfig();
- this.setupConfigWatcher();
- }
+ private constructor() {
+ this.config = this.loadConfig();
+ this.setupConfigWatcher();
+ }
- /**
- * Get singleton instance
- */
- static getInstance(): ConfigManager {
- if (!ConfigManager.instance) {
- ConfigManager.instance = new ConfigManager();
- }
- return ConfigManager.instance;
- }
+ /**
+ * Get singleton instance
+ */
+ static getInstance(): ConfigManager {
+ if (!ConfigManager.instance) {
+ ConfigManager.instance = new ConfigManager();
+ }
+ return ConfigManager.instance;
+ }
- /**
- * Get current configuration
- */
- getConfig(): TaskMasterConfig {
- return { ...this.config };
- }
+ /**
+ * Get current configuration
+ */
+ getConfig(): TaskMasterConfig {
+ return { ...this.config };
+ }
- /**
- * Get MCP configuration for the client
- */
- getMCPConfig(): MCPConfig {
- const mcpConfig = this.config.mcp;
- return {
- command: mcpConfig.command,
- args: mcpConfig.args,
- cwd: mcpConfig.cwd,
- env: mcpConfig.env
- };
- }
+ /**
+ * Get MCP configuration for the client
+ */
+ getMCPConfig(): MCPConfig {
+ const mcpConfig = this.config.mcp;
+ return {
+ command: mcpConfig.command,
+ args: mcpConfig.args,
+ cwd: mcpConfig.cwd,
+ env: mcpConfig.env
+ };
+ }
- /**
- * Update configuration (programmatically)
- */
- async updateConfig(updates: Partial): Promise {
- const newConfig = this.mergeConfig(this.config, updates);
- const validation = this.validateConfig(newConfig);
-
- if (!validation.isValid) {
- throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`);
- }
+ /**
+ * Update configuration (programmatically)
+ */
+ async updateConfig(updates: Partial): Promise {
+ const newConfig = this.mergeConfig(this.config, updates);
+ const validation = this.validateConfig(newConfig);
- // Update VS Code settings
- const vsConfig = vscode.workspace.getConfiguration('taskmaster');
-
- if (updates.mcp) {
- if (updates.mcp.command !== undefined) {
- await vsConfig.update('mcp.command', updates.mcp.command, vscode.ConfigurationTarget.Workspace);
- }
- if (updates.mcp.args !== undefined) {
- await vsConfig.update('mcp.args', updates.mcp.args, vscode.ConfigurationTarget.Workspace);
- }
- if (updates.mcp.cwd !== undefined) {
- await vsConfig.update('mcp.cwd', updates.mcp.cwd, vscode.ConfigurationTarget.Workspace);
- }
- if (updates.mcp.timeout !== undefined) {
- await vsConfig.update('mcp.timeout', updates.mcp.timeout, vscode.ConfigurationTarget.Workspace);
- }
- }
+ if (!validation.isValid) {
+ throw new Error(
+ `Configuration validation failed: ${validation.errors.join(', ')}`
+ );
+ }
- if (updates.ui) {
- if (updates.ui.autoRefresh !== undefined) {
- await vsConfig.update('ui.autoRefresh', updates.ui.autoRefresh, vscode.ConfigurationTarget.Workspace);
- }
- if (updates.ui.theme !== undefined) {
- await vsConfig.update('ui.theme', updates.ui.theme, vscode.ConfigurationTarget.Workspace);
- }
- }
+ // Update VS Code settings
+ const vsConfig = vscode.workspace.getConfiguration('taskmaster');
- if (updates.debug) {
- if (updates.debug.enableLogging !== undefined) {
- await vsConfig.update('debug.enableLogging', updates.debug.enableLogging, vscode.ConfigurationTarget.Workspace);
- }
- if (updates.debug.logLevel !== undefined) {
- await vsConfig.update('debug.logLevel', updates.debug.logLevel, vscode.ConfigurationTarget.Workspace);
- }
- }
+ if (updates.mcp) {
+ if (updates.mcp.command !== undefined) {
+ await vsConfig.update(
+ 'mcp.command',
+ updates.mcp.command,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ if (updates.mcp.args !== undefined) {
+ await vsConfig.update(
+ 'mcp.args',
+ updates.mcp.args,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ if (updates.mcp.cwd !== undefined) {
+ await vsConfig.update(
+ 'mcp.cwd',
+ updates.mcp.cwd,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ if (updates.mcp.timeout !== undefined) {
+ await vsConfig.update(
+ 'mcp.timeout',
+ updates.mcp.timeout,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ }
- this.config = newConfig;
- this.notifyConfigChange();
- }
+ if (updates.ui) {
+ if (updates.ui.autoRefresh !== undefined) {
+ await vsConfig.update(
+ 'ui.autoRefresh',
+ updates.ui.autoRefresh,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ if (updates.ui.theme !== undefined) {
+ await vsConfig.update(
+ 'ui.theme',
+ updates.ui.theme,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ }
- /**
- * Validate configuration
- */
- validateConfig(config: TaskMasterConfig): ConfigValidationResult {
- const errors: string[] = [];
- const warnings: string[] = [];
+ if (updates.debug) {
+ if (updates.debug.enableLogging !== undefined) {
+ await vsConfig.update(
+ 'debug.enableLogging',
+ updates.debug.enableLogging,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ if (updates.debug.logLevel !== undefined) {
+ await vsConfig.update(
+ 'debug.logLevel',
+ updates.debug.logLevel,
+ vscode.ConfigurationTarget.Workspace
+ );
+ }
+ }
- // Validate MCP configuration
- if (!config.mcp.command || config.mcp.command.trim() === '') {
- errors.push('MCP command cannot be empty');
- }
+ this.config = newConfig;
+ this.notifyConfigChange();
+ }
- if (config.mcp.timeout < 1000) {
- warnings.push('MCP timeout is very low (< 1s), this may cause connection issues');
- } else if (config.mcp.timeout > 60000) {
- warnings.push('MCP timeout is very high (> 60s), this may cause slow responses');
- }
+ /**
+ * Validate configuration
+ */
+ validateConfig(config: TaskMasterConfig): ConfigValidationResult {
+ const errors: string[] = [];
+ const warnings: string[] = [];
- if (config.mcp.maxReconnectAttempts < 1) {
- errors.push('Max reconnect attempts must be at least 1');
- } else if (config.mcp.maxReconnectAttempts > 10) {
- warnings.push('Max reconnect attempts is very high, this may cause long delays');
- }
+ // Validate MCP configuration
+ if (!config.mcp.command || config.mcp.command.trim() === '') {
+ errors.push('MCP command cannot be empty');
+ }
- // Validate UI configuration
- if (config.ui.refreshIntervalMs < 1000) {
- warnings.push('UI refresh interval is very low (< 1s), this may impact performance');
- }
+ if (config.mcp.timeout < 1000) {
+ warnings.push(
+ 'MCP timeout is very low (< 1s), this may cause connection issues'
+ );
+ } else if (config.mcp.timeout > 60000) {
+ warnings.push(
+ 'MCP timeout is very high (> 60s), this may cause slow responses'
+ );
+ }
- if (config.ui.taskDisplayLimit < 1) {
- errors.push('Task display limit must be at least 1');
- } else if (config.ui.taskDisplayLimit > 1000) {
- warnings.push('Task display limit is very high, this may impact performance');
- }
+ if (config.mcp.maxReconnectAttempts < 1) {
+ errors.push('Max reconnect attempts must be at least 1');
+ } else if (config.mcp.maxReconnectAttempts > 10) {
+ warnings.push(
+ 'Max reconnect attempts is very high, this may cause long delays'
+ );
+ }
- // Validate performance configuration
- if (config.performance.maxConcurrentRequests < 1) {
- errors.push('Max concurrent requests must be at least 1');
- } else if (config.performance.maxConcurrentRequests > 20) {
- warnings.push('Max concurrent requests is very high, this may overwhelm the server');
- }
+ // Validate UI configuration
+ if (config.ui.refreshIntervalMs < 1000) {
+ warnings.push(
+ 'UI refresh interval is very low (< 1s), this may impact performance'
+ );
+ }
- if (config.performance.requestTimeoutMs < 1000) {
- warnings.push('Request timeout is very low (< 1s), this may cause premature timeouts');
- }
+ if (config.ui.taskDisplayLimit < 1) {
+ errors.push('Task display limit must be at least 1');
+ } else if (config.ui.taskDisplayLimit > 1000) {
+ warnings.push(
+ 'Task display limit is very high, this may impact performance'
+ );
+ }
- // Validate debug configuration
- if (config.debug.maxEventLogSize < 10) {
- errors.push('Max event log size must be at least 10');
- } else if (config.debug.maxEventLogSize > 10000) {
- warnings.push('Max event log size is very high, this may consume significant memory');
- }
+ // Validate performance configuration
+ if (config.performance.maxConcurrentRequests < 1) {
+ errors.push('Max concurrent requests must be at least 1');
+ } else if (config.performance.maxConcurrentRequests > 20) {
+ warnings.push(
+ 'Max concurrent requests is very high, this may overwhelm the server'
+ );
+ }
- return {
- isValid: errors.length === 0,
- errors,
- warnings
- };
- }
+ if (config.performance.requestTimeoutMs < 1000) {
+ warnings.push(
+ 'Request timeout is very low (< 1s), this may cause premature timeouts'
+ );
+ }
- /**
- * Reset configuration to defaults
- */
- async resetToDefaults(): Promise {
- const defaultConfig = this.getDefaultConfig();
- await this.updateConfig(defaultConfig);
- }
+ // Validate debug configuration
+ if (config.debug.maxEventLogSize < 10) {
+ errors.push('Max event log size must be at least 10');
+ } else if (config.debug.maxEventLogSize > 10000) {
+ warnings.push(
+ 'Max event log size is very high, this may consume significant memory'
+ );
+ }
- /**
- * Export configuration to JSON
- */
- exportConfig(): string {
- return JSON.stringify(this.config, null, 2);
- }
+ return {
+ isValid: errors.length === 0,
+ errors,
+ warnings
+ };
+ }
- /**
- * Import configuration from JSON
- */
- async importConfig(jsonConfig: string): Promise {
- try {
- const importedConfig = JSON.parse(jsonConfig) as TaskMasterConfig;
- const validation = this.validateConfig(importedConfig);
-
- if (!validation.isValid) {
- throw new Error(`Invalid configuration: ${validation.errors.join(', ')}`);
- }
+ /**
+ * Reset configuration to defaults
+ */
+ async resetToDefaults(): Promise {
+ const defaultConfig = this.getDefaultConfig();
+ await this.updateConfig(defaultConfig);
+ }
- if (validation.warnings.length > 0) {
- const proceed = await vscode.window.showWarningMessage(
- `Configuration has warnings: ${validation.warnings.join(', ')}. Import anyway?`,
- 'Yes', 'No'
- );
-
- if (proceed !== 'Yes') {
- return;
- }
- }
+ /**
+ * Export configuration to JSON
+ */
+ exportConfig(): string {
+ return JSON.stringify(this.config, null, 2);
+ }
- await this.updateConfig(importedConfig);
- vscode.window.showInformationMessage('Configuration imported successfully');
-
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- vscode.window.showErrorMessage(`Failed to import configuration: ${errorMessage}`);
- throw error;
- }
- }
+ /**
+ * Import configuration from JSON
+ */
+ async importConfig(jsonConfig: string): Promise {
+ try {
+ const importedConfig = JSON.parse(jsonConfig) as TaskMasterConfig;
+ const validation = this.validateConfig(importedConfig);
- /**
- * Add configuration change listener
- */
- onConfigChange(listener: (config: TaskMasterConfig) => void): void {
- this.configListeners.push(listener);
- }
+ if (!validation.isValid) {
+ throw new Error(
+ `Invalid configuration: ${validation.errors.join(', ')}`
+ );
+ }
- /**
- * Remove configuration change listener
- */
- removeConfigListener(listener: (config: TaskMasterConfig) => void): void {
- const index = this.configListeners.indexOf(listener);
- if (index !== -1) {
- this.configListeners.splice(index, 1);
- }
- }
+ if (validation.warnings.length > 0) {
+ const proceed = await vscode.window.showWarningMessage(
+ `Configuration has warnings: ${validation.warnings.join(', ')}. Import anyway?`,
+ 'Yes',
+ 'No'
+ );
- /**
- * Load configuration from VS Code settings
- */
- private loadConfig(): TaskMasterConfig {
- const vsConfig = vscode.workspace.getConfiguration('taskmaster');
- const defaultConfig = this.getDefaultConfig();
+ if (proceed !== 'Yes') {
+ return;
+ }
+ }
- return {
- mcp: {
- command: vsConfig.get('mcp.command', defaultConfig.mcp.command),
- args: vsConfig.get('mcp.args', defaultConfig.mcp.args),
- cwd: vsConfig.get('mcp.cwd', defaultConfig.mcp.cwd),
- env: vsConfig.get('mcp.env', defaultConfig.mcp.env),
- timeout: vsConfig.get('mcp.timeout', defaultConfig.mcp.timeout),
- maxReconnectAttempts: vsConfig.get('mcp.maxReconnectAttempts', defaultConfig.mcp.maxReconnectAttempts),
- reconnectBackoffMs: vsConfig.get('mcp.reconnectBackoffMs', defaultConfig.mcp.reconnectBackoffMs),
- maxBackoffMs: vsConfig.get('mcp.maxBackoffMs', defaultConfig.mcp.maxBackoffMs),
- healthCheckIntervalMs: vsConfig.get('mcp.healthCheckIntervalMs', defaultConfig.mcp.healthCheckIntervalMs)
- },
- ui: {
- autoRefresh: vsConfig.get('ui.autoRefresh', defaultConfig.ui.autoRefresh),
- refreshIntervalMs: vsConfig.get('ui.refreshIntervalMs', defaultConfig.ui.refreshIntervalMs),
- theme: vsConfig.get('ui.theme', defaultConfig.ui.theme),
- showCompletedTasks: vsConfig.get('ui.showCompletedTasks', defaultConfig.ui.showCompletedTasks),
- taskDisplayLimit: vsConfig.get('ui.taskDisplayLimit', defaultConfig.ui.taskDisplayLimit),
- showPriority: vsConfig.get('ui.showPriority', defaultConfig.ui.showPriority),
- showTaskIds: vsConfig.get('ui.showTaskIds', defaultConfig.ui.showTaskIds)
- },
- performance: {
- maxConcurrentRequests: vsConfig.get('performance.maxConcurrentRequests', defaultConfig.performance.maxConcurrentRequests),
- requestTimeoutMs: vsConfig.get('performance.requestTimeoutMs', defaultConfig.performance.requestTimeoutMs),
- cacheTasksMs: vsConfig.get('performance.cacheTasksMs', defaultConfig.performance.cacheTasksMs),
- lazyLoadThreshold: vsConfig.get('performance.lazyLoadThreshold', defaultConfig.performance.lazyLoadThreshold)
- },
- debug: {
- enableLogging: vsConfig.get('debug.enableLogging', defaultConfig.debug.enableLogging),
- logLevel: vsConfig.get('debug.logLevel', defaultConfig.debug.logLevel),
- enableConnectionMetrics: vsConfig.get('debug.enableConnectionMetrics', defaultConfig.debug.enableConnectionMetrics),
- saveEventLogs: vsConfig.get('debug.saveEventLogs', defaultConfig.debug.saveEventLogs),
- maxEventLogSize: vsConfig.get('debug.maxEventLogSize', defaultConfig.debug.maxEventLogSize)
- }
- };
- }
+ await this.updateConfig(importedConfig);
+ vscode.window.showInformationMessage(
+ 'Configuration imported successfully'
+ );
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ vscode.window.showErrorMessage(
+ `Failed to import configuration: ${errorMessage}`
+ );
+ throw error;
+ }
+ }
- /**
- * Get default configuration
- */
- private getDefaultConfig(): TaskMasterConfig {
- return {
- mcp: {
- command: 'npx',
- args: ['-y', '--package=task-master-ai', 'task-master-ai'],
- cwd: vscode.workspace.rootPath || '',
- env: undefined,
- timeout: 30000,
- maxReconnectAttempts: 5,
- reconnectBackoffMs: 1000,
- maxBackoffMs: 30000,
- healthCheckIntervalMs: 15000
- },
- ui: {
- autoRefresh: true,
- refreshIntervalMs: 10000,
- theme: 'auto',
- showCompletedTasks: true,
- taskDisplayLimit: 100,
- showPriority: true,
- showTaskIds: true
- },
- performance: {
- maxConcurrentRequests: 5,
- requestTimeoutMs: 30000,
- cacheTasksMs: 5000,
- lazyLoadThreshold: 50
- },
- debug: {
- enableLogging: true,
- logLevel: 'info',
- enableConnectionMetrics: true,
- saveEventLogs: false,
- maxEventLogSize: 1000
- }
- };
- }
+ /**
+ * Add configuration change listener
+ */
+ onConfigChange(listener: (config: TaskMasterConfig) => void): void {
+ this.configListeners.push(listener);
+ }
- /**
- * Setup configuration watcher
- */
- private setupConfigWatcher(): void {
- vscode.workspace.onDidChangeConfiguration((event) => {
- if (event.affectsConfiguration('taskmaster')) {
- console.log('Task Master configuration changed, reloading...');
- this.config = this.loadConfig();
- this.notifyConfigChange();
- }
- });
- }
+ /**
+ * Remove configuration change listener
+ */
+ removeConfigListener(listener: (config: TaskMasterConfig) => void): void {
+ const index = this.configListeners.indexOf(listener);
+ if (index !== -1) {
+ this.configListeners.splice(index, 1);
+ }
+ }
- /**
- * Merge configurations
- */
- private mergeConfig(baseConfig: TaskMasterConfig, updates: Partial): TaskMasterConfig {
- return {
- mcp: { ...baseConfig.mcp, ...updates.mcp },
- ui: { ...baseConfig.ui, ...updates.ui },
- performance: { ...baseConfig.performance, ...updates.performance },
- debug: { ...baseConfig.debug, ...updates.debug }
- };
- }
+ /**
+ * Load configuration from VS Code settings
+ */
+ private loadConfig(): TaskMasterConfig {
+ const vsConfig = vscode.workspace.getConfiguration('taskmaster');
+ const defaultConfig = this.getDefaultConfig();
- /**
- * Notify configuration change listeners
- */
- private notifyConfigChange(): void {
- this.configListeners.forEach(listener => {
- try {
- listener(this.config);
- } catch (error) {
- console.error('Error in configuration change listener:', error);
- }
- });
- }
+ return {
+ mcp: {
+ command: vsConfig.get('mcp.command', defaultConfig.mcp.command),
+ args: vsConfig.get('mcp.args', defaultConfig.mcp.args),
+ cwd: vsConfig.get('mcp.cwd', defaultConfig.mcp.cwd),
+ env: vsConfig.get('mcp.env', defaultConfig.mcp.env),
+ timeout: vsConfig.get('mcp.timeout', defaultConfig.mcp.timeout),
+ maxReconnectAttempts: vsConfig.get(
+ 'mcp.maxReconnectAttempts',
+ defaultConfig.mcp.maxReconnectAttempts
+ ),
+ reconnectBackoffMs: vsConfig.get(
+ 'mcp.reconnectBackoffMs',
+ defaultConfig.mcp.reconnectBackoffMs
+ ),
+ maxBackoffMs: vsConfig.get(
+ 'mcp.maxBackoffMs',
+ defaultConfig.mcp.maxBackoffMs
+ ),
+ healthCheckIntervalMs: vsConfig.get(
+ 'mcp.healthCheckIntervalMs',
+ defaultConfig.mcp.healthCheckIntervalMs
+ )
+ },
+ ui: {
+ autoRefresh: vsConfig.get(
+ 'ui.autoRefresh',
+ defaultConfig.ui.autoRefresh
+ ),
+ refreshIntervalMs: vsConfig.get(
+ 'ui.refreshIntervalMs',
+ defaultConfig.ui.refreshIntervalMs
+ ),
+ theme: vsConfig.get('ui.theme', defaultConfig.ui.theme),
+ showCompletedTasks: vsConfig.get(
+ 'ui.showCompletedTasks',
+ defaultConfig.ui.showCompletedTasks
+ ),
+ taskDisplayLimit: vsConfig.get(
+ 'ui.taskDisplayLimit',
+ defaultConfig.ui.taskDisplayLimit
+ ),
+ showPriority: vsConfig.get(
+ 'ui.showPriority',
+ defaultConfig.ui.showPriority
+ ),
+ showTaskIds: vsConfig.get(
+ 'ui.showTaskIds',
+ defaultConfig.ui.showTaskIds
+ )
+ },
+ performance: {
+ maxConcurrentRequests: vsConfig.get(
+ 'performance.maxConcurrentRequests',
+ defaultConfig.performance.maxConcurrentRequests
+ ),
+ requestTimeoutMs: vsConfig.get(
+ 'performance.requestTimeoutMs',
+ defaultConfig.performance.requestTimeoutMs
+ ),
+ cacheTasksMs: vsConfig.get(
+ 'performance.cacheTasksMs',
+ defaultConfig.performance.cacheTasksMs
+ ),
+ lazyLoadThreshold: vsConfig.get(
+ 'performance.lazyLoadThreshold',
+ defaultConfig.performance.lazyLoadThreshold
+ )
+ },
+ debug: {
+ enableLogging: vsConfig.get(
+ 'debug.enableLogging',
+ defaultConfig.debug.enableLogging
+ ),
+ logLevel: vsConfig.get('debug.logLevel', defaultConfig.debug.logLevel),
+ enableConnectionMetrics: vsConfig.get(
+ 'debug.enableConnectionMetrics',
+ defaultConfig.debug.enableConnectionMetrics
+ ),
+ saveEventLogs: vsConfig.get(
+ 'debug.saveEventLogs',
+ defaultConfig.debug.saveEventLogs
+ ),
+ maxEventLogSize: vsConfig.get(
+ 'debug.maxEventLogSize',
+ defaultConfig.debug.maxEventLogSize
+ )
+ }
+ };
+ }
+
+ /**
+ * Get default configuration
+ */
+ private getDefaultConfig(): TaskMasterConfig {
+ return {
+ mcp: {
+ command: 'npx',
+ args: ['-y', '--package=task-master-ai', 'task-master-ai'],
+ cwd: vscode.workspace.rootPath || '',
+ env: undefined,
+ timeout: 30000,
+ maxReconnectAttempts: 5,
+ reconnectBackoffMs: 1000,
+ maxBackoffMs: 30000,
+ healthCheckIntervalMs: 15000
+ },
+ ui: {
+ autoRefresh: true,
+ refreshIntervalMs: 10000,
+ theme: 'auto',
+ showCompletedTasks: true,
+ taskDisplayLimit: 100,
+ showPriority: true,
+ showTaskIds: true
+ },
+ performance: {
+ maxConcurrentRequests: 5,
+ requestTimeoutMs: 30000,
+ cacheTasksMs: 5000,
+ lazyLoadThreshold: 50
+ },
+ debug: {
+ enableLogging: true,
+ logLevel: 'info',
+ enableConnectionMetrics: true,
+ saveEventLogs: false,
+ maxEventLogSize: 1000
+ }
+ };
+ }
+
+ /**
+ * Setup configuration watcher
+ */
+ private setupConfigWatcher(): void {
+ vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration('taskmaster')) {
+ console.log('Task Master configuration changed, reloading...');
+ this.config = this.loadConfig();
+ this.notifyConfigChange();
+ }
+ });
+ }
+
+ /**
+ * Merge configurations
+ */
+ private mergeConfig(
+ baseConfig: TaskMasterConfig,
+ updates: Partial
+ ): TaskMasterConfig {
+ return {
+ mcp: { ...baseConfig.mcp, ...updates.mcp },
+ ui: { ...baseConfig.ui, ...updates.ui },
+ performance: { ...baseConfig.performance, ...updates.performance },
+ debug: { ...baseConfig.debug, ...updates.debug }
+ };
+ }
+
+ /**
+ * Notify configuration change listeners
+ */
+ private notifyConfigChange(): void {
+ this.configListeners.forEach((listener) => {
+ try {
+ listener(this.config);
+ } catch (error) {
+ console.error('Error in configuration change listener:', error);
+ }
+ });
+ }
}
/**
* Utility function to get configuration manager instance
*/
export function getConfigManager(): ConfigManager {
- return ConfigManager.getInstance();
-}
\ No newline at end of file
+ return ConfigManager.getInstance();
+}
diff --git a/apps/extension/src/utils/connectionManager.ts b/apps/extension/src/utils/connectionManager.ts
index f37679ee..cc9942d7 100644
--- a/apps/extension/src/utils/connectionManager.ts
+++ b/apps/extension/src/utils/connectionManager.ts
@@ -2,353 +2,381 @@ import * as vscode from 'vscode';
import { MCPClientManager, MCPConfig, MCPServerStatus } from './mcpClient';
export interface ConnectionEvent {
- type: 'connected' | 'disconnected' | 'error' | 'reconnecting';
- timestamp: Date;
- data?: any;
+ type: 'connected' | 'disconnected' | 'error' | 'reconnecting';
+ timestamp: Date;
+ data?: any;
}
export interface ConnectionHealth {
- isHealthy: boolean;
- lastSuccessfulCall?: Date;
- consecutiveFailures: number;
- averageResponseTime: number;
- uptime: number;
+ isHealthy: boolean;
+ lastSuccessfulCall?: Date;
+ consecutiveFailures: number;
+ averageResponseTime: number;
+ uptime: number;
}
export class ConnectionManager {
- private mcpClient: MCPClientManager | null = null;
- private config: MCPConfig;
- private connectionEvents: ConnectionEvent[] = [];
- private health: ConnectionHealth = {
- isHealthy: false,
- consecutiveFailures: 0,
- averageResponseTime: 0,
- uptime: 0
- };
- private startTime: Date | null = null;
- private healthCheckInterval: NodeJS.Timeout | null = null;
- private reconnectAttempts = 0;
- private maxReconnectAttempts = 5;
- private reconnectBackoffMs = 1000; // Start with 1 second
- private maxBackoffMs = 30000; // Max 30 seconds
- private isReconnecting = false;
+ private mcpClient: MCPClientManager | null = null;
+ private config: MCPConfig;
+ private connectionEvents: ConnectionEvent[] = [];
+ private health: ConnectionHealth = {
+ isHealthy: false,
+ consecutiveFailures: 0,
+ averageResponseTime: 0,
+ uptime: 0
+ };
+ private startTime: Date | null = null;
+ private healthCheckInterval: NodeJS.Timeout | null = null;
+ private reconnectAttempts = 0;
+ private maxReconnectAttempts = 5;
+ private reconnectBackoffMs = 1000; // Start with 1 second
+ private maxBackoffMs = 30000; // Max 30 seconds
+ private isReconnecting = false;
- // Event handlers
- private onConnectionChange?: (status: MCPServerStatus, health: ConnectionHealth) => void;
- private onConnectionEvent?: (event: ConnectionEvent) => void;
+ // Event handlers
+ private onConnectionChange?: (
+ status: MCPServerStatus,
+ health: ConnectionHealth
+ ) => void;
+ private onConnectionEvent?: (event: ConnectionEvent) => void;
- constructor(config: MCPConfig) {
- this.config = config;
- this.mcpClient = new MCPClientManager(config);
- }
+ constructor(config: MCPConfig) {
+ this.config = config;
+ this.mcpClient = new MCPClientManager(config);
+ }
- /**
- * Set event handlers
- */
- setEventHandlers(handlers: {
- onConnectionChange?: (status: MCPServerStatus, health: ConnectionHealth) => void;
- onConnectionEvent?: (event: ConnectionEvent) => void;
- }) {
- this.onConnectionChange = handlers.onConnectionChange;
- this.onConnectionEvent = handlers.onConnectionEvent;
- }
+ /**
+ * Set event handlers
+ */
+ setEventHandlers(handlers: {
+ onConnectionChange?: (
+ status: MCPServerStatus,
+ health: ConnectionHealth
+ ) => void;
+ onConnectionEvent?: (event: ConnectionEvent) => void;
+ }) {
+ this.onConnectionChange = handlers.onConnectionChange;
+ this.onConnectionEvent = handlers.onConnectionEvent;
+ }
- /**
- * Connect with automatic retry and health monitoring
- */
- async connect(): Promise {
- try {
- if (!this.mcpClient) {
- throw new Error('MCP client not initialized');
- }
+ /**
+ * Connect with automatic retry and health monitoring
+ */
+ async connect(): Promise {
+ try {
+ if (!this.mcpClient) {
+ throw new Error('MCP client not initialized');
+ }
- this.logEvent({ type: 'reconnecting', timestamp: new Date() });
+ this.logEvent({ type: 'reconnecting', timestamp: new Date() });
- await this.mcpClient.connect();
-
- this.reconnectAttempts = 0;
- this.reconnectBackoffMs = 1000;
- this.isReconnecting = false;
- this.startTime = new Date();
-
- this.updateHealth();
- this.startHealthMonitoring();
-
- this.logEvent({ type: 'connected', timestamp: new Date() });
-
- console.log('Connection manager: Successfully connected');
-
- } catch (error) {
- this.logEvent({
- type: 'error',
- timestamp: new Date(),
- data: { error: error instanceof Error ? error.message : 'Unknown error' }
- });
-
- await this.handleConnectionFailure(error);
- throw error;
- }
- }
+ await this.mcpClient.connect();
- /**
- * Disconnect and stop health monitoring
- */
- async disconnect(): Promise {
- this.stopHealthMonitoring();
- this.isReconnecting = false;
-
- if (this.mcpClient) {
- await this.mcpClient.disconnect();
- }
-
- this.health.isHealthy = false;
- this.startTime = null;
-
- this.logEvent({ type: 'disconnected', timestamp: new Date() });
-
- this.notifyConnectionChange();
- }
+ this.reconnectAttempts = 0;
+ this.reconnectBackoffMs = 1000;
+ this.isReconnecting = false;
+ this.startTime = new Date();
- /**
- * Get current connection status
- */
- getStatus(): MCPServerStatus {
- return this.mcpClient?.getStatus() || { isRunning: false };
- }
+ this.updateHealth();
+ this.startHealthMonitoring();
- /**
- * Get connection health metrics
- */
- getHealth(): ConnectionHealth {
- this.updateHealth();
- return { ...this.health };
- }
+ this.logEvent({ type: 'connected', timestamp: new Date() });
- /**
- * Get recent connection events
- */
- getEvents(limit: number = 10): ConnectionEvent[] {
- return this.connectionEvents.slice(-limit);
- }
+ console.log('Connection manager: Successfully connected');
+ } catch (error) {
+ this.logEvent({
+ type: 'error',
+ timestamp: new Date(),
+ data: {
+ error: error instanceof Error ? error.message : 'Unknown error'
+ }
+ });
- /**
- * Test connection with performance monitoring
- */
- async testConnection(): Promise<{ success: boolean; responseTime: number; error?: string }> {
- if (!this.mcpClient) {
- return { success: false, responseTime: 0, error: 'Client not initialized' };
- }
+ await this.handleConnectionFailure(error);
+ throw error;
+ }
+ }
- const startTime = Date.now();
-
- try {
- const success = await this.mcpClient.testConnection();
- const responseTime = Date.now() - startTime;
-
- if (success) {
- this.health.lastSuccessfulCall = new Date();
- this.health.consecutiveFailures = 0;
- this.updateAverageResponseTime(responseTime);
- } else {
- this.health.consecutiveFailures++;
- }
-
- this.updateHealth();
- this.notifyConnectionChange();
-
- return { success, responseTime };
-
- } catch (error) {
- const responseTime = Date.now() - startTime;
- this.health.consecutiveFailures++;
- this.updateHealth();
- this.notifyConnectionChange();
-
- return {
- success: false,
- responseTime,
- error: error instanceof Error ? error.message : 'Unknown error'
- };
- }
- }
+ /**
+ * Disconnect and stop health monitoring
+ */
+ async disconnect(): Promise {
+ this.stopHealthMonitoring();
+ this.isReconnecting = false;
- /**
- * Call MCP tool with automatic retry and health monitoring
- */
- async callTool(toolName: string, arguments_: Record): Promise {
- if (!this.mcpClient) {
- throw new Error('MCP client not initialized');
- }
+ if (this.mcpClient) {
+ await this.mcpClient.disconnect();
+ }
- const startTime = Date.now();
-
- try {
- const result = await this.mcpClient.callTool(toolName, arguments_);
- const responseTime = Date.now() - startTime;
-
- this.health.lastSuccessfulCall = new Date();
- this.health.consecutiveFailures = 0;
- this.updateAverageResponseTime(responseTime);
- this.updateHealth();
- this.notifyConnectionChange();
-
- return result;
-
- } catch (error) {
- this.health.consecutiveFailures++;
- this.updateHealth();
-
- // Attempt reconnection if connection seems lost
- if (this.health.consecutiveFailures >= 3 && !this.isReconnecting) {
- console.log('Multiple consecutive failures detected, attempting reconnection...');
- this.reconnectWithBackoff().catch(err => {
- console.error('Reconnection failed:', err);
- });
- }
-
- this.notifyConnectionChange();
- throw error;
- }
- }
+ this.health.isHealthy = false;
+ this.startTime = null;
- /**
- * Update configuration and reconnect
- */
- async updateConfig(newConfig: MCPConfig): Promise {
- this.config = newConfig;
-
- await this.disconnect();
- this.mcpClient = new MCPClientManager(newConfig);
-
- // Attempt to reconnect with new config
- try {
- await this.connect();
- } catch (error) {
- console.error('Failed to connect with new configuration:', error);
- }
- }
+ this.logEvent({ type: 'disconnected', timestamp: new Date() });
- /**
- * Start health monitoring
- */
- private startHealthMonitoring(): void {
- this.stopHealthMonitoring();
-
- this.healthCheckInterval = setInterval(async () => {
- try {
- await this.testConnection();
- } catch (error) {
- console.error('Health check failed:', error);
- }
- }, 15000); // Check every 15 seconds
- }
+ this.notifyConnectionChange();
+ }
- /**
- * Stop health monitoring
- */
- private stopHealthMonitoring(): void {
- if (this.healthCheckInterval) {
- clearInterval(this.healthCheckInterval);
- this.healthCheckInterval = null;
- }
- }
+ /**
+ * Get current connection status
+ */
+ getStatus(): MCPServerStatus {
+ return this.mcpClient?.getStatus() || { isRunning: false };
+ }
- /**
- * Handle connection failure with exponential backoff
- */
- private async handleConnectionFailure(error: any): Promise {
- this.health.consecutiveFailures++;
- this.updateHealth();
- this.notifyConnectionChange();
-
- if (this.reconnectAttempts < this.maxReconnectAttempts && !this.isReconnecting) {
- await this.reconnectWithBackoff();
- }
- }
+ /**
+ * Get connection health metrics
+ */
+ getHealth(): ConnectionHealth {
+ this.updateHealth();
+ return { ...this.health };
+ }
- /**
- * Reconnect with exponential backoff
- */
- private async reconnectWithBackoff(): Promise {
- if (this.isReconnecting) {
- return;
- }
-
- this.isReconnecting = true;
- this.reconnectAttempts++;
-
- const backoffMs = Math.min(
- this.reconnectBackoffMs * Math.pow(2, this.reconnectAttempts - 1),
- this.maxBackoffMs
- );
-
- console.log(`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${backoffMs}ms...`);
-
- await new Promise(resolve => setTimeout(resolve, backoffMs));
-
- try {
- await this.connect();
- } catch (error) {
- console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error);
-
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
- this.isReconnecting = false;
- vscode.window.showErrorMessage(
- `Failed to reconnect to Task Master after ${this.maxReconnectAttempts} attempts. Please check your configuration and try manually reconnecting.`
- );
- } else {
- // Try again
- await this.reconnectWithBackoff();
- }
- }
- }
+ /**
+ * Get recent connection events
+ */
+ getEvents(limit: number = 10): ConnectionEvent[] {
+ return this.connectionEvents.slice(-limit);
+ }
- /**
- * Update health metrics
- */
- private updateHealth(): void {
- const status = this.getStatus();
- this.health.isHealthy = status.isRunning && this.health.consecutiveFailures < 3;
-
- if (this.startTime) {
- this.health.uptime = Date.now() - this.startTime.getTime();
- }
- }
+ /**
+ * Test connection with performance monitoring
+ */
+ async testConnection(): Promise<{
+ success: boolean;
+ responseTime: number;
+ error?: string;
+ }> {
+ if (!this.mcpClient) {
+ return {
+ success: false,
+ responseTime: 0,
+ error: 'Client not initialized'
+ };
+ }
- /**
- * Update average response time
- */
- private updateAverageResponseTime(responseTime: number): void {
- // Simple moving average calculation
- if (this.health.averageResponseTime === 0) {
- this.health.averageResponseTime = responseTime;
- } else {
- this.health.averageResponseTime = (this.health.averageResponseTime * 0.8) + (responseTime * 0.2);
- }
- }
+ const startTime = Date.now();
- /**
- * Log connection event
- */
- private logEvent(event: ConnectionEvent): void {
- this.connectionEvents.push(event);
-
- // Keep only last 100 events
- if (this.connectionEvents.length > 100) {
- this.connectionEvents = this.connectionEvents.slice(-100);
- }
-
- if (this.onConnectionEvent) {
- this.onConnectionEvent(event);
- }
- }
+ try {
+ const success = await this.mcpClient.testConnection();
+ const responseTime = Date.now() - startTime;
- /**
- * Notify connection change
- */
- private notifyConnectionChange(): void {
- if (this.onConnectionChange) {
- this.onConnectionChange(this.getStatus(), this.getHealth());
- }
- }
-}
\ No newline at end of file
+ if (success) {
+ this.health.lastSuccessfulCall = new Date();
+ this.health.consecutiveFailures = 0;
+ this.updateAverageResponseTime(responseTime);
+ } else {
+ this.health.consecutiveFailures++;
+ }
+
+ this.updateHealth();
+ this.notifyConnectionChange();
+
+ return { success, responseTime };
+ } catch (error) {
+ const responseTime = Date.now() - startTime;
+ this.health.consecutiveFailures++;
+ this.updateHealth();
+ this.notifyConnectionChange();
+
+ return {
+ success: false,
+ responseTime,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+ }
+
+ /**
+ * Call MCP tool with automatic retry and health monitoring
+ */
+ async callTool(
+ toolName: string,
+ arguments_: Record
+ ): Promise {
+ if (!this.mcpClient) {
+ throw new Error('MCP client not initialized');
+ }
+
+ const startTime = Date.now();
+
+ try {
+ const result = await this.mcpClient.callTool(toolName, arguments_);
+ const responseTime = Date.now() - startTime;
+
+ this.health.lastSuccessfulCall = new Date();
+ this.health.consecutiveFailures = 0;
+ this.updateAverageResponseTime(responseTime);
+ this.updateHealth();
+ this.notifyConnectionChange();
+
+ return result;
+ } catch (error) {
+ this.health.consecutiveFailures++;
+ this.updateHealth();
+
+ // Attempt reconnection if connection seems lost
+ if (this.health.consecutiveFailures >= 3 && !this.isReconnecting) {
+ console.log(
+ 'Multiple consecutive failures detected, attempting reconnection...'
+ );
+ this.reconnectWithBackoff().catch((err) => {
+ console.error('Reconnection failed:', err);
+ });
+ }
+
+ this.notifyConnectionChange();
+ throw error;
+ }
+ }
+
+ /**
+ * Update configuration and reconnect
+ */
+ async updateConfig(newConfig: MCPConfig): Promise {
+ this.config = newConfig;
+
+ await this.disconnect();
+ this.mcpClient = new MCPClientManager(newConfig);
+
+ // Attempt to reconnect with new config
+ try {
+ await this.connect();
+ } catch (error) {
+ console.error('Failed to connect with new configuration:', error);
+ }
+ }
+
+ /**
+ * Start health monitoring
+ */
+ private startHealthMonitoring(): void {
+ this.stopHealthMonitoring();
+
+ this.healthCheckInterval = setInterval(async () => {
+ try {
+ await this.testConnection();
+ } catch (error) {
+ console.error('Health check failed:', error);
+ }
+ }, 15000); // Check every 15 seconds
+ }
+
+ /**
+ * Stop health monitoring
+ */
+ private stopHealthMonitoring(): void {
+ if (this.healthCheckInterval) {
+ clearInterval(this.healthCheckInterval);
+ this.healthCheckInterval = null;
+ }
+ }
+
+ /**
+ * Handle connection failure with exponential backoff
+ */
+ private async handleConnectionFailure(error: any): Promise {
+ this.health.consecutiveFailures++;
+ this.updateHealth();
+ this.notifyConnectionChange();
+
+ if (
+ this.reconnectAttempts < this.maxReconnectAttempts &&
+ !this.isReconnecting
+ ) {
+ await this.reconnectWithBackoff();
+ }
+ }
+
+ /**
+ * Reconnect with exponential backoff
+ */
+ private async reconnectWithBackoff(): Promise {
+ if (this.isReconnecting) {
+ return;
+ }
+
+ this.isReconnecting = true;
+ this.reconnectAttempts++;
+
+ const backoffMs = Math.min(
+ this.reconnectBackoffMs * Math.pow(2, this.reconnectAttempts - 1),
+ this.maxBackoffMs
+ );
+
+ console.log(
+ `Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${backoffMs}ms...`
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
+
+ try {
+ await this.connect();
+ } catch (error) {
+ console.error(
+ `Reconnection attempt ${this.reconnectAttempts} failed:`,
+ error
+ );
+
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ this.isReconnecting = false;
+ vscode.window.showErrorMessage(
+ `Failed to reconnect to Task Master after ${this.maxReconnectAttempts} attempts. Please check your configuration and try manually reconnecting.`
+ );
+ } else {
+ // Try again
+ await this.reconnectWithBackoff();
+ }
+ }
+ }
+
+ /**
+ * Update health metrics
+ */
+ private updateHealth(): void {
+ const status = this.getStatus();
+ this.health.isHealthy =
+ status.isRunning && this.health.consecutiveFailures < 3;
+
+ if (this.startTime) {
+ this.health.uptime = Date.now() - this.startTime.getTime();
+ }
+ }
+
+ /**
+ * Update average response time
+ */
+ private updateAverageResponseTime(responseTime: number): void {
+ // Simple moving average calculation
+ if (this.health.averageResponseTime === 0) {
+ this.health.averageResponseTime = responseTime;
+ } else {
+ this.health.averageResponseTime =
+ this.health.averageResponseTime * 0.8 + responseTime * 0.2;
+ }
+ }
+
+ /**
+ * Log connection event
+ */
+ private logEvent(event: ConnectionEvent): void {
+ this.connectionEvents.push(event);
+
+ // Keep only last 100 events
+ if (this.connectionEvents.length > 100) {
+ this.connectionEvents = this.connectionEvents.slice(-100);
+ }
+
+ if (this.onConnectionEvent) {
+ this.onConnectionEvent(event);
+ }
+ }
+
+ /**
+ * Notify connection change
+ */
+ private notifyConnectionChange(): void {
+ if (this.onConnectionChange) {
+ this.onConnectionChange(this.getStatus(), this.getHealth());
+ }
+ }
+}
diff --git a/apps/extension/src/utils/errorHandler.ts b/apps/extension/src/utils/errorHandler.ts
index 917fa554..94cbbeec 100644
--- a/apps/extension/src/utils/errorHandler.ts
+++ b/apps/extension/src/utils/errorHandler.ts
@@ -1,564 +1,603 @@
import * as vscode from 'vscode';
-import { shouldShowNotification, getNotificationType, getToastDuration } from './notificationPreferences';
+import {
+ shouldShowNotification,
+ getNotificationType,
+ getToastDuration
+} from './notificationPreferences';
export enum ErrorSeverity {
- LOW = 'low',
- MEDIUM = 'medium',
- HIGH = 'high',
- CRITICAL = 'critical'
+ LOW = 'low',
+ MEDIUM = 'medium',
+ HIGH = 'high',
+ CRITICAL = 'critical'
}
export enum ErrorCategory {
- MCP_CONNECTION = 'mcp_connection',
- CONFIGURATION = 'configuration',
- TASK_LOADING = 'task_loading',
- UI_RENDERING = 'ui_rendering',
- VALIDATION = 'validation',
- NETWORK = 'network',
- INTERNAL = 'internal',
- TASK_MASTER_API = 'TASK_MASTER_API',
- DATA_VALIDATION = 'DATA_VALIDATION',
- DATA_PARSING = 'DATA_PARSING',
- TASK_DATA_CORRUPTION = 'TASK_DATA_CORRUPTION',
- VSCODE_API = 'VSCODE_API',
- WEBVIEW = 'WEBVIEW',
- EXTENSION_HOST = 'EXTENSION_HOST',
- USER_INTERACTION = 'USER_INTERACTION',
- DRAG_DROP = 'DRAG_DROP',
- COMPONENT_RENDER = 'COMPONENT_RENDER',
- PERMISSION = 'PERMISSION',
- FILE_SYSTEM = 'FILE_SYSTEM',
- UNKNOWN = 'UNKNOWN'
+ MCP_CONNECTION = 'mcp_connection',
+ CONFIGURATION = 'configuration',
+ TASK_LOADING = 'task_loading',
+ UI_RENDERING = 'ui_rendering',
+ VALIDATION = 'validation',
+ NETWORK = 'network',
+ INTERNAL = 'internal',
+ TASK_MASTER_API = 'TASK_MASTER_API',
+ DATA_VALIDATION = 'DATA_VALIDATION',
+ DATA_PARSING = 'DATA_PARSING',
+ TASK_DATA_CORRUPTION = 'TASK_DATA_CORRUPTION',
+ VSCODE_API = 'VSCODE_API',
+ WEBVIEW = 'WEBVIEW',
+ EXTENSION_HOST = 'EXTENSION_HOST',
+ USER_INTERACTION = 'USER_INTERACTION',
+ DRAG_DROP = 'DRAG_DROP',
+ COMPONENT_RENDER = 'COMPONENT_RENDER',
+ PERMISSION = 'PERMISSION',
+ FILE_SYSTEM = 'FILE_SYSTEM',
+ UNKNOWN = 'UNKNOWN'
}
export enum NotificationType {
- VSCODE_INFO = 'VSCODE_INFO',
- VSCODE_WARNING = 'VSCODE_WARNING',
- VSCODE_ERROR = 'VSCODE_ERROR',
- TOAST_SUCCESS = 'TOAST_SUCCESS',
- TOAST_INFO = 'TOAST_INFO',
- TOAST_WARNING = 'TOAST_WARNING',
- TOAST_ERROR = 'TOAST_ERROR',
- CONSOLE_ONLY = 'CONSOLE_ONLY',
- SILENT = 'SILENT'
+ VSCODE_INFO = 'VSCODE_INFO',
+ VSCODE_WARNING = 'VSCODE_WARNING',
+ VSCODE_ERROR = 'VSCODE_ERROR',
+ TOAST_SUCCESS = 'TOAST_SUCCESS',
+ TOAST_INFO = 'TOAST_INFO',
+ TOAST_WARNING = 'TOAST_WARNING',
+ TOAST_ERROR = 'TOAST_ERROR',
+ CONSOLE_ONLY = 'CONSOLE_ONLY',
+ SILENT = 'SILENT'
}
export interface ErrorContext {
- // Core error information
- category: ErrorCategory;
- severity: ErrorSeverity;
- message: string;
- originalError?: Error | unknown;
-
- // Contextual information
- operation?: string; // What operation was being performed
- taskId?: string; // Related task ID if applicable
- userId?: string; // User context if applicable
- sessionId?: string; // Session context
-
- // Technical details
- stackTrace?: string;
- userAgent?: string;
- timestamp?: number;
-
- // Recovery information
- isRecoverable?: boolean;
- suggestedActions?: string[];
- documentationLink?: string;
-
- // Notification preferences
- notificationType?: NotificationType;
- showToUser?: boolean;
- logToConsole?: boolean;
- logToFile?: boolean;
+ // Core error information
+ category: ErrorCategory;
+ severity: ErrorSeverity;
+ message: string;
+ originalError?: Error | unknown;
+
+ // Contextual information
+ operation?: string; // What operation was being performed
+ taskId?: string; // Related task ID if applicable
+ userId?: string; // User context if applicable
+ sessionId?: string; // Session context
+
+ // Technical details
+ stackTrace?: string;
+ userAgent?: string;
+ timestamp?: number;
+
+ // Recovery information
+ isRecoverable?: boolean;
+ suggestedActions?: string[];
+ documentationLink?: string;
+
+ // Notification preferences
+ notificationType?: NotificationType;
+ showToUser?: boolean;
+ logToConsole?: boolean;
+ logToFile?: boolean;
}
export interface ErrorDetails {
- code: string;
- message: string;
- category: ErrorCategory;
- severity: ErrorSeverity;
- timestamp: Date;
- context?: Record;
- stack?: string;
- userAction?: string;
- recovery?: {
- automatic: boolean;
- action?: () => Promise;
- description?: string;
- };
+ code: string;
+ message: string;
+ category: ErrorCategory;
+ severity: ErrorSeverity;
+ timestamp: Date;
+ context?: Record;
+ stack?: string;
+ userAction?: string;
+ recovery?: {
+ automatic: boolean;
+ action?: () => Promise;
+ description?: string;
+ };
}
export interface ErrorLogEntry {
- id: string;
- error: ErrorDetails;
- resolved: boolean;
- resolvedAt?: Date;
- attempts: number;
- lastAttempt?: Date;
+ id: string;
+ error: ErrorDetails;
+ resolved: boolean;
+ resolvedAt?: Date;
+ attempts: number;
+ lastAttempt?: Date;
}
/**
* Base class for all Task Master errors
*/
export abstract class TaskMasterError extends Error {
- public readonly code: string;
- public readonly category: ErrorCategory;
- public readonly severity: ErrorSeverity;
- public readonly timestamp: Date;
- public readonly context?: Record;
- public readonly userAction?: string;
- public readonly recovery?: {
- automatic: boolean;
- action?: () => Promise;
- description?: string;
- };
+ public readonly code: string;
+ public readonly category: ErrorCategory;
+ public readonly severity: ErrorSeverity;
+ public readonly timestamp: Date;
+ public readonly context?: Record;
+ public readonly userAction?: string;
+ public readonly recovery?: {
+ automatic: boolean;
+ action?: () => Promise;
+ description?: string;
+ };
- constructor(
- message: string,
- code: string,
- category: ErrorCategory,
- severity: ErrorSeverity = ErrorSeverity.MEDIUM,
- context?: Record,
- userAction?: string,
- recovery?: {
- automatic: boolean;
- action?: () => Promise;
- description?: string;
- }
- ) {
- super(message);
- this.name = this.constructor.name;
- this.code = code;
- this.category = category;
- this.severity = severity;
- this.timestamp = new Date();
- this.context = context;
- this.userAction = userAction;
- this.recovery = recovery;
+ constructor(
+ message: string,
+ code: string,
+ category: ErrorCategory,
+ severity: ErrorSeverity = ErrorSeverity.MEDIUM,
+ context?: Record,
+ userAction?: string,
+ recovery?: {
+ automatic: boolean;
+ action?: () => Promise;
+ description?: string;
+ }
+ ) {
+ super(message);
+ this.name = this.constructor.name;
+ this.code = code;
+ this.category = category;
+ this.severity = severity;
+ this.timestamp = new Date();
+ this.context = context;
+ this.userAction = userAction;
+ this.recovery = recovery;
- // Capture stack trace
- if (Error.captureStackTrace) {
- Error.captureStackTrace(this, this.constructor);
- }
- }
+ // Capture stack trace
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, this.constructor);
+ }
+ }
- public toErrorDetails(): ErrorDetails {
- return {
- code: this.code,
- message: this.message,
- category: this.category,
- severity: this.severity,
- timestamp: this.timestamp,
- context: this.context,
- stack: this.stack,
- userAction: this.userAction,
- recovery: this.recovery
- };
- }
+ public toErrorDetails(): ErrorDetails {
+ return {
+ code: this.code,
+ message: this.message,
+ category: this.category,
+ severity: this.severity,
+ timestamp: this.timestamp,
+ context: this.context,
+ stack: this.stack,
+ userAction: this.userAction,
+ recovery: this.recovery
+ };
+ }
}
/**
* MCP Connection related errors
*/
export class MCPConnectionError extends TaskMasterError {
- constructor(
- message: string,
- code: string = 'MCP_CONNECTION_FAILED',
- context?: Record,
- recovery?: { automatic: boolean; action?: () => Promise; description?: string }
- ) {
- super(
- message,
- code,
- ErrorCategory.MCP_CONNECTION,
- ErrorSeverity.HIGH,
- context,
- 'Check your Task Master configuration and ensure the MCP server is accessible.',
- recovery
- );
- }
+ constructor(
+ message: string,
+ code: string = 'MCP_CONNECTION_FAILED',
+ context?: Record,
+ recovery?: {
+ automatic: boolean;
+ action?: () => Promise;
+ description?: string;
+ }
+ ) {
+ super(
+ message,
+ code,
+ ErrorCategory.MCP_CONNECTION,
+ ErrorSeverity.HIGH,
+ context,
+ 'Check your Task Master configuration and ensure the MCP server is accessible.',
+ recovery
+ );
+ }
}
/**
* Configuration related errors
*/
export class ConfigurationError extends TaskMasterError {
- constructor(
- message: string,
- code: string = 'CONFIGURATION_INVALID',
- context?: Record
- ) {
- super(
- message,
- code,
- ErrorCategory.CONFIGURATION,
- ErrorSeverity.MEDIUM,
- context,
- 'Check your Task Master configuration in VS Code settings.'
- );
- }
+ constructor(
+ message: string,
+ code: string = 'CONFIGURATION_INVALID',
+ context?: Record
+ ) {
+ super(
+ message,
+ code,
+ ErrorCategory.CONFIGURATION,
+ ErrorSeverity.MEDIUM,
+ context,
+ 'Check your Task Master configuration in VS Code settings.'
+ );
+ }
}
/**
* Task loading related errors
*/
export class TaskLoadingError extends TaskMasterError {
- constructor(
- message: string,
- code: string = 'TASK_LOADING_FAILED',
- context?: Record,
- recovery?: { automatic: boolean; action?: () => Promise; description?: string }
- ) {
- super(
- message,
- code,
- ErrorCategory.TASK_LOADING,
- ErrorSeverity.MEDIUM,
- context,
- 'Try refreshing the task list or check your project configuration.',
- recovery
- );
- }
+ constructor(
+ message: string,
+ code: string = 'TASK_LOADING_FAILED',
+ context?: Record,
+ recovery?: {
+ automatic: boolean;
+ action?: () => Promise;
+ description?: string;
+ }
+ ) {
+ super(
+ message,
+ code,
+ ErrorCategory.TASK_LOADING,
+ ErrorSeverity.MEDIUM,
+ context,
+ 'Try refreshing the task list or check your project configuration.',
+ recovery
+ );
+ }
}
/**
* UI rendering related errors
*/
export class UIRenderingError extends TaskMasterError {
- constructor(
- message: string,
- code: string = 'UI_RENDERING_FAILED',
- context?: Record
- ) {
- super(
- message,
- code,
- ErrorCategory.UI_RENDERING,
- ErrorSeverity.LOW,
- context,
- 'Try closing and reopening the Kanban board.'
- );
- }
+ constructor(
+ message: string,
+ code: string = 'UI_RENDERING_FAILED',
+ context?: Record
+ ) {
+ super(
+ message,
+ code,
+ ErrorCategory.UI_RENDERING,
+ ErrorSeverity.LOW,
+ context,
+ 'Try closing and reopening the Kanban board.'
+ );
+ }
}
/**
* Network related errors
*/
export class NetworkError extends TaskMasterError {
- constructor(
- message: string,
- code: string = 'NETWORK_ERROR',
- context?: Record,
- recovery?: { automatic: boolean; action?: () => Promise; description?: string }
- ) {
- super(
- message,
- code,
- ErrorCategory.NETWORK,
- ErrorSeverity.MEDIUM,
- context,
- 'Check your network connection and firewall settings.',
- recovery
- );
- }
+ constructor(
+ message: string,
+ code: string = 'NETWORK_ERROR',
+ context?: Record,
+ recovery?: {
+ automatic: boolean;
+ action?: () => Promise;
+ description?: string;
+ }
+ ) {
+ super(
+ message,
+ code,
+ ErrorCategory.NETWORK,
+ ErrorSeverity.MEDIUM,
+ context,
+ 'Check your network connection and firewall settings.',
+ recovery
+ );
+ }
}
/**
* Centralized error handler
*/
export class ErrorHandler {
- private static instance: ErrorHandler | null = null;
- private errorLog: ErrorLogEntry[] = [];
- private maxLogSize = 1000;
- private errorListeners: ((error: ErrorDetails) => void)[] = [];
+ private static instance: ErrorHandler | null = null;
+ private errorLog: ErrorLogEntry[] = [];
+ private maxLogSize = 1000;
+ private errorListeners: ((error: ErrorDetails) => void)[] = [];
- private constructor() {
- this.setupGlobalErrorHandlers();
- }
+ private constructor() {
+ this.setupGlobalErrorHandlers();
+ }
- static getInstance(): ErrorHandler {
- if (!ErrorHandler.instance) {
- ErrorHandler.instance = new ErrorHandler();
- }
- return ErrorHandler.instance;
- }
+ static getInstance(): ErrorHandler {
+ if (!ErrorHandler.instance) {
+ ErrorHandler.instance = new ErrorHandler();
+ }
+ return ErrorHandler.instance;
+ }
- /**
- * Handle an error with comprehensive logging and recovery
- */
- async handleError(error: Error | TaskMasterError, context?: Record): Promise {
- const errorDetails = this.createErrorDetails(error, context);
- const logEntry = this.logError(errorDetails);
+ /**
+ * Handle an error with comprehensive logging and recovery
+ */
+ async handleError(
+ error: Error | TaskMasterError,
+ context?: Record
+ ): Promise {
+ const errorDetails = this.createErrorDetails(error, context);
+ const logEntry = this.logError(errorDetails);
- // Notify listeners
- this.notifyErrorListeners(errorDetails);
+ // Notify listeners
+ this.notifyErrorListeners(errorDetails);
- // Show user notification based on severity
- await this.showUserNotification(errorDetails);
+ // Show user notification based on severity
+ await this.showUserNotification(errorDetails);
- // Attempt recovery if available
- if (errorDetails.recovery?.automatic && errorDetails.recovery.action) {
- try {
- await errorDetails.recovery.action();
- this.markErrorResolved(logEntry.id);
- } catch (recoveryError) {
- console.error('Error recovery failed:', recoveryError);
- logEntry.attempts++;
- logEntry.lastAttempt = new Date();
- }
- }
+ // Attempt recovery if available
+ if (errorDetails.recovery?.automatic && errorDetails.recovery.action) {
+ try {
+ await errorDetails.recovery.action();
+ this.markErrorResolved(logEntry.id);
+ } catch (recoveryError) {
+ console.error('Error recovery failed:', recoveryError);
+ logEntry.attempts++;
+ logEntry.lastAttempt = new Date();
+ }
+ }
- // Log to console with appropriate level
- this.logToConsole(errorDetails);
- }
+ // Log to console with appropriate level
+ this.logToConsole(errorDetails);
+ }
- /**
- * Handle critical errors that should stop execution
- */
- async handleCriticalError(error: Error | TaskMasterError, context?: Record): Promise {
- const errorDetails = this.createErrorDetails(error, context);
- errorDetails.severity = ErrorSeverity.CRITICAL;
+ /**
+ * Handle critical errors that should stop execution
+ */
+ async handleCriticalError(
+ error: Error | TaskMasterError,
+ context?: Record
+ ): Promise {
+ const errorDetails = this.createErrorDetails(error, context);
+ errorDetails.severity = ErrorSeverity.CRITICAL;
- await this.handleError(error, context);
+ await this.handleError(error, context);
- // Show critical error dialog
- const action = await vscode.window.showErrorMessage(
- `Critical Error in Task Master: ${errorDetails.message}`,
- 'View Details',
- 'Report Issue',
- 'Restart Extension'
- );
+ // Show critical error dialog
+ const action = await vscode.window.showErrorMessage(
+ `Critical Error in Task Master: ${errorDetails.message}`,
+ 'View Details',
+ 'Report Issue',
+ 'Restart Extension'
+ );
- switch (action) {
- case 'View Details':
- await this.showErrorDetails(errorDetails);
- break;
- case 'Report Issue':
- await this.openIssueReport(errorDetails);
- break;
- case 'Restart Extension':
- await vscode.commands.executeCommand('workbench.action.reloadWindow');
- break;
- }
- }
+ switch (action) {
+ case 'View Details':
+ await this.showErrorDetails(errorDetails);
+ break;
+ case 'Report Issue':
+ await this.openIssueReport(errorDetails);
+ break;
+ case 'Restart Extension':
+ await vscode.commands.executeCommand('workbench.action.reloadWindow');
+ break;
+ }
+ }
- /**
- * Add error event listener
- */
- onError(listener: (error: ErrorDetails) => void): void {
- this.errorListeners.push(listener);
- }
+ /**
+ * Add error event listener
+ */
+ onError(listener: (error: ErrorDetails) => void): void {
+ this.errorListeners.push(listener);
+ }
- /**
- * Remove error event listener
- */
- removeErrorListener(listener: (error: ErrorDetails) => void): void {
- const index = this.errorListeners.indexOf(listener);
- if (index !== -1) {
- this.errorListeners.splice(index, 1);
- }
- }
+ /**
+ * Remove error event listener
+ */
+ removeErrorListener(listener: (error: ErrorDetails) => void): void {
+ const index = this.errorListeners.indexOf(listener);
+ if (index !== -1) {
+ this.errorListeners.splice(index, 1);
+ }
+ }
- /**
- * Get error log
- */
- getErrorLog(category?: ErrorCategory, severity?: ErrorSeverity): ErrorLogEntry[] {
- let filteredLog = this.errorLog;
+ /**
+ * Get error log
+ */
+ getErrorLog(
+ category?: ErrorCategory,
+ severity?: ErrorSeverity
+ ): ErrorLogEntry[] {
+ let filteredLog = this.errorLog;
- if (category) {
- filteredLog = filteredLog.filter(entry => entry.error.category === category);
- }
+ if (category) {
+ filteredLog = filteredLog.filter(
+ (entry) => entry.error.category === category
+ );
+ }
- if (severity) {
- filteredLog = filteredLog.filter(entry => entry.error.severity === severity);
- }
+ if (severity) {
+ filteredLog = filteredLog.filter(
+ (entry) => entry.error.severity === severity
+ );
+ }
- return filteredLog.slice().reverse(); // Most recent first
- }
+ return filteredLog.slice().reverse(); // Most recent first
+ }
- /**
- * Clear error log
- */
- clearErrorLog(): void {
- this.errorLog = [];
- }
+ /**
+ * Clear error log
+ */
+ clearErrorLog(): void {
+ this.errorLog = [];
+ }
- /**
- * Export error log for debugging
- */
- exportErrorLog(): string {
- return JSON.stringify(this.errorLog, null, 2);
- }
+ /**
+ * Export error log for debugging
+ */
+ exportErrorLog(): string {
+ return JSON.stringify(this.errorLog, null, 2);
+ }
- /**
- * Create error details from error instance
- */
- private createErrorDetails(error: Error | TaskMasterError, context?: Record): ErrorDetails {
- if (error instanceof TaskMasterError) {
- const details = error.toErrorDetails();
- if (context) {
- details.context = { ...details.context, ...context };
- }
- return details;
- }
+ /**
+ * Create error details from error instance
+ */
+ private createErrorDetails(
+ error: Error | TaskMasterError,
+ context?: Record
+ ): ErrorDetails {
+ if (error instanceof TaskMasterError) {
+ const details = error.toErrorDetails();
+ if (context) {
+ details.context = { ...details.context, ...context };
+ }
+ return details;
+ }
- // Handle standard Error objects
- return {
- code: 'UNKNOWN_ERROR',
- message: error.message || 'An unknown error occurred',
- category: ErrorCategory.INTERNAL,
- severity: ErrorSeverity.MEDIUM,
- timestamp: new Date(),
- context: { ...context, errorName: error.name },
- stack: error.stack
- };
- }
+ // Handle standard Error objects
+ return {
+ code: 'UNKNOWN_ERROR',
+ message: error.message || 'An unknown error occurred',
+ category: ErrorCategory.INTERNAL,
+ severity: ErrorSeverity.MEDIUM,
+ timestamp: new Date(),
+ context: { ...context, errorName: error.name },
+ stack: error.stack
+ };
+ }
- /**
- * Log error to internal log
- */
- private logError(errorDetails: ErrorDetails): ErrorLogEntry {
- const logEntry: ErrorLogEntry = {
- id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
- error: errorDetails,
- resolved: false,
- attempts: 0
- };
+ /**
+ * Log error to internal log
+ */
+ private logError(errorDetails: ErrorDetails): ErrorLogEntry {
+ const logEntry: ErrorLogEntry = {
+ id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ error: errorDetails,
+ resolved: false,
+ attempts: 0
+ };
- this.errorLog.push(logEntry);
+ this.errorLog.push(logEntry);
- // Maintain log size limit
- if (this.errorLog.length > this.maxLogSize) {
- this.errorLog = this.errorLog.slice(-this.maxLogSize);
- }
+ // Maintain log size limit
+ if (this.errorLog.length > this.maxLogSize) {
+ this.errorLog = this.errorLog.slice(-this.maxLogSize);
+ }
- return logEntry;
- }
+ return logEntry;
+ }
- /**
- * Mark error as resolved
- */
- private markErrorResolved(errorId: string): void {
- const entry = this.errorLog.find(e => e.id === errorId);
- if (entry) {
- entry.resolved = true;
- entry.resolvedAt = new Date();
- }
- }
+ /**
+ * Mark error as resolved
+ */
+ private markErrorResolved(errorId: string): void {
+ const entry = this.errorLog.find((e) => e.id === errorId);
+ if (entry) {
+ entry.resolved = true;
+ entry.resolvedAt = new Date();
+ }
+ }
- /**
- * Show user notification based on error severity and user preferences
- */
- private async showUserNotification(errorDetails: ErrorDetails): Promise {
- // Check if user wants to see this notification
- if (!shouldShowNotification(errorDetails.category, errorDetails.severity)) {
- return;
- }
+ /**
+ * Show user notification based on error severity and user preferences
+ */
+ private async showUserNotification(
+ errorDetails: ErrorDetails
+ ): Promise {
+ // Check if user wants to see this notification
+ if (!shouldShowNotification(errorDetails.category, errorDetails.severity)) {
+ return;
+ }
- const notificationType = getNotificationType(errorDetails.category, errorDetails.severity);
- const message = errorDetails.userAction
- ? `${errorDetails.message} ${errorDetails.userAction}`
- : errorDetails.message;
+ const notificationType = getNotificationType(
+ errorDetails.category,
+ errorDetails.severity
+ );
+ const message = errorDetails.userAction
+ ? `${errorDetails.message} ${errorDetails.userAction}`
+ : errorDetails.message;
- // Handle different notification types based on user preferences
- switch (notificationType) {
- case 'VSCODE_ERROR':
- await vscode.window.showErrorMessage(message);
- break;
- case 'VSCODE_WARNING':
- await vscode.window.showWarningMessage(message);
- break;
- case 'VSCODE_INFO':
- await vscode.window.showInformationMessage(message);
- break;
- case 'TOAST_SUCCESS':
- case 'TOAST_INFO':
- case 'TOAST_WARNING':
- case 'TOAST_ERROR':
- // These will be handled by the webview toast system
- // The error listener in extension.ts will send these to webview
- break;
- case 'CONSOLE_ONLY':
- case 'SILENT':
- // No user notification, just console logging
- break;
- default:
- // Fallback to severity-based notifications
- switch (errorDetails.severity) {
- case ErrorSeverity.CRITICAL:
- await vscode.window.showErrorMessage(message);
- break;
- case ErrorSeverity.HIGH:
- await vscode.window.showErrorMessage(message);
- break;
- case ErrorSeverity.MEDIUM:
- await vscode.window.showWarningMessage(message);
- break;
- case ErrorSeverity.LOW:
- await vscode.window.showInformationMessage(message);
- break;
- }
- }
- }
+ // Handle different notification types based on user preferences
+ switch (notificationType) {
+ case 'VSCODE_ERROR':
+ await vscode.window.showErrorMessage(message);
+ break;
+ case 'VSCODE_WARNING':
+ await vscode.window.showWarningMessage(message);
+ break;
+ case 'VSCODE_INFO':
+ await vscode.window.showInformationMessage(message);
+ break;
+ case 'TOAST_SUCCESS':
+ case 'TOAST_INFO':
+ case 'TOAST_WARNING':
+ case 'TOAST_ERROR':
+ // These will be handled by the webview toast system
+ // The error listener in extension.ts will send these to webview
+ break;
+ case 'CONSOLE_ONLY':
+ case 'SILENT':
+ // No user notification, just console logging
+ break;
+ default:
+ // Fallback to severity-based notifications
+ switch (errorDetails.severity) {
+ case ErrorSeverity.CRITICAL:
+ await vscode.window.showErrorMessage(message);
+ break;
+ case ErrorSeverity.HIGH:
+ await vscode.window.showErrorMessage(message);
+ break;
+ case ErrorSeverity.MEDIUM:
+ await vscode.window.showWarningMessage(message);
+ break;
+ case ErrorSeverity.LOW:
+ await vscode.window.showInformationMessage(message);
+ break;
+ }
+ }
+ }
- /**
- * Log to console with appropriate level
- */
- private logToConsole(errorDetails: ErrorDetails): void {
- const logMessage = `[${errorDetails.category}] ${errorDetails.code}: ${errorDetails.message}`;
-
- switch (errorDetails.severity) {
- case ErrorSeverity.CRITICAL:
- case ErrorSeverity.HIGH:
- console.error(logMessage, errorDetails);
- break;
- case ErrorSeverity.MEDIUM:
- console.warn(logMessage, errorDetails);
- break;
- case ErrorSeverity.LOW:
- console.info(logMessage, errorDetails);
- break;
- }
- }
+ /**
+ * Log to console with appropriate level
+ */
+ private logToConsole(errorDetails: ErrorDetails): void {
+ const logMessage = `[${errorDetails.category}] ${errorDetails.code}: ${errorDetails.message}`;
- /**
- * Show detailed error information
- */
- private async showErrorDetails(errorDetails: ErrorDetails): Promise {
- const details = [
- `Error Code: ${errorDetails.code}`,
- `Category: ${errorDetails.category}`,
- `Severity: ${errorDetails.severity}`,
- `Time: ${errorDetails.timestamp.toISOString()}`,
- `Message: ${errorDetails.message}`
- ];
+ switch (errorDetails.severity) {
+ case ErrorSeverity.CRITICAL:
+ case ErrorSeverity.HIGH:
+ console.error(logMessage, errorDetails);
+ break;
+ case ErrorSeverity.MEDIUM:
+ console.warn(logMessage, errorDetails);
+ break;
+ case ErrorSeverity.LOW:
+ console.info(logMessage, errorDetails);
+ break;
+ }
+ }
- if (errorDetails.context) {
- details.push(`Context: ${JSON.stringify(errorDetails.context, null, 2)}`);
- }
+ /**
+ * Show detailed error information
+ */
+ private async showErrorDetails(errorDetails: ErrorDetails): Promise {
+ const details = [
+ `Error Code: ${errorDetails.code}`,
+ `Category: ${errorDetails.category}`,
+ `Severity: ${errorDetails.severity}`,
+ `Time: ${errorDetails.timestamp.toISOString()}`,
+ `Message: ${errorDetails.message}`
+ ];
- if (errorDetails.stack) {
- details.push(`Stack Trace: ${errorDetails.stack}`);
- }
+ if (errorDetails.context) {
+ details.push(`Context: ${JSON.stringify(errorDetails.context, null, 2)}`);
+ }
- const content = details.join('\n\n');
-
- // Create temporary document to show error details
- const doc = await vscode.workspace.openTextDocument({
- content,
- language: 'plaintext'
- });
-
- await vscode.window.showTextDocument(doc);
- }
+ if (errorDetails.stack) {
+ details.push(`Stack Trace: ${errorDetails.stack}`);
+ }
- /**
- * Open GitHub issue report
- */
- private async openIssueReport(errorDetails: ErrorDetails): Promise {
- const issueTitle = encodeURIComponent(`Error: ${errorDetails.code} - ${errorDetails.message}`);
- const issueBody = encodeURIComponent(`
+ const content = details.join('\n\n');
+
+ // Create temporary document to show error details
+ const doc = await vscode.workspace.openTextDocument({
+ content,
+ language: 'plaintext'
+ });
+
+ await vscode.window.showTextDocument(doc);
+ }
+
+ /**
+ * Open GitHub issue report
+ */
+ private async openIssueReport(errorDetails: ErrorDetails): Promise {
+ const issueTitle = encodeURIComponent(
+ `Error: ${errorDetails.code} - ${errorDetails.message}`
+ );
+ const issueBody = encodeURIComponent(`
**Error Details:**
- Code: ${errorDetails.code}
- Category: ${errorDetails.category}
@@ -583,205 +622,236 @@ ${errorDetails.context ? JSON.stringify(errorDetails.context, null, 2) : 'None'}
`);
- const issueUrl = `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${issueTitle}&body=${issueBody}`;
- await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
- }
+ const issueUrl = `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${issueTitle}&body=${issueBody}`;
+ await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
+ }
- /**
- * Notify error listeners
- */
- private notifyErrorListeners(errorDetails: ErrorDetails): void {
- this.errorListeners.forEach(listener => {
- try {
- listener(errorDetails);
- } catch (error) {
- console.error('Error in error listener:', error);
- }
- });
- }
+ /**
+ * Notify error listeners
+ */
+ private notifyErrorListeners(errorDetails: ErrorDetails): void {
+ this.errorListeners.forEach((listener) => {
+ try {
+ listener(errorDetails);
+ } catch (error) {
+ console.error('Error in error listener:', error);
+ }
+ });
+ }
- /**
- * Setup global error handlers
- */
- private setupGlobalErrorHandlers(): void {
- // Handle unhandled promise rejections
- process.on('unhandledRejection', (reason, promise) => {
- // Create a concrete error class for internal errors
- class InternalError extends TaskMasterError {
- constructor(message: string, code: string, severity: ErrorSeverity, context?: Record) {
- super(message, code, ErrorCategory.INTERNAL, severity, context);
- }
- }
-
- const error = new InternalError(
- 'Unhandled Promise Rejection',
- 'UNHANDLED_REJECTION',
- ErrorSeverity.HIGH,
- { reason: String(reason), promise: String(promise) }
- );
- this.handleError(error);
- });
+ /**
+ * Setup global error handlers
+ */
+ private setupGlobalErrorHandlers(): void {
+ // Handle unhandled promise rejections
+ process.on('unhandledRejection', (reason, promise) => {
+ // Create a concrete error class for internal errors
+ class InternalError extends TaskMasterError {
+ constructor(
+ message: string,
+ code: string,
+ severity: ErrorSeverity,
+ context?: Record
+ ) {
+ super(message, code, ErrorCategory.INTERNAL, severity, context);
+ }
+ }
- // Handle uncaught exceptions
- process.on('uncaughtException', (error) => {
- // Create a concrete error class for internal errors
- class InternalError extends TaskMasterError {
- constructor(message: string, code: string, severity: ErrorSeverity, context?: Record) {
- super(message, code, ErrorCategory.INTERNAL, severity, context);
- }
- }
-
- const taskMasterError = new InternalError(
- 'Uncaught Exception',
- 'UNCAUGHT_EXCEPTION',
- ErrorSeverity.CRITICAL,
- { originalError: error.message, stack: error.stack }
- );
- this.handleCriticalError(taskMasterError);
- });
- }
+ const error = new InternalError(
+ 'Unhandled Promise Rejection',
+ 'UNHANDLED_REJECTION',
+ ErrorSeverity.HIGH,
+ { reason: String(reason), promise: String(promise) }
+ );
+ this.handleError(error);
+ });
+
+ // Handle uncaught exceptions
+ process.on('uncaughtException', (error) => {
+ // Create a concrete error class for internal errors
+ class InternalError extends TaskMasterError {
+ constructor(
+ message: string,
+ code: string,
+ severity: ErrorSeverity,
+ context?: Record
+ ) {
+ super(message, code, ErrorCategory.INTERNAL, severity, context);
+ }
+ }
+
+ const taskMasterError = new InternalError(
+ 'Uncaught Exception',
+ 'UNCAUGHT_EXCEPTION',
+ ErrorSeverity.CRITICAL,
+ { originalError: error.message, stack: error.stack }
+ );
+ this.handleCriticalError(taskMasterError);
+ });
+ }
}
/**
* Utility functions for error handling
*/
export function getErrorHandler(): ErrorHandler {
- return ErrorHandler.getInstance();
+ return ErrorHandler.getInstance();
}
-export function createRecoveryAction(action: () => Promise, description: string) {
- return {
- automatic: false,
- action,
- description
- };
+export function createRecoveryAction(
+ action: () => Promise,
+ description: string
+) {
+ return {
+ automatic: false,
+ action,
+ description
+ };
}
-export function createAutoRecoveryAction(action: () => Promise, description: string) {
- return {
- automatic: true,
- action,
- description
- };
+export function createAutoRecoveryAction(
+ action: () => Promise,
+ description: string
+) {
+ return {
+ automatic: true,
+ action,
+ description
+ };
}
// Default error categorization rules
export const ERROR_CATEGORIZATION_RULES: Record = {
- // Network patterns
- 'ECONNREFUSED': ErrorCategory.NETWORK,
- 'ENOTFOUND': ErrorCategory.NETWORK,
- 'ETIMEDOUT': ErrorCategory.NETWORK,
- 'Network request failed': ErrorCategory.NETWORK,
- 'fetch failed': ErrorCategory.NETWORK,
-
- // MCP patterns
- 'MCP': ErrorCategory.MCP_CONNECTION,
- 'Task Master': ErrorCategory.TASK_MASTER_API,
- 'polling': ErrorCategory.TASK_MASTER_API,
-
- // VS Code patterns
- 'vscode': ErrorCategory.VSCODE_API,
- 'webview': ErrorCategory.WEBVIEW,
- 'extension': ErrorCategory.EXTENSION_HOST,
-
- // Data patterns
- 'JSON': ErrorCategory.DATA_PARSING,
- 'parse': ErrorCategory.DATA_PARSING,
- 'validation': ErrorCategory.DATA_VALIDATION,
- 'invalid': ErrorCategory.DATA_VALIDATION,
-
- // Permission patterns
- 'EACCES': ErrorCategory.PERMISSION,
- 'EPERM': ErrorCategory.PERMISSION,
- 'permission': ErrorCategory.PERMISSION,
-
- // File system patterns
- 'ENOENT': ErrorCategory.FILE_SYSTEM,
- 'EISDIR': ErrorCategory.FILE_SYSTEM,
- 'file': ErrorCategory.FILE_SYSTEM
+ // Network patterns
+ ECONNREFUSED: ErrorCategory.NETWORK,
+ ENOTFOUND: ErrorCategory.NETWORK,
+ ETIMEDOUT: ErrorCategory.NETWORK,
+ 'Network request failed': ErrorCategory.NETWORK,
+ 'fetch failed': ErrorCategory.NETWORK,
+
+ // MCP patterns
+ MCP: ErrorCategory.MCP_CONNECTION,
+ 'Task Master': ErrorCategory.TASK_MASTER_API,
+ polling: ErrorCategory.TASK_MASTER_API,
+
+ // VS Code patterns
+ vscode: ErrorCategory.VSCODE_API,
+ webview: ErrorCategory.WEBVIEW,
+ extension: ErrorCategory.EXTENSION_HOST,
+
+ // Data patterns
+ JSON: ErrorCategory.DATA_PARSING,
+ parse: ErrorCategory.DATA_PARSING,
+ validation: ErrorCategory.DATA_VALIDATION,
+ invalid: ErrorCategory.DATA_VALIDATION,
+
+ // Permission patterns
+ EACCES: ErrorCategory.PERMISSION,
+ EPERM: ErrorCategory.PERMISSION,
+ permission: ErrorCategory.PERMISSION,
+
+ // File system patterns
+ ENOENT: ErrorCategory.FILE_SYSTEM,
+ EISDIR: ErrorCategory.FILE_SYSTEM,
+ file: ErrorCategory.FILE_SYSTEM
};
-// Severity mapping based on error categories
+// Severity mapping based on error categories
export const CATEGORY_SEVERITY_MAPPING: Record = {
- [ErrorCategory.NETWORK]: ErrorSeverity.MEDIUM,
- [ErrorCategory.MCP_CONNECTION]: ErrorSeverity.HIGH,
- [ErrorCategory.TASK_MASTER_API]: ErrorSeverity.HIGH,
- [ErrorCategory.DATA_VALIDATION]: ErrorSeverity.MEDIUM,
- [ErrorCategory.DATA_PARSING]: ErrorSeverity.HIGH,
- [ErrorCategory.TASK_DATA_CORRUPTION]: ErrorSeverity.CRITICAL,
- [ErrorCategory.VSCODE_API]: ErrorSeverity.HIGH,
- [ErrorCategory.WEBVIEW]: ErrorSeverity.MEDIUM,
- [ErrorCategory.EXTENSION_HOST]: ErrorSeverity.CRITICAL,
- [ErrorCategory.USER_INTERACTION]: ErrorSeverity.LOW,
- [ErrorCategory.DRAG_DROP]: ErrorSeverity.MEDIUM,
- [ErrorCategory.COMPONENT_RENDER]: ErrorSeverity.MEDIUM,
- [ErrorCategory.PERMISSION]: ErrorSeverity.CRITICAL,
- [ErrorCategory.FILE_SYSTEM]: ErrorSeverity.HIGH,
- [ErrorCategory.CONFIGURATION]: ErrorSeverity.MEDIUM,
- [ErrorCategory.UNKNOWN]: ErrorSeverity.HIGH,
- // Legacy mappings for existing categories
- [ErrorCategory.TASK_LOADING]: ErrorSeverity.HIGH,
- [ErrorCategory.UI_RENDERING]: ErrorSeverity.MEDIUM,
- [ErrorCategory.VALIDATION]: ErrorSeverity.MEDIUM,
- [ErrorCategory.INTERNAL]: ErrorSeverity.HIGH
+ [ErrorCategory.NETWORK]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.MCP_CONNECTION]: ErrorSeverity.HIGH,
+ [ErrorCategory.TASK_MASTER_API]: ErrorSeverity.HIGH,
+ [ErrorCategory.DATA_VALIDATION]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.DATA_PARSING]: ErrorSeverity.HIGH,
+ [ErrorCategory.TASK_DATA_CORRUPTION]: ErrorSeverity.CRITICAL,
+ [ErrorCategory.VSCODE_API]: ErrorSeverity.HIGH,
+ [ErrorCategory.WEBVIEW]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.EXTENSION_HOST]: ErrorSeverity.CRITICAL,
+ [ErrorCategory.USER_INTERACTION]: ErrorSeverity.LOW,
+ [ErrorCategory.DRAG_DROP]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.COMPONENT_RENDER]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.PERMISSION]: ErrorSeverity.CRITICAL,
+ [ErrorCategory.FILE_SYSTEM]: ErrorSeverity.HIGH,
+ [ErrorCategory.CONFIGURATION]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.UNKNOWN]: ErrorSeverity.HIGH,
+ // Legacy mappings for existing categories
+ [ErrorCategory.TASK_LOADING]: ErrorSeverity.HIGH,
+ [ErrorCategory.UI_RENDERING]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.VALIDATION]: ErrorSeverity.MEDIUM,
+ [ErrorCategory.INTERNAL]: ErrorSeverity.HIGH
};
// Notification type mapping based on severity
-export const SEVERITY_NOTIFICATION_MAPPING: Record = {
- [ErrorSeverity.LOW]: NotificationType.TOAST_INFO,
- [ErrorSeverity.MEDIUM]: NotificationType.TOAST_WARNING,
- [ErrorSeverity.HIGH]: NotificationType.VSCODE_WARNING,
- [ErrorSeverity.CRITICAL]: NotificationType.VSCODE_ERROR
+export const SEVERITY_NOTIFICATION_MAPPING: Record<
+ ErrorSeverity,
+ NotificationType
+> = {
+ [ErrorSeverity.LOW]: NotificationType.TOAST_INFO,
+ [ErrorSeverity.MEDIUM]: NotificationType.TOAST_WARNING,
+ [ErrorSeverity.HIGH]: NotificationType.VSCODE_WARNING,
+ [ErrorSeverity.CRITICAL]: NotificationType.VSCODE_ERROR
};
/**
* Automatically categorize an error based on its message and type
*/
-export function categorizeError(error: Error | unknown, operation?: string): ErrorCategory {
- const errorMessage = error instanceof Error ? error.message : String(error);
- const errorStack = error instanceof Error ? error.stack : undefined;
- const searchText = `${errorMessage} ${errorStack || ''} ${operation || ''}`.toLowerCase();
-
- for (const [pattern, category] of Object.entries(ERROR_CATEGORIZATION_RULES)) {
- if (searchText.includes(pattern.toLowerCase())) {
- return category;
- }
- }
-
- return ErrorCategory.UNKNOWN;
+export function categorizeError(
+ error: Error | unknown,
+ operation?: string
+): ErrorCategory {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ const errorStack = error instanceof Error ? error.stack : undefined;
+ const searchText =
+ `${errorMessage} ${errorStack || ''} ${operation || ''}`.toLowerCase();
+
+ for (const [pattern, category] of Object.entries(
+ ERROR_CATEGORIZATION_RULES
+ )) {
+ if (searchText.includes(pattern.toLowerCase())) {
+ return category;
+ }
+ }
+
+ return ErrorCategory.UNKNOWN;
}
export function getSuggestedSeverity(category: ErrorCategory): ErrorSeverity {
- return CATEGORY_SEVERITY_MAPPING[category] || ErrorSeverity.HIGH;
+ return CATEGORY_SEVERITY_MAPPING[category] || ErrorSeverity.HIGH;
}
-export function getSuggestedNotificationType(severity: ErrorSeverity): NotificationType {
- return SEVERITY_NOTIFICATION_MAPPING[severity] || NotificationType.CONSOLE_ONLY;
+export function getSuggestedNotificationType(
+ severity: ErrorSeverity
+): NotificationType {
+ return (
+ SEVERITY_NOTIFICATION_MAPPING[severity] || NotificationType.CONSOLE_ONLY
+ );
}
export function createErrorContext(
- error: Error | unknown,
- operation?: string,
- overrides?: Partial
+ error: Error | unknown,
+ operation?: string,
+ overrides?: Partial
): ErrorContext {
- const category = categorizeError(error, operation);
- const severity = getSuggestedSeverity(category);
- const notificationType = getSuggestedNotificationType(severity);
-
- const baseContext: ErrorContext = {
- category,
- severity,
- message: error instanceof Error ? error.message : String(error),
- originalError: error,
- operation,
- timestamp: Date.now(),
- stackTrace: error instanceof Error ? error.stack : undefined,
- isRecoverable: severity !== ErrorSeverity.CRITICAL,
- notificationType,
- showToUser: severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL,
- logToConsole: true,
- logToFile: severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL
- };
-
- return { ...baseContext, ...overrides };
-}
\ No newline at end of file
+ const category = categorizeError(error, operation);
+ const severity = getSuggestedSeverity(category);
+ const notificationType = getSuggestedNotificationType(severity);
+
+ const baseContext: ErrorContext = {
+ category,
+ severity,
+ message: error instanceof Error ? error.message : String(error),
+ originalError: error,
+ operation,
+ timestamp: Date.now(),
+ stackTrace: error instanceof Error ? error.stack : undefined,
+ isRecoverable: severity !== ErrorSeverity.CRITICAL,
+ notificationType,
+ showToUser:
+ severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL,
+ logToConsole: true,
+ logToFile:
+ severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL
+ };
+
+ return { ...baseContext, ...overrides };
+}
diff --git a/apps/extension/src/utils/mcpClient.ts b/apps/extension/src/utils/mcpClient.ts
index 724da466..ca05412b 100644
--- a/apps/extension/src/utils/mcpClient.ts
+++ b/apps/extension/src/utils/mcpClient.ts
@@ -3,333 +3,379 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import * as vscode from 'vscode';
export interface MCPConfig {
- command: string;
- args: string[];
- cwd?: string;
- env?: Record;
+ command: string;
+ args: string[];
+ cwd?: string;
+ env?: Record;
}
export interface MCPServerStatus {
- isRunning: boolean;
- pid?: number;
- error?: string;
+ isRunning: boolean;
+ pid?: number;
+ error?: string;
}
export class MCPClientManager {
- private client: Client | null = null;
- private transport: StdioClientTransport | null = null;
- private config: MCPConfig;
- private status: MCPServerStatus = { isRunning: false };
- private connectionPromise: Promise | null = null;
+ private client: Client | null = null;
+ private transport: StdioClientTransport | null = null;
+ private config: MCPConfig;
+ private status: MCPServerStatus = { isRunning: false };
+ private connectionPromise: Promise | null = null;
- constructor(config: MCPConfig) {
- console.log('🔍 DEBUGGING: MCPClientManager constructor called with config:', config);
- this.config = config;
- }
+ constructor(config: MCPConfig) {
+ console.log(
+ '🔍 DEBUGGING: MCPClientManager constructor called with config:',
+ config
+ );
+ this.config = config;
+ }
- /**
- * Get the current server status
- */
- getStatus(): MCPServerStatus {
- return { ...this.status };
- }
+ /**
+ * Get the current server status
+ */
+ getStatus(): MCPServerStatus {
+ return { ...this.status };
+ }
- /**
- * Start the MCP server process and establish client connection
- */
- async connect(): Promise {
- if (this.connectionPromise) {
- return this.connectionPromise;
- }
+ /**
+ * Start the MCP server process and establish client connection
+ */
+ async connect(): Promise {
+ if (this.connectionPromise) {
+ return this.connectionPromise;
+ }
- this.connectionPromise = this._doConnect();
- return this.connectionPromise;
- }
+ this.connectionPromise = this._doConnect();
+ return this.connectionPromise;
+ }
- private async _doConnect(): Promise {
- try {
- // Clean up any existing connections
- await this.disconnect();
+ private async _doConnect(): Promise {
+ try {
+ // Clean up any existing connections
+ await this.disconnect();
- // Create the transport - it will handle spawning the server process internally
- console.log(`Starting MCP server: ${this.config.command} ${this.config.args?.join(' ') || ''}`);
- console.log('🔍 DEBUGGING: Transport config cwd:', this.config.cwd);
- console.log('🔍 DEBUGGING: Process cwd before spawn:', process.cwd());
-
- // Test if the target directory and .taskmaster exist
- const fs = require('fs');
- const path = require('path');
- try {
- const targetDir = this.config.cwd;
- const taskmasterDir = path.join(targetDir, '.taskmaster');
- const tasksFile = path.join(taskmasterDir, 'tasks', 'tasks.json');
-
- console.log('🔍 DEBUGGING: Checking target directory:', targetDir, 'exists:', fs.existsSync(targetDir));
- console.log('🔍 DEBUGGING: Checking .taskmaster dir:', taskmasterDir, 'exists:', fs.existsSync(taskmasterDir));
- console.log('🔍 DEBUGGING: Checking tasks.json:', tasksFile, 'exists:', fs.existsSync(tasksFile));
-
- if (fs.existsSync(tasksFile)) {
- const stats = fs.statSync(tasksFile);
- console.log('🔍 DEBUGGING: tasks.json size:', stats.size, 'bytes');
- }
- } catch (error) {
- console.log('🔍 DEBUGGING: Error checking filesystem:', error);
- }
-
- this.transport = new StdioClientTransport({
- command: this.config.command,
- args: this.config.args || [],
- cwd: this.config.cwd,
- env: {
- ...Object.fromEntries(
- Object.entries(process.env).filter(([, v]) => v !== undefined)
- ) as Record,
- ...this.config.env,
- },
- });
-
- console.log('🔍 DEBUGGING: Transport created, checking process...');
+ // Create the transport - it will handle spawning the server process internally
+ console.log(
+ `Starting MCP server: ${this.config.command} ${this.config.args?.join(' ') || ''}`
+ );
+ console.log('🔍 DEBUGGING: Transport config cwd:', this.config.cwd);
+ console.log('🔍 DEBUGGING: Process cwd before spawn:', process.cwd());
- // Set up transport event handlers
- this.transport.onerror = (error: Error) => {
- console.error('❌ MCP transport error:', error);
- console.error('Transport error details:', {
- message: error.message,
- stack: error.stack,
- code: (error as any).code,
- errno: (error as any).errno,
- syscall: (error as any).syscall
- });
- this.status = { isRunning: false, error: error.message };
- vscode.window.showErrorMessage(`Task Master MCP transport error: ${error.message}`);
- };
+ // Test if the target directory and .taskmaster exist
+ const fs = require('fs');
+ const path = require('path');
+ try {
+ const targetDir = this.config.cwd;
+ const taskmasterDir = path.join(targetDir, '.taskmaster');
+ const tasksFile = path.join(taskmasterDir, 'tasks', 'tasks.json');
- this.transport.onclose = () => {
- console.log('🔌 MCP transport closed');
- this.status = { isRunning: false };
- this.client = null;
- this.transport = null;
- };
+ console.log(
+ '🔍 DEBUGGING: Checking target directory:',
+ targetDir,
+ 'exists:',
+ fs.existsSync(targetDir)
+ );
+ console.log(
+ '🔍 DEBUGGING: Checking .taskmaster dir:',
+ taskmasterDir,
+ 'exists:',
+ fs.existsSync(taskmasterDir)
+ );
+ console.log(
+ '🔍 DEBUGGING: Checking tasks.json:',
+ tasksFile,
+ 'exists:',
+ fs.existsSync(tasksFile)
+ );
- // Add message handler like the working debug script
- this.transport.onmessage = (message: any) => {
- console.log('📤 MCP server message:', message);
- };
+ if (fs.existsSync(tasksFile)) {
+ const stats = fs.statSync(tasksFile);
+ console.log('🔍 DEBUGGING: tasks.json size:', stats.size, 'bytes');
+ }
+ } catch (error) {
+ console.log('🔍 DEBUGGING: Error checking filesystem:', error);
+ }
- // Create the client
- this.client = new Client(
- {
- name: 'taskr-vscode-extension',
- version: '1.0.0',
- },
- {
- capabilities: {
- tools: {},
- },
- }
- );
+ this.transport = new StdioClientTransport({
+ command: this.config.command,
+ args: this.config.args || [],
+ cwd: this.config.cwd,
+ env: {
+ ...(Object.fromEntries(
+ Object.entries(process.env).filter(([, v]) => v !== undefined)
+ ) as Record),
+ ...this.config.env
+ }
+ });
- // Connect the client to the transport (this automatically starts the transport)
- console.log('🔄 Attempting MCP client connection...');
- console.log('MCP config:', { command: this.config.command, args: this.config.args, cwd: this.config.cwd });
- console.log('Current working directory:', process.cwd());
- console.log('VS Code workspace folders:', vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath));
-
- // Check if process was created before connecting
- if (this.transport && (this.transport as any).process) {
- const proc = (this.transport as any).process;
- console.log('📝 MCP server process PID:', proc.pid);
- console.log('📝 Process working directory will be:', this.config.cwd);
-
- proc.on('exit', (code: number, signal: string) => {
- console.log(`🔚 MCP server process exited with code ${code}, signal ${signal}`);
- if (code !== 0) {
- console.log('❌ Non-zero exit code indicates server failure');
- }
- });
-
- proc.on('error', (error: Error) => {
- console.log('❌ MCP server process error:', error);
- });
-
- // Listen to stderr to see server-side errors
- if (proc.stderr) {
- proc.stderr.on('data', (data: Buffer) => {
- console.log('📥 MCP server stderr:', data.toString());
- });
- }
-
- // Listen to stdout for server messages
- if (proc.stdout) {
- proc.stdout.on('data', (data: Buffer) => {
- console.log('📤 MCP server stdout:', data.toString());
- });
- }
- } else {
- console.log('⚠️ No process found in transport before connection');
- }
+ console.log('🔍 DEBUGGING: Transport created, checking process...');
- await this.client.connect(this.transport);
+ // Set up transport event handlers
+ this.transport.onerror = (error: Error) => {
+ console.error('❌ MCP transport error:', error);
+ console.error('Transport error details:', {
+ message: error.message,
+ stack: error.stack,
+ code: (error as any).code,
+ errno: (error as any).errno,
+ syscall: (error as any).syscall
+ });
+ this.status = { isRunning: false, error: error.message };
+ vscode.window.showErrorMessage(
+ `Task Master MCP transport error: ${error.message}`
+ );
+ };
- // Update status
- this.status = {
- isRunning: true,
- pid: this.transport.pid || undefined,
- };
+ this.transport.onclose = () => {
+ console.log('🔌 MCP transport closed');
+ this.status = { isRunning: false };
+ this.client = null;
+ this.transport = null;
+ };
- console.log('MCP client connected successfully');
- vscode.window.showInformationMessage('Task Master connected successfully');
+ // Add message handler like the working debug script
+ this.transport.onmessage = (message: any) => {
+ console.log('📤 MCP server message:', message);
+ };
- } catch (error) {
- console.error('Failed to connect to MCP server:', error);
- this.status = {
- isRunning: false,
- error: error instanceof Error ? error.message : 'Unknown error'
- };
-
- // Clean up on error
- await this.disconnect();
-
- throw error;
- } finally {
- this.connectionPromise = null;
- }
- }
+ // Create the client
+ this.client = new Client(
+ {
+ name: 'taskr-vscode-extension',
+ version: '1.0.0'
+ },
+ {
+ capabilities: {
+ tools: {}
+ }
+ }
+ );
- /**
- * Disconnect from the MCP server and clean up resources
- */
- async disconnect(): Promise {
- console.log('Disconnecting from MCP server');
+ // Connect the client to the transport (this automatically starts the transport)
+ console.log('🔄 Attempting MCP client connection...');
+ console.log('MCP config:', {
+ command: this.config.command,
+ args: this.config.args,
+ cwd: this.config.cwd
+ });
+ console.log('Current working directory:', process.cwd());
+ console.log(
+ 'VS Code workspace folders:',
+ vscode.workspace.workspaceFolders?.map((f) => f.uri.fsPath)
+ );
- if (this.client) {
- try {
- await this.client.close();
- } catch (error) {
- console.error('Error closing MCP client:', error);
- }
- this.client = null;
- }
+ // Check if process was created before connecting
+ if (this.transport && (this.transport as any).process) {
+ const proc = (this.transport as any).process;
+ console.log('📝 MCP server process PID:', proc.pid);
+ console.log('📝 Process working directory will be:', this.config.cwd);
- if (this.transport) {
- try {
- await this.transport.close();
- } catch (error) {
- console.error('Error closing MCP transport:', error);
- }
- this.transport = null;
- }
+ proc.on('exit', (code: number, signal: string) => {
+ console.log(
+ `🔚 MCP server process exited with code ${code}, signal ${signal}`
+ );
+ if (code !== 0) {
+ console.log('❌ Non-zero exit code indicates server failure');
+ }
+ });
- this.status = { isRunning: false };
- }
+ proc.on('error', (error: Error) => {
+ console.log('❌ MCP server process error:', error);
+ });
- /**
- * Get the MCP client instance (if connected)
- */
- getClient(): Client | null {
- return this.client;
- }
+ // Listen to stderr to see server-side errors
+ if (proc.stderr) {
+ proc.stderr.on('data', (data: Buffer) => {
+ console.log('📥 MCP server stderr:', data.toString());
+ });
+ }
- /**
- * Call an MCP tool
- */
- async callTool(toolName: string, arguments_: Record): Promise {
- if (!this.client) {
- throw new Error('MCP client is not connected');
- }
+ // Listen to stdout for server messages
+ if (proc.stdout) {
+ proc.stdout.on('data', (data: Buffer) => {
+ console.log('📤 MCP server stdout:', data.toString());
+ });
+ }
+ } else {
+ console.log('⚠️ No process found in transport before connection');
+ }
- try {
- const result = await this.client.callTool({
- name: toolName,
- arguments: arguments_,
- });
+ await this.client.connect(this.transport);
- return result;
- } catch (error) {
- console.error(`Error calling MCP tool "${toolName}":`, error);
- throw error;
- }
- }
+ // Update status
+ this.status = {
+ isRunning: true,
+ pid: this.transport.pid || undefined
+ };
- /**
- * Test the connection by calling a simple MCP tool
- */
- async testConnection(): Promise {
- try {
- // Try to list available tools as a connection test
- if (!this.client) {
- return false;
- }
-
- const result = await this.client.listTools();
- console.log('Available MCP tools:', result.tools?.map(t => t.name) || []);
- return true;
- } catch (error) {
- console.error('Connection test failed:', error);
- return false;
- }
- }
+ console.log('MCP client connected successfully');
+ vscode.window.showInformationMessage(
+ 'Task Master connected successfully'
+ );
+ } catch (error) {
+ console.error('Failed to connect to MCP server:', error);
+ this.status = {
+ isRunning: false,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ };
- /**
- * Get stderr stream from the transport (if available)
- */
- getStderr(): NodeJS.ReadableStream | null {
- const stderr = this.transport?.stderr;
- return stderr ? (stderr as unknown as NodeJS.ReadableStream) : null;
- }
+ // Clean up on error
+ await this.disconnect();
- /**
- * Get the process ID of the spawned server
- */
- getPid(): number | null {
- return this.transport?.pid || null;
- }
+ throw error;
+ } finally {
+ this.connectionPromise = null;
+ }
+ }
+
+ /**
+ * Disconnect from the MCP server and clean up resources
+ */
+ async disconnect(): Promise {
+ console.log('Disconnecting from MCP server');
+
+ if (this.client) {
+ try {
+ await this.client.close();
+ } catch (error) {
+ console.error('Error closing MCP client:', error);
+ }
+ this.client = null;
+ }
+
+ if (this.transport) {
+ try {
+ await this.transport.close();
+ } catch (error) {
+ console.error('Error closing MCP transport:', error);
+ }
+ this.transport = null;
+ }
+
+ this.status = { isRunning: false };
+ }
+
+ /**
+ * Get the MCP client instance (if connected)
+ */
+ getClient(): Client | null {
+ return this.client;
+ }
+
+ /**
+ * Call an MCP tool
+ */
+ async callTool(
+ toolName: string,
+ arguments_: Record
+ ): Promise {
+ if (!this.client) {
+ throw new Error('MCP client is not connected');
+ }
+
+ try {
+ const result = await this.client.callTool({
+ name: toolName,
+ arguments: arguments_
+ });
+
+ return result;
+ } catch (error) {
+ console.error(`Error calling MCP tool "${toolName}":`, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Test the connection by calling a simple MCP tool
+ */
+ async testConnection(): Promise {
+ try {
+ // Try to list available tools as a connection test
+ if (!this.client) {
+ return false;
+ }
+
+ const result = await this.client.listTools();
+ console.log(
+ 'Available MCP tools:',
+ result.tools?.map((t) => t.name) || []
+ );
+ return true;
+ } catch (error) {
+ console.error('Connection test failed:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Get stderr stream from the transport (if available)
+ */
+ getStderr(): NodeJS.ReadableStream | null {
+ const stderr = this.transport?.stderr;
+ return stderr ? (stderr as unknown as NodeJS.ReadableStream) : null;
+ }
+
+ /**
+ * Get the process ID of the spawned server
+ */
+ getPid(): number | null {
+ return this.transport?.pid || null;
+ }
}
/**
* Create MCP configuration from VS Code settings
*/
export function createMCPConfigFromSettings(): MCPConfig {
- console.log('🔍 DEBUGGING: createMCPConfigFromSettings called at', new Date().toISOString());
- const config = vscode.workspace.getConfiguration('taskmaster');
-
- let command = config.get('mcp.command', 'npx');
- const args = config.get('mcp.args', ['-y', '--package=task-master-ai', 'task-master-ai']);
-
- // Use proper VS Code workspace detection
- const defaultCwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
- const cwd = config.get('mcp.cwd', defaultCwd);
- const env = config.get>('mcp.env');
-
- console.log('✅ Using workspace directory:', defaultCwd);
+ console.log(
+ '🔍 DEBUGGING: createMCPConfigFromSettings called at',
+ new Date().toISOString()
+ );
+ const config = vscode.workspace.getConfiguration('taskmaster');
- // If using default 'npx', try to find the full path on macOS/Linux
- if (command === 'npx') {
- const fs = require('fs');
- const npxPaths = [
- '/opt/homebrew/bin/npx', // Homebrew on Apple Silicon
- '/usr/local/bin/npx', // Homebrew on Intel
- '/usr/bin/npx', // System npm
- 'npx' // Final fallback to PATH
- ];
-
- for (const path of npxPaths) {
- try {
- if (path === 'npx' || fs.existsSync(path)) {
- command = path;
- console.log(`✅ Using npx at: ${path}`);
- break;
- }
- } catch (error) {
- // Continue to next path
- }
- }
- }
+ let command = config.get('mcp.command', 'npx');
+ const args = config.get('mcp.args', [
+ '-y',
+ '--package=task-master-ai',
+ 'task-master-ai'
+ ]);
- return {
- command,
- args,
- cwd: cwd || defaultCwd,
- env
- };
-}
\ No newline at end of file
+ // Use proper VS Code workspace detection
+ const defaultCwd =
+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
+ const cwd = config.get('mcp.cwd', defaultCwd);
+ const env = config.get>('mcp.env');
+
+ console.log('✅ Using workspace directory:', defaultCwd);
+
+ // If using default 'npx', try to find the full path on macOS/Linux
+ if (command === 'npx') {
+ const fs = require('fs');
+ const npxPaths = [
+ '/opt/homebrew/bin/npx', // Homebrew on Apple Silicon
+ '/usr/local/bin/npx', // Homebrew on Intel
+ '/usr/bin/npx', // System npm
+ 'npx' // Final fallback to PATH
+ ];
+
+ for (const path of npxPaths) {
+ try {
+ if (path === 'npx' || fs.existsSync(path)) {
+ command = path;
+ console.log(`✅ Using npx at: ${path}`);
+ break;
+ }
+ } catch (error) {
+ // Continue to next path
+ }
+ }
+ }
+
+ return {
+ command,
+ args,
+ cwd: cwd || defaultCwd,
+ env
+ };
+}
diff --git a/apps/extension/src/utils/notificationPreferences.ts b/apps/extension/src/utils/notificationPreferences.ts
index 291dd63f..f4e4b66c 100644
--- a/apps/extension/src/utils/notificationPreferences.ts
+++ b/apps/extension/src/utils/notificationPreferences.ts
@@ -2,275 +2,463 @@ import * as vscode from 'vscode';
import { ErrorCategory, ErrorSeverity, NotificationType } from './errorHandler';
export interface NotificationPreferences {
- // Global notification toggles
- enableToastNotifications: boolean;
- enableVSCodeNotifications: boolean;
- enableConsoleLogging: boolean;
-
- // Toast notification settings
- toastDuration: {
- info: number;
- warning: number;
- error: number;
- };
-
- // Category-based preferences
- categoryPreferences: Record;
-
- // Severity-based preferences
- severityPreferences: Record;
-
- // Advanced settings
- maxToastCount: number;
- enableErrorTracking: boolean;
- enableDetailedErrorInfo: boolean;
+ // Global notification toggles
+ enableToastNotifications: boolean;
+ enableVSCodeNotifications: boolean;
+ enableConsoleLogging: boolean;
+
+ // Toast notification settings
+ toastDuration: {
+ info: number;
+ warning: number;
+ error: number;
+ };
+
+ // Category-based preferences
+ categoryPreferences: Record<
+ ErrorCategory,
+ {
+ showToUser: boolean;
+ notificationType: NotificationType;
+ logToConsole: boolean;
+ }
+ >;
+
+ // Severity-based preferences
+ severityPreferences: Record<
+ ErrorSeverity,
+ {
+ showToUser: boolean;
+ notificationType: NotificationType;
+ minToastDuration: number;
+ }
+ >;
+
+ // Advanced settings
+ maxToastCount: number;
+ enableErrorTracking: boolean;
+ enableDetailedErrorInfo: boolean;
}
export class NotificationPreferencesManager {
- private static instance: NotificationPreferencesManager | null = null;
- private readonly configSection = 'taskMasterKanban';
+ private static instance: NotificationPreferencesManager | null = null;
+ private readonly configSection = 'taskMasterKanban';
- private constructor() {}
+ private constructor() {}
- static getInstance(): NotificationPreferencesManager {
- if (!NotificationPreferencesManager.instance) {
- NotificationPreferencesManager.instance = new NotificationPreferencesManager();
- }
- return NotificationPreferencesManager.instance;
- }
+ static getInstance(): NotificationPreferencesManager {
+ if (!NotificationPreferencesManager.instance) {
+ NotificationPreferencesManager.instance =
+ new NotificationPreferencesManager();
+ }
+ return NotificationPreferencesManager.instance;
+ }
- /**
- * Get current notification preferences from VS Code settings
- */
- getPreferences(): NotificationPreferences {
- const config = vscode.workspace.getConfiguration(this.configSection);
-
- return {
- enableToastNotifications: config.get('notifications.enableToast', true),
- enableVSCodeNotifications: config.get('notifications.enableVSCode', true),
- enableConsoleLogging: config.get('notifications.enableConsole', true),
-
- toastDuration: {
- info: config.get('notifications.toastDuration.info', 5000),
- warning: config.get('notifications.toastDuration.warning', 7000),
- error: config.get('notifications.toastDuration.error', 10000),
- },
-
- categoryPreferences: this.getCategoryPreferences(config),
- severityPreferences: this.getSeverityPreferences(config),
-
- maxToastCount: config.get('notifications.maxToastCount', 5),
- enableErrorTracking: config.get('notifications.enableErrorTracking', true),
- enableDetailedErrorInfo: config.get('notifications.enableDetailedErrorInfo', false),
- };
- }
+ /**
+ * Get current notification preferences from VS Code settings
+ */
+ getPreferences(): NotificationPreferences {
+ const config = vscode.workspace.getConfiguration(this.configSection);
- /**
- * Update notification preferences in VS Code settings
- */
- async updatePreferences(preferences: Partial): Promise {
- const config = vscode.workspace.getConfiguration(this.configSection);
-
- if (preferences.enableToastNotifications !== undefined) {
- await config.update('notifications.enableToast', preferences.enableToastNotifications, vscode.ConfigurationTarget.Global);
- }
-
- if (preferences.enableVSCodeNotifications !== undefined) {
- await config.update('notifications.enableVSCode', preferences.enableVSCodeNotifications, vscode.ConfigurationTarget.Global);
- }
-
- if (preferences.enableConsoleLogging !== undefined) {
- await config.update('notifications.enableConsole', preferences.enableConsoleLogging, vscode.ConfigurationTarget.Global);
- }
-
- if (preferences.toastDuration) {
- await config.update('notifications.toastDuration', preferences.toastDuration, vscode.ConfigurationTarget.Global);
- }
-
- if (preferences.maxToastCount !== undefined) {
- await config.update('notifications.maxToastCount', preferences.maxToastCount, vscode.ConfigurationTarget.Global);
- }
-
- if (preferences.enableErrorTracking !== undefined) {
- await config.update('notifications.enableErrorTracking', preferences.enableErrorTracking, vscode.ConfigurationTarget.Global);
- }
-
- if (preferences.enableDetailedErrorInfo !== undefined) {
- await config.update('notifications.enableDetailedErrorInfo', preferences.enableDetailedErrorInfo, vscode.ConfigurationTarget.Global);
- }
- }
+ return {
+ enableToastNotifications: config.get('notifications.enableToast', true),
+ enableVSCodeNotifications: config.get('notifications.enableVSCode', true),
+ enableConsoleLogging: config.get('notifications.enableConsole', true),
- /**
- * Check if notifications should be shown for a specific error category and severity
- */
- shouldShowNotification(category: ErrorCategory, severity: ErrorSeverity): boolean {
- const preferences = this.getPreferences();
-
- // Check global toggles first
- if (!preferences.enableToastNotifications && !preferences.enableVSCodeNotifications) {
- return false;
- }
-
- // Check category preferences
- const categoryPref = preferences.categoryPreferences[category];
- if (categoryPref && !categoryPref.showToUser) {
- return false;
- }
-
- // Check severity preferences
- const severityPref = preferences.severityPreferences[severity];
- if (severityPref && !severityPref.showToUser) {
- return false;
- }
-
- return true;
- }
+ toastDuration: {
+ info: config.get('notifications.toastDuration.info', 5000),
+ warning: config.get('notifications.toastDuration.warning', 7000),
+ error: config.get('notifications.toastDuration.error', 10000)
+ },
- /**
- * Get the appropriate notification type for an error
- */
- getNotificationType(category: ErrorCategory, severity: ErrorSeverity): NotificationType {
- const preferences = this.getPreferences();
-
- // Check category preference first
- const categoryPref = preferences.categoryPreferences[category];
- if (categoryPref) {
- return categoryPref.notificationType;
- }
-
- // Fall back to severity preference
- const severityPref = preferences.severityPreferences[severity];
- if (severityPref) {
- return severityPref.notificationType;
- }
-
- // Default fallback
- return this.getDefaultNotificationType(severity);
- }
+ categoryPreferences: this.getCategoryPreferences(config),
+ severityPreferences: this.getSeverityPreferences(config),
- /**
- * Get toast duration for a specific severity
- */
- getToastDuration(severity: ErrorSeverity): number {
- const preferences = this.getPreferences();
-
- switch (severity) {
- case ErrorSeverity.LOW:
- return preferences.toastDuration.info;
- case ErrorSeverity.MEDIUM:
- return preferences.toastDuration.warning;
- case ErrorSeverity.HIGH:
- case ErrorSeverity.CRITICAL:
- return preferences.toastDuration.error;
- default:
- return preferences.toastDuration.warning;
- }
- }
+ maxToastCount: config.get('notifications.maxToastCount', 5),
+ enableErrorTracking: config.get(
+ 'notifications.enableErrorTracking',
+ true
+ ),
+ enableDetailedErrorInfo: config.get(
+ 'notifications.enableDetailedErrorInfo',
+ false
+ )
+ };
+ }
- /**
- * Reset preferences to defaults
- */
- async resetToDefaults(): Promise {
- const config = vscode.workspace.getConfiguration(this.configSection);
-
- // Reset all notification settings
- await config.update('notifications', undefined, vscode.ConfigurationTarget.Global);
-
- console.log('Task Master Kanban notification preferences reset to defaults');
- }
+ /**
+ * Update notification preferences in VS Code settings
+ */
+ async updatePreferences(
+ preferences: Partial
+ ): Promise {
+ const config = vscode.workspace.getConfiguration(this.configSection);
- /**
- * Get category-based preferences with defaults
- */
- private getCategoryPreferences(config: vscode.WorkspaceConfiguration): Record {
- const defaults = {
- [ErrorCategory.MCP_CONNECTION]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.CONFIGURATION]: { showToUser: true, notificationType: NotificationType.VSCODE_WARNING, logToConsole: true },
- [ErrorCategory.TASK_LOADING]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
- [ErrorCategory.UI_RENDERING]: { showToUser: true, notificationType: NotificationType.TOAST_INFO, logToConsole: false },
- [ErrorCategory.VALIDATION]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
- [ErrorCategory.NETWORK]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
- [ErrorCategory.INTERNAL]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.TASK_MASTER_API]: { showToUser: true, notificationType: NotificationType.TOAST_ERROR, logToConsole: true },
- [ErrorCategory.DATA_VALIDATION]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
- [ErrorCategory.DATA_PARSING]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.TASK_DATA_CORRUPTION]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.VSCODE_API]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.WEBVIEW]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
- [ErrorCategory.EXTENSION_HOST]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.USER_INTERACTION]: { showToUser: false, notificationType: NotificationType.CONSOLE_ONLY, logToConsole: true },
- [ErrorCategory.DRAG_DROP]: { showToUser: true, notificationType: NotificationType.TOAST_INFO, logToConsole: false },
- [ErrorCategory.COMPONENT_RENDER]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, logToConsole: true },
- [ErrorCategory.PERMISSION]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.FILE_SYSTEM]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, logToConsole: true },
- [ErrorCategory.UNKNOWN]: { showToUser: true, notificationType: NotificationType.VSCODE_WARNING, logToConsole: true },
- };
+ if (preferences.enableToastNotifications !== undefined) {
+ await config.update(
+ 'notifications.enableToast',
+ preferences.enableToastNotifications,
+ vscode.ConfigurationTarget.Global
+ );
+ }
- // Allow user overrides from settings
- const userPreferences = config.get('notifications.categoryPreferences', {});
- return { ...defaults, ...userPreferences };
- }
+ if (preferences.enableVSCodeNotifications !== undefined) {
+ await config.update(
+ 'notifications.enableVSCode',
+ preferences.enableVSCodeNotifications,
+ vscode.ConfigurationTarget.Global
+ );
+ }
- /**
- * Get severity-based preferences with defaults
- */
- private getSeverityPreferences(config: vscode.WorkspaceConfiguration): Record {
- const defaults = {
- [ErrorSeverity.LOW]: { showToUser: true, notificationType: NotificationType.TOAST_INFO, minToastDuration: 3000 },
- [ErrorSeverity.MEDIUM]: { showToUser: true, notificationType: NotificationType.TOAST_WARNING, minToastDuration: 5000 },
- [ErrorSeverity.HIGH]: { showToUser: true, notificationType: NotificationType.VSCODE_WARNING, minToastDuration: 7000 },
- [ErrorSeverity.CRITICAL]: { showToUser: true, notificationType: NotificationType.VSCODE_ERROR, minToastDuration: 10000 },
- };
+ if (preferences.enableConsoleLogging !== undefined) {
+ await config.update(
+ 'notifications.enableConsole',
+ preferences.enableConsoleLogging,
+ vscode.ConfigurationTarget.Global
+ );
+ }
- // Allow user overrides from settings
- const userPreferences = config.get('notifications.severityPreferences', {});
- return { ...defaults, ...userPreferences };
- }
+ if (preferences.toastDuration) {
+ await config.update(
+ 'notifications.toastDuration',
+ preferences.toastDuration,
+ vscode.ConfigurationTarget.Global
+ );
+ }
- /**
- * Get default notification type for severity
- */
- private getDefaultNotificationType(severity: ErrorSeverity): NotificationType {
- switch (severity) {
- case ErrorSeverity.LOW:
- return NotificationType.TOAST_INFO;
- case ErrorSeverity.MEDIUM:
- return NotificationType.TOAST_WARNING;
- case ErrorSeverity.HIGH:
- return NotificationType.VSCODE_WARNING;
- case ErrorSeverity.CRITICAL:
- return NotificationType.VSCODE_ERROR;
- default:
- return NotificationType.CONSOLE_ONLY;
- }
- }
+ if (preferences.maxToastCount !== undefined) {
+ await config.update(
+ 'notifications.maxToastCount',
+ preferences.maxToastCount,
+ vscode.ConfigurationTarget.Global
+ );
+ }
+
+ if (preferences.enableErrorTracking !== undefined) {
+ await config.update(
+ 'notifications.enableErrorTracking',
+ preferences.enableErrorTracking,
+ vscode.ConfigurationTarget.Global
+ );
+ }
+
+ if (preferences.enableDetailedErrorInfo !== undefined) {
+ await config.update(
+ 'notifications.enableDetailedErrorInfo',
+ preferences.enableDetailedErrorInfo,
+ vscode.ConfigurationTarget.Global
+ );
+ }
+ }
+
+ /**
+ * Check if notifications should be shown for a specific error category and severity
+ */
+ shouldShowNotification(
+ category: ErrorCategory,
+ severity: ErrorSeverity
+ ): boolean {
+ const preferences = this.getPreferences();
+
+ // Check global toggles first
+ if (
+ !preferences.enableToastNotifications &&
+ !preferences.enableVSCodeNotifications
+ ) {
+ return false;
+ }
+
+ // Check category preferences
+ const categoryPref = preferences.categoryPreferences[category];
+ if (categoryPref && !categoryPref.showToUser) {
+ return false;
+ }
+
+ // Check severity preferences
+ const severityPref = preferences.severityPreferences[severity];
+ if (severityPref && !severityPref.showToUser) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the appropriate notification type for an error
+ */
+ getNotificationType(
+ category: ErrorCategory,
+ severity: ErrorSeverity
+ ): NotificationType {
+ const preferences = this.getPreferences();
+
+ // Check category preference first
+ const categoryPref = preferences.categoryPreferences[category];
+ if (categoryPref) {
+ return categoryPref.notificationType;
+ }
+
+ // Fall back to severity preference
+ const severityPref = preferences.severityPreferences[severity];
+ if (severityPref) {
+ return severityPref.notificationType;
+ }
+
+ // Default fallback
+ return this.getDefaultNotificationType(severity);
+ }
+
+ /**
+ * Get toast duration for a specific severity
+ */
+ getToastDuration(severity: ErrorSeverity): number {
+ const preferences = this.getPreferences();
+
+ switch (severity) {
+ case ErrorSeverity.LOW:
+ return preferences.toastDuration.info;
+ case ErrorSeverity.MEDIUM:
+ return preferences.toastDuration.warning;
+ case ErrorSeverity.HIGH:
+ case ErrorSeverity.CRITICAL:
+ return preferences.toastDuration.error;
+ default:
+ return preferences.toastDuration.warning;
+ }
+ }
+
+ /**
+ * Reset preferences to defaults
+ */
+ async resetToDefaults(): Promise {
+ const config = vscode.workspace.getConfiguration(this.configSection);
+
+ // Reset all notification settings
+ await config.update(
+ 'notifications',
+ undefined,
+ vscode.ConfigurationTarget.Global
+ );
+
+ console.log(
+ 'Task Master Kanban notification preferences reset to defaults'
+ );
+ }
+
+ /**
+ * Get category-based preferences with defaults
+ */
+ private getCategoryPreferences(config: vscode.WorkspaceConfiguration): Record<
+ ErrorCategory,
+ {
+ showToUser: boolean;
+ notificationType: NotificationType;
+ logToConsole: boolean;
+ }
+ > {
+ const defaults = {
+ [ErrorCategory.MCP_CONNECTION]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.CONFIGURATION]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.TASK_LOADING]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.UI_RENDERING]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_INFO,
+ logToConsole: false
+ },
+ [ErrorCategory.VALIDATION]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.NETWORK]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.INTERNAL]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.TASK_MASTER_API]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.DATA_VALIDATION]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.DATA_PARSING]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.TASK_DATA_CORRUPTION]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.VSCODE_API]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.WEBVIEW]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.EXTENSION_HOST]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.USER_INTERACTION]: {
+ showToUser: false,
+ notificationType: NotificationType.CONSOLE_ONLY,
+ logToConsole: true
+ },
+ [ErrorCategory.DRAG_DROP]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_INFO,
+ logToConsole: false
+ },
+ [ErrorCategory.COMPONENT_RENDER]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ logToConsole: true
+ },
+ [ErrorCategory.PERMISSION]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.FILE_SYSTEM]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ logToConsole: true
+ },
+ [ErrorCategory.UNKNOWN]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_WARNING,
+ logToConsole: true
+ }
+ };
+
+ // Allow user overrides from settings
+ const userPreferences = config.get('notifications.categoryPreferences', {});
+ return { ...defaults, ...userPreferences };
+ }
+
+ /**
+ * Get severity-based preferences with defaults
+ */
+ private getSeverityPreferences(config: vscode.WorkspaceConfiguration): Record<
+ ErrorSeverity,
+ {
+ showToUser: boolean;
+ notificationType: NotificationType;
+ minToastDuration: number;
+ }
+ > {
+ const defaults = {
+ [ErrorSeverity.LOW]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_INFO,
+ minToastDuration: 3000
+ },
+ [ErrorSeverity.MEDIUM]: {
+ showToUser: true,
+ notificationType: NotificationType.TOAST_WARNING,
+ minToastDuration: 5000
+ },
+ [ErrorSeverity.HIGH]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_WARNING,
+ minToastDuration: 7000
+ },
+ [ErrorSeverity.CRITICAL]: {
+ showToUser: true,
+ notificationType: NotificationType.VSCODE_ERROR,
+ minToastDuration: 10000
+ }
+ };
+
+ // Allow user overrides from settings
+ const userPreferences = config.get('notifications.severityPreferences', {});
+ return { ...defaults, ...userPreferences };
+ }
+
+ /**
+ * Get default notification type for severity
+ */
+ private getDefaultNotificationType(
+ severity: ErrorSeverity
+ ): NotificationType {
+ switch (severity) {
+ case ErrorSeverity.LOW:
+ return NotificationType.TOAST_INFO;
+ case ErrorSeverity.MEDIUM:
+ return NotificationType.TOAST_WARNING;
+ case ErrorSeverity.HIGH:
+ return NotificationType.VSCODE_WARNING;
+ case ErrorSeverity.CRITICAL:
+ return NotificationType.VSCODE_ERROR;
+ default:
+ return NotificationType.CONSOLE_ONLY;
+ }
+ }
}
// Export convenience functions
export function getNotificationPreferences(): NotificationPreferences {
- return NotificationPreferencesManager.getInstance().getPreferences();
+ return NotificationPreferencesManager.getInstance().getPreferences();
}
-export function updateNotificationPreferences(preferences: Partial): Promise {
- return NotificationPreferencesManager.getInstance().updatePreferences(preferences);
+export function updateNotificationPreferences(
+ preferences: Partial
+): Promise {
+ return NotificationPreferencesManager.getInstance().updatePreferences(
+ preferences
+ );
}
-export function shouldShowNotification(category: ErrorCategory, severity: ErrorSeverity): boolean {
- return NotificationPreferencesManager.getInstance().shouldShowNotification(category, severity);
+export function shouldShowNotification(
+ category: ErrorCategory,
+ severity: ErrorSeverity
+): boolean {
+ return NotificationPreferencesManager.getInstance().shouldShowNotification(
+ category,
+ severity
+ );
}
-export function getNotificationType(category: ErrorCategory, severity: ErrorSeverity): NotificationType {
- return NotificationPreferencesManager.getInstance().getNotificationType(category, severity);
+export function getNotificationType(
+ category: ErrorCategory,
+ severity: ErrorSeverity
+): NotificationType {
+ return NotificationPreferencesManager.getInstance().getNotificationType(
+ category,
+ severity
+ );
}
export function getToastDuration(severity: ErrorSeverity): number {
- return NotificationPreferencesManager.getInstance().getToastDuration(severity);
-}
\ No newline at end of file
+ return NotificationPreferencesManager.getInstance().getToastDuration(
+ severity
+ );
+}
diff --git a/apps/extension/src/utils/taskFileReader.ts b/apps/extension/src/utils/taskFileReader.ts
index 0a106236..d0c2d466 100644
--- a/apps/extension/src/utils/taskFileReader.ts
+++ b/apps/extension/src/utils/taskFileReader.ts
@@ -2,30 +2,30 @@ import * as fs from 'fs';
import * as path from 'path';
export interface TaskFileData {
- details?: string;
- testStrategy?: string;
+ details?: string;
+ testStrategy?: string;
}
export interface TasksJsonStructure {
- [tagName: string]: {
- tasks: TaskWithDetails[];
- metadata: {
- createdAt: string;
- description?: string;
- };
- };
+ [tagName: string]: {
+ tasks: TaskWithDetails[];
+ metadata: {
+ createdAt: string;
+ description?: string;
+ };
+ };
}
export interface TaskWithDetails {
- id: string | number;
- title: string;
- description: string;
- status: string;
- priority: string;
- dependencies?: (string | number)[];
- details?: string;
- testStrategy?: string;
- subtasks?: TaskWithDetails[];
+ id: string | number;
+ title: string;
+ description: string;
+ status: string;
+ priority: string;
+ dependencies?: (string | number)[];
+ details?: string;
+ testStrategy?: string;
+ subtasks?: TaskWithDetails[];
}
/**
@@ -34,54 +34,60 @@ export interface TaskWithDetails {
* @param tagName - The tag/context name (defaults to "master")
* @returns TaskFileData with details and testStrategy fields
*/
-export async function readTaskFileData(taskId: string, tagName: string = 'master'): Promise {
- try {
- // Check if we're in a VS Code webview context
- if (typeof window !== 'undefined' && (window as any).vscode) {
- // Use VS Code API to read the file
- const vscode = (window as any).vscode;
-
- // Request file content from the extension
- return new Promise((resolve, reject) => {
- const messageId = Date.now().toString();
-
- // Listen for response
- const messageHandler = (event: MessageEvent) => {
- const message = event.data;
- if (message.type === 'taskFileData' && message.messageId === messageId) {
- window.removeEventListener('message', messageHandler);
- if (message.error) {
- reject(new Error(message.error));
- } else {
- resolve(message.data);
- }
- }
- };
-
- window.addEventListener('message', messageHandler);
-
- // Send request to extension
- vscode.postMessage({
- type: 'readTaskFileData',
- messageId,
- taskId,
- tagName
- });
-
- // Timeout after 5 seconds
- setTimeout(() => {
- window.removeEventListener('message', messageHandler);
- reject(new Error('Timeout reading task file data'));
- }, 5000);
- });
- } else {
- // Fallback for non-VS Code environments
- return { details: undefined, testStrategy: undefined };
- }
- } catch (error) {
- console.error('Error reading task file data:', error);
- return { details: undefined, testStrategy: undefined };
- }
+export async function readTaskFileData(
+ taskId: string,
+ tagName: string = 'master'
+): Promise {
+ try {
+ // Check if we're in a VS Code webview context
+ if (typeof window !== 'undefined' && (window as any).vscode) {
+ // Use VS Code API to read the file
+ const vscode = (window as any).vscode;
+
+ // Request file content from the extension
+ return new Promise((resolve, reject) => {
+ const messageId = Date.now().toString();
+
+ // Listen for response
+ const messageHandler = (event: MessageEvent) => {
+ const message = event.data;
+ if (
+ message.type === 'taskFileData' &&
+ message.messageId === messageId
+ ) {
+ window.removeEventListener('message', messageHandler);
+ if (message.error) {
+ reject(new Error(message.error));
+ } else {
+ resolve(message.data);
+ }
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+
+ // Send request to extension
+ vscode.postMessage({
+ type: 'readTaskFileData',
+ messageId,
+ taskId,
+ tagName
+ });
+
+ // Timeout after 5 seconds
+ setTimeout(() => {
+ window.removeEventListener('message', messageHandler);
+ reject(new Error('Timeout reading task file data'));
+ }, 5000);
+ });
+ } else {
+ // Fallback for non-VS Code environments
+ return { details: undefined, testStrategy: undefined };
+ }
+ } catch (error) {
+ console.error('Error reading task file data:', error);
+ return { details: undefined, testStrategy: undefined };
+ }
}
/**
@@ -90,41 +96,53 @@ export async function readTaskFileData(taskId: string, tagName: string = 'master
* @param taskId - ID to search for
* @returns The task object if found, undefined otherwise
*/
-export function findTaskById(tasks: TaskWithDetails[], taskId: string): TaskWithDetails | undefined {
- // Check if this is a subtask ID with dotted notation (e.g., "1.2")
- if (taskId.includes('.')) {
- const [parentId, subtaskId] = taskId.split('.');
- console.log('🔍 Looking for subtask:', { parentId, subtaskId, taskId });
-
- // Find the parent task first
- const parentTask = tasks.find(task => String(task.id) === parentId);
- if (!parentTask || !parentTask.subtasks) {
- console.log('❌ Parent task not found or has no subtasks:', parentId);
- return undefined;
- }
-
- console.log('📋 Parent task found with', parentTask.subtasks.length, 'subtasks');
- console.log('🔍 Subtask IDs in parent:', parentTask.subtasks.map(st => st.id));
-
- // Find the subtask within the parent
- const subtask = parentTask.subtasks.find(st => String(st.id) === subtaskId);
- if (subtask) {
- console.log('✅ Subtask found:', subtask.id);
- } else {
- console.log('❌ Subtask not found:', subtaskId);
- }
- return subtask;
- }
-
- // For regular task IDs (not dotted notation)
- for (const task of tasks) {
- // Convert both to strings for comparison to handle string vs number IDs
- if (String(task.id) === String(taskId)) {
- return task;
- }
- }
-
- return undefined;
+export function findTaskById(
+ tasks: TaskWithDetails[],
+ taskId: string
+): TaskWithDetails | undefined {
+ // Check if this is a subtask ID with dotted notation (e.g., "1.2")
+ if (taskId.includes('.')) {
+ const [parentId, subtaskId] = taskId.split('.');
+ console.log('🔍 Looking for subtask:', { parentId, subtaskId, taskId });
+
+ // Find the parent task first
+ const parentTask = tasks.find((task) => String(task.id) === parentId);
+ if (!parentTask || !parentTask.subtasks) {
+ console.log('❌ Parent task not found or has no subtasks:', parentId);
+ return undefined;
+ }
+
+ console.log(
+ '📋 Parent task found with',
+ parentTask.subtasks.length,
+ 'subtasks'
+ );
+ console.log(
+ '🔍 Subtask IDs in parent:',
+ parentTask.subtasks.map((st) => st.id)
+ );
+
+ // Find the subtask within the parent
+ const subtask = parentTask.subtasks.find(
+ (st) => String(st.id) === subtaskId
+ );
+ if (subtask) {
+ console.log('✅ Subtask found:', subtask.id);
+ } else {
+ console.log('❌ Subtask not found:', subtaskId);
+ }
+ return subtask;
+ }
+
+ // For regular task IDs (not dotted notation)
+ for (const task of tasks) {
+ // Convert both to strings for comparison to handle string vs number IDs
+ if (String(task.id) === String(taskId)) {
+ return task;
+ }
+ }
+
+ return undefined;
}
/**
@@ -135,40 +153,62 @@ export function findTaskById(tasks: TaskWithDetails[], taskId: string): TaskWith
* @param workspacePath - Path to workspace root (not used anymore but kept for compatibility)
* @returns TaskFileData with details and testStrategy only
*/
-export function parseTaskFileData(content: string, taskId: string, tagName: string, workspacePath?: string): TaskFileData {
- console.log('🔍 parseTaskFileData called with:', { taskId, tagName, contentLength: content.length });
-
- try {
- const tasksJson: TasksJsonStructure = JSON.parse(content);
- console.log('📊 Available tags:', Object.keys(tasksJson));
-
- // Get the tag data
- const tagData = tasksJson[tagName];
- if (!tagData || !tagData.tasks) {
- console.log('❌ Tag not found or no tasks in tag:', tagName);
- return { details: undefined, testStrategy: undefined };
- }
-
- console.log('📋 Tag found with', tagData.tasks.length, 'tasks');
- console.log('🔍 Available task IDs:', tagData.tasks.map(t => t.id));
-
- // Find the task
- const task = findTaskById(tagData.tasks, taskId);
- if (!task) {
- console.log('❌ Task not found:', taskId);
- return { details: undefined, testStrategy: undefined };
- }
-
- console.log('✅ Task found:', task.id);
- console.log('📝 Task has details:', !!task.details, 'length:', task.details?.length);
- console.log('🧪 Task has testStrategy:', !!task.testStrategy, 'length:', task.testStrategy?.length);
-
- return {
- details: task.details,
- testStrategy: task.testStrategy
- };
- } catch (error) {
- console.error('❌ Error parsing tasks.json:', error);
- return { details: undefined, testStrategy: undefined };
- }
-}
\ No newline at end of file
+export function parseTaskFileData(
+ content: string,
+ taskId: string,
+ tagName: string,
+ workspacePath?: string
+): TaskFileData {
+ console.log('🔍 parseTaskFileData called with:', {
+ taskId,
+ tagName,
+ contentLength: content.length
+ });
+
+ try {
+ const tasksJson: TasksJsonStructure = JSON.parse(content);
+ console.log('📊 Available tags:', Object.keys(tasksJson));
+
+ // Get the tag data
+ const tagData = tasksJson[tagName];
+ if (!tagData || !tagData.tasks) {
+ console.log('❌ Tag not found or no tasks in tag:', tagName);
+ return { details: undefined, testStrategy: undefined };
+ }
+
+ console.log('📋 Tag found with', tagData.tasks.length, 'tasks');
+ console.log(
+ '🔍 Available task IDs:',
+ tagData.tasks.map((t) => t.id)
+ );
+
+ // Find the task
+ const task = findTaskById(tagData.tasks, taskId);
+ if (!task) {
+ console.log('❌ Task not found:', taskId);
+ return { details: undefined, testStrategy: undefined };
+ }
+
+ console.log('✅ Task found:', task.id);
+ console.log(
+ '📝 Task has details:',
+ !!task.details,
+ 'length:',
+ task.details?.length
+ );
+ console.log(
+ '🧪 Task has testStrategy:',
+ !!task.testStrategy,
+ 'length:',
+ task.testStrategy?.length
+ );
+
+ return {
+ details: task.details,
+ testStrategy: task.testStrategy
+ };
+ } catch (error) {
+ console.error('❌ Error parsing tasks.json:', error);
+ return { details: undefined, testStrategy: undefined };
+ }
+}
diff --git a/apps/extension/src/utils/taskMasterApi.ts b/apps/extension/src/utils/taskMasterApi.ts
index c7b9ac29..77062afa 100644
--- a/apps/extension/src/utils/taskMasterApi.ts
+++ b/apps/extension/src/utils/taskMasterApi.ts
@@ -3,1132 +3,1345 @@ import { MCPClientManager } from './mcpClient';
// Task Master MCP API response types
export interface MCPTaskResponse {
- data?: {
- tasks?: Array<{
- id: number | string;
- title: string;
- description: string;
- status: string;
- priority: string;
- details?: string;
- testStrategy?: string;
- dependencies?: Array;
- complexityScore?: number;
- subtasks?: Array<{
- id: number;
- title: string;
- description?: string;
- status: string;
- details?: string;
- dependencies?: Array;
- }>;
- }>;
- tag?: {
- currentTag: string;
- availableTags: string[];
- };
- };
- version?: {
- version: string;
- name: string;
- };
- error?: string;
+ data?: {
+ tasks?: Array<{
+ id: number | string;
+ title: string;
+ description: string;
+ status: string;
+ priority: string;
+ details?: string;
+ testStrategy?: string;
+ dependencies?: Array;
+ complexityScore?: number;
+ subtasks?: Array<{
+ id: number;
+ title: string;
+ description?: string;
+ status: string;
+ details?: string;
+ dependencies?: Array;
+ }>;
+ }>;
+ tag?: {
+ currentTag: string;
+ availableTags: string[];
+ };
+ };
+ version?: {
+ version: string;
+ name: string;
+ };
+ error?: string;
}
// Our internal Task interface (matches the webview expectations)
export interface TaskMasterTask {
- id: string;
- title: string;
- description: string;
- status: 'pending' | 'in-progress' | 'review' | 'done' | 'deferred' | 'cancelled';
- priority: 'high' | 'medium' | 'low';
- details?: string;
- testStrategy?: string;
- dependencies?: string[];
- complexityScore?: number;
- subtasks?: Array<{
- id: number;
- title: string;
- description?: string;
- status: string;
- details?: string;
- dependencies?: Array;
- }>;
+ id: string;
+ title: string;
+ description: string;
+ status:
+ | 'pending'
+ | 'in-progress'
+ | 'review'
+ | 'done'
+ | 'deferred'
+ | 'cancelled';
+ priority: 'high' | 'medium' | 'low';
+ details?: string;
+ testStrategy?: string;
+ dependencies?: string[];
+ complexityScore?: number;
+ subtasks?: Array<{
+ id: number;
+ title: string;
+ description?: string;
+ status: string;
+ details?: string;
+ dependencies?: Array;
+ }>;
}
// API response wrapper
export interface TaskMasterApiResponse {
- success: boolean;
- data?: T;
- error?: string;
- requestDuration?: number;
+ success: boolean;
+ data?: T;
+ error?: string;
+ requestDuration?: number;
}
// API configuration
export interface TaskMasterApiConfig {
- timeout: number;
- retryAttempts: number;
- cacheDuration: number;
- projectRoot?: string;
- // Enhanced caching configuration
- cache?: {
- maxSize: number; // Maximum number of cache entries
- enableBackgroundRefresh: boolean; // Enable background cache refresh
- refreshInterval: number; // Background refresh interval in ms
- enableAnalytics: boolean; // Track cache hit/miss statistics
- enablePrefetch: boolean; // Enable prefetching of related data
- compressionEnabled: boolean; // Enable data compression for large datasets
- persistToDisk: boolean; // Persist cache to disk (future enhancement)
- };
+ timeout: number;
+ retryAttempts: number;
+ cacheDuration: number;
+ projectRoot?: string;
+ // Enhanced caching configuration
+ cache?: {
+ maxSize: number; // Maximum number of cache entries
+ enableBackgroundRefresh: boolean; // Enable background cache refresh
+ refreshInterval: number; // Background refresh interval in ms
+ enableAnalytics: boolean; // Track cache hit/miss statistics
+ enablePrefetch: boolean; // Enable prefetching of related data
+ compressionEnabled: boolean; // Enable data compression for large datasets
+ persistToDisk: boolean; // Persist cache to disk (future enhancement)
+ };
}
// Enhanced cache entry interface
interface CacheEntry {
- data: any;
- timestamp: number;
- accessCount: number;
- lastAccessed: number;
- size: number;
- ttl?: number;
- tags: string[];
+ data: any;
+ timestamp: number;
+ accessCount: number;
+ lastAccessed: number;
+ size: number;
+ ttl?: number;
+ tags: string[];
}
// Cache analytics interface
interface CacheAnalytics {
- hits: number;
- misses: number;
- evictions: number;
- refreshes: number;
- totalSize: number;
- averageAccessTime: number;
- hitRate: number;
+ hits: number;
+ misses: number;
+ evictions: number;
+ refreshes: number;
+ totalSize: number;
+ averageAccessTime: number;
+ hitRate: number;
}
/**
* Task Master API client that wraps MCP tool calls
*/
export class TaskMasterApi {
- private mcpClient: MCPClientManager;
- private config: TaskMasterApiConfig;
- private cache = new Map();
- private cacheAnalytics: CacheAnalytics = {
- hits: 0,
- misses: 0,
- evictions: 0,
- refreshes: 0,
- totalSize: 0,
- averageAccessTime: 0,
- hitRate: 0
- };
- private backgroundRefreshTimer?: NodeJS.Timeout;
- private readonly defaultCacheConfig = {
- maxSize: 100,
- enableBackgroundRefresh: true,
- refreshInterval: 5 * 60 * 1000, // 5 minutes
- enableAnalytics: true,
- enablePrefetch: true,
- compressionEnabled: false,
- persistToDisk: false
- };
-
- constructor(mcpClient: MCPClientManager, config?: Partial) {
- this.mcpClient = mcpClient;
- this.config = {
- timeout: 30000,
- retryAttempts: 3,
- cacheDuration: 5 * 60 * 1000, // 5 minutes
- ...config,
- cache: { ...this.defaultCacheConfig, ...config?.cache }
- };
-
- // Initialize background refresh if enabled
- if (this.config.cache?.enableBackgroundRefresh) {
- this.initializeBackgroundRefresh();
- }
-
- console.log('TaskMasterApi: Initialized with enhanced caching:', {
- cacheDuration: this.config.cacheDuration,
- maxSize: this.config.cache?.maxSize,
- backgroundRefresh: this.config.cache?.enableBackgroundRefresh,
- analytics: this.config.cache?.enableAnalytics
- });
- }
-
- /**
- * Get tasks from Task Master using the get_tasks MCP tool
- */
- async getTasks(options?: {
- status?: string;
- withSubtasks?: boolean;
- tag?: string;
- projectRoot?: string;
- }): Promise> {
- const startTime = Date.now();
- const cacheKey = `get_tasks_${JSON.stringify(options || {})}`;
-
- try {
- // Check cache first
- const cached = this.getFromCache(cacheKey);
- if (cached) {
- return {
- success: true,
- data: cached,
- requestDuration: Date.now() - startTime
- };
- }
-
- // Prepare MCP tool arguments
- const mcpArgs: Record = {
- projectRoot: options?.projectRoot || this.getWorkspaceRoot(),
- withSubtasks: options?.withSubtasks ?? true
- };
-
- // Add optional parameters
- if (options?.status) {
- mcpArgs.status = options.status;
- }
- if (options?.tag) {
- mcpArgs.tag = options.tag;
- }
-
- console.log('TaskMasterApi: Calling get_tasks with args:', mcpArgs);
-
- // Call the MCP tool
- const mcpResponse = await this.callMCPTool('get_tasks', mcpArgs);
-
- // Transform the response
- const transformedTasks = this.transformMCPTasksResponse(mcpResponse);
-
- // Cache the result
- this.setCache(cacheKey, transformedTasks);
-
- return {
- success: true,
- data: transformedTasks,
- requestDuration: Date.now() - startTime
- };
-
- } catch (error) {
- console.error('TaskMasterApi: Error getting tasks:', error);
-
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- requestDuration: Date.now() - startTime
- };
- }
- }
-
- /**
- * Update task status using the set_task_status MCP tool
- */
- async updateTaskStatus(taskId: string, status: string, options?: {
- projectRoot?: string;
- }): Promise> {
- const startTime = Date.now();
-
- try {
- const mcpArgs: Record = {
- id: taskId,
- status: status,
- projectRoot: options?.projectRoot || this.getWorkspaceRoot()
- };
-
- console.log('TaskMasterApi: Calling set_task_status with args:', mcpArgs);
-
- await this.callMCPTool('set_task_status', mcpArgs);
-
- // Clear relevant caches
- this.clearCachePattern('get_tasks');
-
- return {
- success: true,
- data: true,
- requestDuration: Date.now() - startTime
- };
-
- } catch (error) {
- console.error('TaskMasterApi: Error updating task status:', error);
-
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- requestDuration: Date.now() - startTime
- };
- }
- }
-
- /**
- * Update task content using the update_task MCP tool
- */
- async updateTask(taskId: string, updates: {
- title?: string;
- description?: string;
- details?: string;
- priority?: 'high' | 'medium' | 'low';
- testStrategy?: string;
- dependencies?: string[];
- }, options?: {
- projectRoot?: string;
- append?: boolean;
- research?: boolean;
- }): Promise> {
- const startTime = Date.now();
-
- try {
- // Build the prompt for the update_task MCP tool
- const updateFields: string[] = [];
-
- if (updates.title !== undefined) {
- updateFields.push(`Title: ${updates.title}`);
- }
- if (updates.description !== undefined) {
- updateFields.push(`Description: ${updates.description}`);
- }
- if (updates.details !== undefined) {
- updateFields.push(`Details: ${updates.details}`);
- }
- if (updates.priority !== undefined) {
- updateFields.push(`Priority: ${updates.priority}`);
- }
- if (updates.testStrategy !== undefined) {
- updateFields.push(`Test Strategy: ${updates.testStrategy}`);
- }
- if (updates.dependencies !== undefined) {
- updateFields.push(`Dependencies: ${updates.dependencies.join(', ')}`);
- }
-
- const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`;
-
- const mcpArgs: Record = {
- id: taskId,
- prompt: prompt,
- projectRoot: options?.projectRoot || this.getWorkspaceRoot()
- };
-
- // Add optional parameters
- if (options?.append !== undefined) {
- mcpArgs.append = options.append;
- }
- if (options?.research !== undefined) {
- mcpArgs.research = options.research;
- }
-
- console.log('TaskMasterApi: Calling update_task with args:', mcpArgs);
-
- await this.callMCPTool('update_task', mcpArgs);
-
- // Clear relevant caches
- this.clearCachePattern('get_tasks');
-
- return {
- success: true,
- data: true,
- requestDuration: Date.now() - startTime
- };
-
- } catch (error) {
- console.error('TaskMasterApi: Error updating task:', error);
-
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- requestDuration: Date.now() - startTime
- };
- }
- }
-
- /**
- * Update subtask content using the update_subtask MCP tool
- */
- async updateSubtask(taskId: string, prompt: string, options?: {
- projectRoot?: string;
- research?: boolean;
- }): Promise> {
- const startTime = Date.now();
-
- try {
- const mcpArgs: Record = {
- id: taskId,
- prompt: prompt,
- projectRoot: options?.projectRoot || this.getWorkspaceRoot()
- };
-
- // Add optional parameters
- if (options?.research !== undefined) {
- mcpArgs.research = options.research;
- }
-
- console.log('TaskMasterApi: Calling update_subtask with args:', mcpArgs);
-
- await this.callMCPTool('update_subtask', mcpArgs);
-
- // Clear relevant caches
- this.clearCachePattern('get_tasks');
-
- return {
- success: true,
- data: true,
- requestDuration: Date.now() - startTime
- };
-
- } catch (error) {
- console.error('TaskMasterApi: Error updating subtask:', error);
-
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- requestDuration: Date.now() - startTime
- };
- }
- }
-
- /**
- * Add a new subtask to an existing task using the add_subtask MCP tool
- */
- async addSubtask(parentTaskId: string, subtaskData: {
- title: string;
- description?: string;
- dependencies?: string[];
- status?: string;
- }, options?: {
- projectRoot?: string;
- }): Promise> {
- const startTime = Date.now();
-
- try {
- const mcpArgs: Record = {
- id: parentTaskId,
- title: subtaskData.title,
- projectRoot: options?.projectRoot || this.getWorkspaceRoot()
- };
-
- // Add optional parameters
- if (subtaskData.description) {
- mcpArgs.description = subtaskData.description;
- }
- if (subtaskData.dependencies && subtaskData.dependencies.length > 0) {
- mcpArgs.dependencies = subtaskData.dependencies.join(',');
- }
- if (subtaskData.status) {
- mcpArgs.status = subtaskData.status;
- }
-
- console.log('TaskMasterApi: Calling add_subtask with args:', mcpArgs);
-
- await this.callMCPTool('add_subtask', mcpArgs);
-
- // Clear relevant caches to force refresh
- this.clearCachePattern('get_tasks');
-
- return {
- success: true,
- data: true,
- requestDuration: Date.now() - startTime
- };
-
- } catch (error) {
- console.error('TaskMasterApi: Error adding subtask:', error);
-
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- requestDuration: Date.now() - startTime
- };
- }
- }
-
- /**
- * Get current Task Master connection status
- */
- getConnectionStatus(): { isConnected: boolean; error?: string } {
- const status = this.mcpClient.getStatus();
- return {
- isConnected: status.isRunning,
- error: status.error
- };
- }
-
- /**
- * Test the connection to Task Master
- */
- async testConnection(): Promise> {
- const startTime = Date.now();
-
- try {
- const isConnected = await this.mcpClient.testConnection();
-
- return {
- success: true,
- data: isConnected,
- requestDuration: Date.now() - startTime
- };
-
- } catch (error) {
- console.error('TaskMasterApi: Connection test failed:', error);
-
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Connection test failed',
- requestDuration: Date.now() - startTime
- };
- }
- }
-
- /**
- * Clear all cached data
- */
- clearCache(): void {
- this.cache.clear();
- this.resetCacheAnalytics();
- }
-
- /**
- * Get cache analytics
- */
- getCacheAnalytics(): CacheAnalytics {
- this.updateAnalytics();
- return { ...this.cacheAnalytics };
- }
-
- /**
- * Initialize background refresh timer
- */
- private initializeBackgroundRefresh(): void {
- if (this.backgroundRefreshTimer) {
- clearInterval(this.backgroundRefreshTimer);
- }
-
- const interval = this.config.cache?.refreshInterval || 5 * 60 * 1000;
- this.backgroundRefreshTimer = setInterval(() => {
- this.performBackgroundRefresh();
- }, interval);
-
- console.log(`TaskMasterApi: Background refresh initialized with ${interval}ms interval`);
- }
-
- /**
- * Perform background refresh of frequently accessed cache entries
- */
- private async performBackgroundRefresh(): Promise {
- if (!this.config.cache?.enableBackgroundRefresh) {
- return;
- }
-
- console.log('TaskMasterApi: Starting background cache refresh');
- const startTime = Date.now();
-
- // Find frequently accessed entries that are close to expiration
- const refreshCandidates = Array.from(this.cache.entries())
- .filter(([key, entry]) => {
- const age = Date.now() - entry.timestamp;
- const isNearExpiration = age > (this.config.cacheDuration * 0.7); // 70% of TTL
- const isFrequentlyAccessed = entry.accessCount >= 3;
- return isNearExpiration && isFrequentlyAccessed && key.includes('get_tasks');
- })
- .sort((a, b) => b[1].accessCount - a[1].accessCount) // Most accessed first
- .slice(0, 5); // Limit to top 5 entries
-
- let refreshedCount = 0;
- for (const [key, entry] of refreshCandidates) {
- try {
- // Parse the cache key to extract options
- const optionsMatch = key.match(/get_tasks_(.+)/);
- if (optionsMatch) {
- const options = JSON.parse(optionsMatch[1]);
- console.log(`TaskMasterApi: Background refreshing cache key: ${key}`);
-
- // Perform the refresh (this will update the cache)
- await this.getTasks(options);
- refreshedCount++;
- this.cacheAnalytics.refreshes++;
- }
- } catch (error) {
- console.warn(`TaskMasterApi: Background refresh failed for key ${key}:`, error);
- }
- }
-
- const duration = Date.now() - startTime;
- console.log(`TaskMasterApi: Background refresh completed in ${duration}ms, refreshed ${refreshedCount} entries`);
- }
-
- /**
- * Reset cache analytics
- */
- private resetCacheAnalytics(): void {
- this.cacheAnalytics = {
- hits: 0,
- misses: 0,
- evictions: 0,
- refreshes: 0,
- totalSize: 0,
- averageAccessTime: 0,
- hitRate: 0
- };
- }
-
- /**
- * Update cache analytics calculations
- */
- private updateAnalytics(): void {
- const total = this.cacheAnalytics.hits + this.cacheAnalytics.misses;
- this.cacheAnalytics.hitRate = total > 0 ? this.cacheAnalytics.hits / total : 0;
- this.cacheAnalytics.totalSize = this.cache.size;
-
- if (this.cache.size > 0) {
- const totalAccessTime = Array.from(this.cache.values())
- .reduce((sum, entry) => sum + (entry.lastAccessed - entry.timestamp), 0);
- this.cacheAnalytics.averageAccessTime = totalAccessTime / this.cache.size;
- }
- }
-
- /**
- * Clear cache entries matching a pattern
- */
- private clearCachePattern(pattern: string): void {
- let evictedCount = 0;
- for (const key of this.cache.keys()) {
- if (key.includes(pattern)) {
- this.cache.delete(key);
- evictedCount++;
- }
- }
-
- if (evictedCount > 0) {
- this.cacheAnalytics.evictions += evictedCount;
- console.log(`TaskMasterApi: Evicted ${evictedCount} cache entries matching pattern: ${pattern}`);
- }
- }
-
- /**
- * Get data from cache if not expired with analytics tracking
- */
- private getFromCache(key: string): any {
- const startTime = Date.now();
- const cached = this.cache.get(key);
-
- if (cached) {
- const isExpired = Date.now() - cached.timestamp >= (cached.ttl || this.config.cacheDuration);
-
- if (!isExpired) {
- // Update access statistics
- cached.accessCount++;
- cached.lastAccessed = Date.now();
-
- if (this.config.cache?.enableAnalytics) {
- this.cacheAnalytics.hits++;
- }
-
- const accessTime = Date.now() - startTime;
- console.log(`TaskMasterApi: Cache hit for ${key} (${accessTime}ms, ${cached.accessCount} accesses)`);
- return cached.data;
- } else {
- // Remove expired entry
- this.cache.delete(key);
- console.log(`TaskMasterApi: Cache entry expired and removed: ${key}`);
- }
- }
-
- if (this.config.cache?.enableAnalytics) {
- this.cacheAnalytics.misses++;
- }
-
- console.log(`TaskMasterApi: Cache miss for ${key}`);
- return null;
- }
-
- /**
- * Set data in cache with enhanced metadata and LRU eviction
- */
- private setCache(key: string, data: any, options?: { ttl?: number; tags?: string[] }): void {
- const now = Date.now();
- const dataSize = this.estimateDataSize(data);
-
- // Create cache entry
- const entry: CacheEntry = {
- data,
- timestamp: now,
- accessCount: 1,
- lastAccessed: now,
- size: dataSize,
- ttl: options?.ttl,
- tags: options?.tags || [key.split('_')[0]] // Default tag based on key prefix
- };
-
- // Check if we need to evict entries (LRU strategy)
- const maxSize = this.config.cache?.maxSize || 100;
- if (this.cache.size >= maxSize) {
- this.evictLRUEntries(Math.max(1, Math.floor(maxSize * 0.1))); // Evict 10% of max size
- }
-
- this.cache.set(key, entry);
- console.log(`TaskMasterApi: Cached data for ${key} (size: ${dataSize} bytes, TTL: ${entry.ttl || this.config.cacheDuration}ms)`);
-
- // Trigger prefetch if enabled
- if (this.config.cache?.enablePrefetch) {
- this.scheduleRelatedDataPrefetch(key, data);
- }
- }
-
- /**
- * Evict least recently used cache entries
- */
- private evictLRUEntries(count: number): void {
- const entries = Array.from(this.cache.entries())
- .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed) // Oldest first
- .slice(0, count);
-
- for (const [key] of entries) {
- this.cache.delete(key);
- this.cacheAnalytics.evictions++;
- }
-
- if (entries.length > 0) {
- console.log(`TaskMasterApi: Evicted ${entries.length} LRU cache entries`);
- }
- }
-
- /**
- * Estimate data size for cache analytics
- */
- private estimateDataSize(data: any): number {
- try {
- return JSON.stringify(data).length * 2; // Rough estimate (2 bytes per character)
- } catch {
- return 1000; // Default fallback size
- }
- }
-
- /**
- * Schedule prefetch of related data
- */
- private scheduleRelatedDataPrefetch(key: string, data: any): void {
- // This is a simple implementation - in a more sophisticated system,
- // we might prefetch related tasks, subtasks, or dependency data
- if (key.includes('get_tasks') && Array.isArray(data)) {
- console.log(`TaskMasterApi: Scheduled prefetch for ${data.length} tasks related to ${key}`);
- // Future enhancement: prefetch individual task details, related dependencies, etc.
- }
- }
-
- /**
- * Cleanup method to clear timers
- */
- destroy(): void {
- if (this.backgroundRefreshTimer) {
- clearInterval(this.backgroundRefreshTimer);
- this.backgroundRefreshTimer = undefined;
- }
- this.clearCache();
- console.log('TaskMasterApi: Destroyed and cleaned up resources');
- }
-
- /**
- * Call MCP tool with retry logic
- */
- private async callMCPTool(toolName: string, args: Record): Promise {
- let lastError: Error | null = null;
-
- for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
- try {
- const rawResponse = await this.mcpClient.callTool(toolName, args);
- console.log(`🔍 DEBUGGING: Raw MCP response for ${toolName}:`, JSON.stringify(rawResponse, null, 2));
-
- // Parse MCP response format: { content: [{ type: 'text', text: '{"data": {...}}' }] }
- if (rawResponse && rawResponse.content && Array.isArray(rawResponse.content) && rawResponse.content[0]) {
- const contentItem = rawResponse.content[0];
- if (contentItem.type === 'text' && contentItem.text) {
- try {
- const parsedData = JSON.parse(contentItem.text);
- console.log(`🔍 DEBUGGING: Parsed MCP data for ${toolName}:`, parsedData);
- return parsedData;
- } catch (parseError) {
- console.error(`TaskMasterApi: Failed to parse MCP response text for ${toolName}:`, parseError);
- console.error(`TaskMasterApi: Raw text was:`, contentItem.text);
- return rawResponse; // Fall back to original response
- }
- }
- }
-
- // If not in expected format, return as-is
- console.warn(`TaskMasterApi: Unexpected MCP response format for ${toolName}, returning raw response`);
- return rawResponse;
- } catch (error) {
- lastError = error instanceof Error ? error : new Error('Unknown error');
- console.warn(`TaskMasterApi: Attempt ${attempt}/${this.config.retryAttempts} failed for ${toolName}:`, lastError.message);
-
- if (attempt < this.config.retryAttempts) {
- // Exponential backoff
- const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
- await new Promise(resolve => setTimeout(resolve, delay));
- }
- }
- }
-
- throw lastError || new Error(`Failed to call ${toolName} after ${this.config.retryAttempts} attempts`);
- }
-
- /**
- * Transform MCP tasks response to our internal format with comprehensive validation
- */
- private transformMCPTasksResponse(mcpResponse: any): TaskMasterTask[] {
- const transformStartTime = Date.now();
-
- try {
- // Validate response structure
- const validationResult = this.validateMCPResponse(mcpResponse);
- if (!validationResult.isValid) {
- console.warn('TaskMasterApi: MCP response validation failed:', validationResult.errors);
- return [];
- }
-
- const tasks = mcpResponse.data.tasks || [];
- console.log(`TaskMasterApi: Transforming ${tasks.length} tasks from MCP response`);
-
- const transformedTasks: TaskMasterTask[] = [];
- const transformationErrors: Array<{ taskId: any; error: string; task: any }> = [];
-
- for (let i = 0; i < tasks.length; i++) {
- try {
- const task = tasks[i];
- const transformedTask = this.transformSingleTask(task, i);
- if (transformedTask) {
- transformedTasks.push(transformedTask);
- }
- } catch (error) {
- const errorMsg = error instanceof Error ? error.message : 'Unknown transformation error';
- transformationErrors.push({
- taskId: tasks[i]?.id || `unknown_${i}`,
- error: errorMsg,
- task: tasks[i]
- });
- console.error(`TaskMasterApi: Failed to transform task at index ${i}:`, errorMsg, tasks[i]);
- }
- }
-
- // Log transformation summary
- const transformDuration = Date.now() - transformStartTime;
- console.log(`TaskMasterApi: Transformation completed in ${transformDuration}ms`, {
- totalTasks: tasks.length,
- successfulTransformations: transformedTasks.length,
- errors: transformationErrors.length,
- errorSummary: transformationErrors.map(e => ({ id: e.taskId, error: e.error }))
- });
-
- return transformedTasks;
-
- } catch (error) {
- console.error('TaskMasterApi: Critical error during response transformation:', error);
- return [];
- }
- }
-
- /**
- * Validate MCP response structure
- */
- private validateMCPResponse(mcpResponse: any): { isValid: boolean; errors: string[] } {
- const errors: string[] = [];
-
- if (!mcpResponse) {
- errors.push('Response is null or undefined');
- return { isValid: false, errors };
- }
-
- if (typeof mcpResponse !== 'object') {
- errors.push('Response is not an object');
- return { isValid: false, errors };
- }
-
- if (mcpResponse.error) {
- errors.push(`MCP error: ${mcpResponse.error}`);
- }
-
- if (!mcpResponse.data) {
- errors.push('Response missing data property');
- } else if (typeof mcpResponse.data !== 'object') {
- errors.push('Response data is not an object');
- }
-
- if (mcpResponse.data && !Array.isArray(mcpResponse.data.tasks)) {
- // Allow null/undefined tasks array, but not wrong type
- if (mcpResponse.data.tasks !== null && mcpResponse.data.tasks !== undefined) {
- errors.push('Response data.tasks is not an array');
- }
- }
-
- return { isValid: errors.length === 0, errors };
- }
-
- /**
- * Transform a single task with comprehensive validation
- */
- private transformSingleTask(task: any, index: number): TaskMasterTask | null {
- if (!task || typeof task !== 'object') {
- console.warn(`TaskMasterApi: Task at index ${index} is not a valid object:`, task);
- return null;
- }
-
- try {
- // Validate required fields
- const taskId = this.validateAndNormalizeId(task.id, index);
- const title = this.validateAndNormalizeString(task.title, 'Untitled Task', `title for task ${taskId}`) || 'Untitled Task';
- const description = this.validateAndNormalizeString(task.description, '', `description for task ${taskId}`) || '';
-
- // Normalize and validate status/priority
- const status = this.normalizeStatus(task.status);
- const priority = this.normalizePriority(task.priority);
-
- // Handle optional fields
- const details = this.validateAndNormalizeString(task.details, undefined, `details for task ${taskId}`);
- const testStrategy = this.validateAndNormalizeString(task.testStrategy, undefined, `testStrategy for task ${taskId}`);
-
- // Handle complexity score
- const complexityScore = typeof task.complexityScore === 'number' ? task.complexityScore : undefined;
-
- // Transform dependencies
- const dependencies = this.transformDependencies(task.dependencies, taskId);
-
- // Transform subtasks
- const subtasks = this.transformSubtasks(task.subtasks, taskId);
-
- const transformedTask: TaskMasterTask = {
- id: taskId,
- title,
- description,
- status,
- priority,
- details,
- testStrategy,
- complexityScore,
- dependencies,
- subtasks
- };
-
- // Log successful transformation for complex tasks
- if (subtasks.length > 0 || dependencies.length > 0 || complexityScore !== undefined) {
- console.log(`TaskMasterApi: Successfully transformed complex task ${taskId}:`, {
- subtaskCount: subtasks.length,
- dependencyCount: dependencies.length,
- status,
- priority,
- complexityScore
- });
- }
-
- return transformedTask;
-
- } catch (error) {
- console.error(`TaskMasterApi: Error transforming task at index ${index}:`, error, task);
- return null;
- }
- }
-
- /**
- * Validate and normalize task ID
- */
- private validateAndNormalizeId(id: any, fallbackIndex: number): string {
- if (id === null || id === undefined) {
- const generatedId = `generated_${fallbackIndex}_${Date.now()}`;
- console.warn(`TaskMasterApi: Task missing ID, generated: ${generatedId}`);
- return generatedId;
- }
-
- const stringId = String(id).trim();
- if (stringId === '') {
- const generatedId = `empty_${fallbackIndex}_${Date.now()}`;
- console.warn(`TaskMasterApi: Task has empty ID, generated: ${generatedId}`);
- return generatedId;
- }
-
- return stringId;
- }
-
- /**
- * Validate and normalize string fields
- */
- private validateAndNormalizeString(value: any, defaultValue: string | undefined, fieldName: string): string | undefined {
- if (value === null || value === undefined) {
- return defaultValue;
- }
-
- if (typeof value !== 'string') {
- console.warn(`TaskMasterApi: ${fieldName} is not a string, converting:`, value);
- return String(value).trim() || defaultValue;
- }
-
- const trimmed = value.trim();
- if (trimmed === '' && defaultValue !== undefined) {
- return defaultValue;
- }
-
- return trimmed || defaultValue;
- }
-
- /**
- * Transform and validate dependencies
- */
- private transformDependencies(dependencies: any, taskId: string): string[] {
- if (!dependencies) {
- return [];
- }
-
- if (!Array.isArray(dependencies)) {
- console.warn(`TaskMasterApi: Dependencies for task ${taskId} is not an array:`, dependencies);
- return [];
- }
-
- const validDependencies: string[] = [];
- for (let i = 0; i < dependencies.length; i++) {
- const dep = dependencies[i];
- if (dep === null || dep === undefined) {
- console.warn(`TaskMasterApi: Null dependency at index ${i} for task ${taskId}`);
- continue;
- }
-
- const stringDep = String(dep).trim();
- if (stringDep === '') {
- console.warn(`TaskMasterApi: Empty dependency at index ${i} for task ${taskId}`);
- continue;
- }
-
- // Check for self-dependency
- if (stringDep === taskId) {
- console.warn(`TaskMasterApi: Self-dependency detected for task ${taskId}, skipping`);
- continue;
- }
-
- validDependencies.push(stringDep);
- }
-
- return validDependencies;
- }
-
- /**
- * Transform and validate subtasks
- */
- private transformSubtasks(subtasks: any, parentTaskId: string): Array<{
- id: number;
- title: string;
- description?: string;
- status: string;
- details?: string;
- dependencies?: Array;
- }> {
- if (!subtasks) {
- return [];
- }
-
- if (!Array.isArray(subtasks)) {
- console.warn(`TaskMasterApi: Subtasks for task ${parentTaskId} is not an array:`, subtasks);
- return [];
- }
-
- const validSubtasks = [];
- for (let i = 0; i < subtasks.length; i++) {
- try {
- const subtask = subtasks[i];
- if (!subtask || typeof subtask !== 'object') {
- console.warn(`TaskMasterApi: Invalid subtask at index ${i} for task ${parentTaskId}:`, subtask);
- continue;
- }
-
- const transformedSubtask = {
- id: typeof subtask.id === 'number' ? subtask.id : i + 1,
- title: this.validateAndNormalizeString(subtask.title, `Subtask ${i + 1}`, `subtask title for parent ${parentTaskId}`) || `Subtask ${i + 1}`,
- description: this.validateAndNormalizeString(subtask.description, undefined, `subtask description for parent ${parentTaskId}`),
- status: this.validateAndNormalizeString(subtask.status, 'pending', `subtask status for parent ${parentTaskId}`) || 'pending',
- details: this.validateAndNormalizeString(subtask.details, undefined, `subtask details for parent ${parentTaskId}`),
- dependencies: subtask.dependencies || []
- };
-
- validSubtasks.push(transformedSubtask);
- } catch (error) {
- console.error(`TaskMasterApi: Error transforming subtask at index ${i} for task ${parentTaskId}:`, error);
- }
- }
-
- return validSubtasks;
- }
-
- /**
- * Normalize task status to our expected values with detailed logging
- */
- private normalizeStatus(status: string): TaskMasterTask['status'] {
- const original = status;
- const normalized = status?.toLowerCase()?.trim() || 'pending';
-
- const statusMap: Record = {
- 'pending': 'pending',
- 'in-progress': 'in-progress',
- 'in_progress': 'in-progress',
- 'inprogress': 'in-progress',
- 'progress': 'in-progress',
- 'working': 'in-progress',
- 'active': 'in-progress',
- 'review': 'review',
- 'reviewing': 'review',
- 'in-review': 'review',
- 'in_review': 'review',
- 'done': 'done',
- 'completed': 'done',
- 'complete': 'done',
- 'finished': 'done',
- 'closed': 'done',
- 'resolved': 'done',
- 'blocked': 'deferred',
- 'block': 'deferred',
- 'stuck': 'deferred',
- 'waiting': 'deferred',
- 'cancelled': 'cancelled',
- 'canceled': 'cancelled',
- 'cancel': 'cancelled',
- 'abandoned': 'cancelled',
- 'deferred': 'deferred',
- 'defer': 'deferred',
- 'postponed': 'deferred',
- 'later': 'deferred'
- };
-
- const result = statusMap[normalized] || 'pending';
-
- if (original && original !== result) {
- console.log(`TaskMasterApi: Normalized status '${original}' -> '${result}'`);
- }
-
- return result;
- }
-
- /**
- * Normalize task priority to our expected values with detailed logging
- */
- private normalizePriority(priority: string): TaskMasterTask['priority'] {
- const original = priority;
- const normalized = priority?.toLowerCase()?.trim() || 'medium';
-
- let result: TaskMasterTask['priority'] = 'medium';
-
- if (normalized.includes('high') || normalized.includes('urgent') || normalized.includes('critical') ||
- normalized.includes('important') || normalized === 'h' || normalized === '3') {
- result = 'high';
- } else if (normalized.includes('low') || normalized.includes('minor') || normalized.includes('trivial') ||
- normalized === 'l' || normalized === '1') {
- result = 'low';
- } else if (normalized.includes('medium') || normalized.includes('normal') || normalized.includes('standard') ||
- normalized === 'm' || normalized === '2') {
- result = 'medium';
- }
-
- if (original && original !== result) {
- console.log(`TaskMasterApi: Normalized priority '${original}' -> '${result}'`);
- }
-
- return result;
- }
-
- /**
- * Get workspace root path
- */
- private getWorkspaceRoot(): string {
- return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
- }
-}
\ No newline at end of file
+ private mcpClient: MCPClientManager;
+ private config: TaskMasterApiConfig;
+ private cache = new Map();
+ private cacheAnalytics: CacheAnalytics = {
+ hits: 0,
+ misses: 0,
+ evictions: 0,
+ refreshes: 0,
+ totalSize: 0,
+ averageAccessTime: 0,
+ hitRate: 0
+ };
+ private backgroundRefreshTimer?: NodeJS.Timeout;
+ private readonly defaultCacheConfig = {
+ maxSize: 100,
+ enableBackgroundRefresh: true,
+ refreshInterval: 5 * 60 * 1000, // 5 minutes
+ enableAnalytics: true,
+ enablePrefetch: true,
+ compressionEnabled: false,
+ persistToDisk: false
+ };
+
+ constructor(
+ mcpClient: MCPClientManager,
+ config?: Partial
+ ) {
+ this.mcpClient = mcpClient;
+ this.config = {
+ timeout: 30000,
+ retryAttempts: 3,
+ cacheDuration: 5 * 60 * 1000, // 5 minutes
+ ...config,
+ cache: { ...this.defaultCacheConfig, ...config?.cache }
+ };
+
+ // Initialize background refresh if enabled
+ if (this.config.cache?.enableBackgroundRefresh) {
+ this.initializeBackgroundRefresh();
+ }
+
+ console.log('TaskMasterApi: Initialized with enhanced caching:', {
+ cacheDuration: this.config.cacheDuration,
+ maxSize: this.config.cache?.maxSize,
+ backgroundRefresh: this.config.cache?.enableBackgroundRefresh,
+ analytics: this.config.cache?.enableAnalytics
+ });
+ }
+
+ /**
+ * Get tasks from Task Master using the get_tasks MCP tool
+ */
+ async getTasks(options?: {
+ status?: string;
+ withSubtasks?: boolean;
+ tag?: string;
+ projectRoot?: string;
+ }): Promise> {
+ const startTime = Date.now();
+ const cacheKey = `get_tasks_${JSON.stringify(options || {})}`;
+
+ try {
+ // Check cache first
+ const cached = this.getFromCache(cacheKey);
+ if (cached) {
+ return {
+ success: true,
+ data: cached,
+ requestDuration: Date.now() - startTime
+ };
+ }
+
+ // Prepare MCP tool arguments
+ const mcpArgs: Record = {
+ projectRoot: options?.projectRoot || this.getWorkspaceRoot(),
+ withSubtasks: options?.withSubtasks ?? true
+ };
+
+ // Add optional parameters
+ if (options?.status) {
+ mcpArgs.status = options.status;
+ }
+ if (options?.tag) {
+ mcpArgs.tag = options.tag;
+ }
+
+ console.log('TaskMasterApi: Calling get_tasks with args:', mcpArgs);
+
+ // Call the MCP tool
+ const mcpResponse = await this.callMCPTool('get_tasks', mcpArgs);
+
+ // Transform the response
+ const transformedTasks = this.transformMCPTasksResponse(mcpResponse);
+
+ // Cache the result
+ this.setCache(cacheKey, transformedTasks);
+
+ return {
+ success: true,
+ data: transformedTasks,
+ requestDuration: Date.now() - startTime
+ };
+ } catch (error) {
+ console.error('TaskMasterApi: Error getting tasks:', error);
+
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Unknown error occurred',
+ requestDuration: Date.now() - startTime
+ };
+ }
+ }
+
+ /**
+ * Update task status using the set_task_status MCP tool
+ */
+ async updateTaskStatus(
+ taskId: string,
+ status: string,
+ options?: {
+ projectRoot?: string;
+ }
+ ): Promise> {
+ const startTime = Date.now();
+
+ try {
+ const mcpArgs: Record = {
+ id: taskId,
+ status: status,
+ projectRoot: options?.projectRoot || this.getWorkspaceRoot()
+ };
+
+ console.log('TaskMasterApi: Calling set_task_status with args:', mcpArgs);
+
+ await this.callMCPTool('set_task_status', mcpArgs);
+
+ // Clear relevant caches
+ this.clearCachePattern('get_tasks');
+
+ return {
+ success: true,
+ data: true,
+ requestDuration: Date.now() - startTime
+ };
+ } catch (error) {
+ console.error('TaskMasterApi: Error updating task status:', error);
+
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Unknown error occurred',
+ requestDuration: Date.now() - startTime
+ };
+ }
+ }
+
+ /**
+ * Update task content using the update_task MCP tool
+ */
+ async updateTask(
+ taskId: string,
+ updates: {
+ title?: string;
+ description?: string;
+ details?: string;
+ priority?: 'high' | 'medium' | 'low';
+ testStrategy?: string;
+ dependencies?: string[];
+ },
+ options?: {
+ projectRoot?: string;
+ append?: boolean;
+ research?: boolean;
+ }
+ ): Promise> {
+ const startTime = Date.now();
+
+ try {
+ // Build the prompt for the update_task MCP tool
+ const updateFields: string[] = [];
+
+ if (updates.title !== undefined) {
+ updateFields.push(`Title: ${updates.title}`);
+ }
+ if (updates.description !== undefined) {
+ updateFields.push(`Description: ${updates.description}`);
+ }
+ if (updates.details !== undefined) {
+ updateFields.push(`Details: ${updates.details}`);
+ }
+ if (updates.priority !== undefined) {
+ updateFields.push(`Priority: ${updates.priority}`);
+ }
+ if (updates.testStrategy !== undefined) {
+ updateFields.push(`Test Strategy: ${updates.testStrategy}`);
+ }
+ if (updates.dependencies !== undefined) {
+ updateFields.push(`Dependencies: ${updates.dependencies.join(', ')}`);
+ }
+
+ const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`;
+
+ const mcpArgs: Record = {
+ id: taskId,
+ prompt: prompt,
+ projectRoot: options?.projectRoot || this.getWorkspaceRoot()
+ };
+
+ // Add optional parameters
+ if (options?.append !== undefined) {
+ mcpArgs.append = options.append;
+ }
+ if (options?.research !== undefined) {
+ mcpArgs.research = options.research;
+ }
+
+ console.log('TaskMasterApi: Calling update_task with args:', mcpArgs);
+
+ await this.callMCPTool('update_task', mcpArgs);
+
+ // Clear relevant caches
+ this.clearCachePattern('get_tasks');
+
+ return {
+ success: true,
+ data: true,
+ requestDuration: Date.now() - startTime
+ };
+ } catch (error) {
+ console.error('TaskMasterApi: Error updating task:', error);
+
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Unknown error occurred',
+ requestDuration: Date.now() - startTime
+ };
+ }
+ }
+
+ /**
+ * Update subtask content using the update_subtask MCP tool
+ */
+ async updateSubtask(
+ taskId: string,
+ prompt: string,
+ options?: {
+ projectRoot?: string;
+ research?: boolean;
+ }
+ ): Promise> {
+ const startTime = Date.now();
+
+ try {
+ const mcpArgs: Record = {
+ id: taskId,
+ prompt: prompt,
+ projectRoot: options?.projectRoot || this.getWorkspaceRoot()
+ };
+
+ // Add optional parameters
+ if (options?.research !== undefined) {
+ mcpArgs.research = options.research;
+ }
+
+ console.log('TaskMasterApi: Calling update_subtask with args:', mcpArgs);
+
+ await this.callMCPTool('update_subtask', mcpArgs);
+
+ // Clear relevant caches
+ this.clearCachePattern('get_tasks');
+
+ return {
+ success: true,
+ data: true,
+ requestDuration: Date.now() - startTime
+ };
+ } catch (error) {
+ console.error('TaskMasterApi: Error updating subtask:', error);
+
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Unknown error occurred',
+ requestDuration: Date.now() - startTime
+ };
+ }
+ }
+
+ /**
+ * Add a new subtask to an existing task using the add_subtask MCP tool
+ */
+ async addSubtask(
+ parentTaskId: string,
+ subtaskData: {
+ title: string;
+ description?: string;
+ dependencies?: string[];
+ status?: string;
+ },
+ options?: {
+ projectRoot?: string;
+ }
+ ): Promise> {
+ const startTime = Date.now();
+
+ try {
+ const mcpArgs: Record = {
+ id: parentTaskId,
+ title: subtaskData.title,
+ projectRoot: options?.projectRoot || this.getWorkspaceRoot()
+ };
+
+ // Add optional parameters
+ if (subtaskData.description) {
+ mcpArgs.description = subtaskData.description;
+ }
+ if (subtaskData.dependencies && subtaskData.dependencies.length > 0) {
+ mcpArgs.dependencies = subtaskData.dependencies.join(',');
+ }
+ if (subtaskData.status) {
+ mcpArgs.status = subtaskData.status;
+ }
+
+ console.log('TaskMasterApi: Calling add_subtask with args:', mcpArgs);
+
+ await this.callMCPTool('add_subtask', mcpArgs);
+
+ // Clear relevant caches to force refresh
+ this.clearCachePattern('get_tasks');
+
+ return {
+ success: true,
+ data: true,
+ requestDuration: Date.now() - startTime
+ };
+ } catch (error) {
+ console.error('TaskMasterApi: Error adding subtask:', error);
+
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Unknown error occurred',
+ requestDuration: Date.now() - startTime
+ };
+ }
+ }
+
+ /**
+ * Get current Task Master connection status
+ */
+ getConnectionStatus(): { isConnected: boolean; error?: string } {
+ const status = this.mcpClient.getStatus();
+ return {
+ isConnected: status.isRunning,
+ error: status.error
+ };
+ }
+
+ /**
+ * Test the connection to Task Master
+ */
+ async testConnection(): Promise> {
+ const startTime = Date.now();
+
+ try {
+ const isConnected = await this.mcpClient.testConnection();
+
+ return {
+ success: true,
+ data: isConnected,
+ requestDuration: Date.now() - startTime
+ };
+ } catch (error) {
+ console.error('TaskMasterApi: Connection test failed:', error);
+
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Connection test failed',
+ requestDuration: Date.now() - startTime
+ };
+ }
+ }
+
+ /**
+ * Clear all cached data
+ */
+ clearCache(): void {
+ this.cache.clear();
+ this.resetCacheAnalytics();
+ }
+
+ /**
+ * Get cache analytics
+ */
+ getCacheAnalytics(): CacheAnalytics {
+ this.updateAnalytics();
+ return { ...this.cacheAnalytics };
+ }
+
+ /**
+ * Initialize background refresh timer
+ */
+ private initializeBackgroundRefresh(): void {
+ if (this.backgroundRefreshTimer) {
+ clearInterval(this.backgroundRefreshTimer);
+ }
+
+ const interval = this.config.cache?.refreshInterval || 5 * 60 * 1000;
+ this.backgroundRefreshTimer = setInterval(() => {
+ this.performBackgroundRefresh();
+ }, interval);
+
+ console.log(
+ `TaskMasterApi: Background refresh initialized with ${interval}ms interval`
+ );
+ }
+
+ /**
+ * Perform background refresh of frequently accessed cache entries
+ */
+ private async performBackgroundRefresh(): Promise {
+ if (!this.config.cache?.enableBackgroundRefresh) {
+ return;
+ }
+
+ console.log('TaskMasterApi: Starting background cache refresh');
+ const startTime = Date.now();
+
+ // Find frequently accessed entries that are close to expiration
+ const refreshCandidates = Array.from(this.cache.entries())
+ .filter(([key, entry]) => {
+ const age = Date.now() - entry.timestamp;
+ const isNearExpiration = age > this.config.cacheDuration * 0.7; // 70% of TTL
+ const isFrequentlyAccessed = entry.accessCount >= 3;
+ return (
+ isNearExpiration && isFrequentlyAccessed && key.includes('get_tasks')
+ );
+ })
+ .sort((a, b) => b[1].accessCount - a[1].accessCount) // Most accessed first
+ .slice(0, 5); // Limit to top 5 entries
+
+ let refreshedCount = 0;
+ for (const [key, entry] of refreshCandidates) {
+ try {
+ // Parse the cache key to extract options
+ const optionsMatch = key.match(/get_tasks_(.+)/);
+ if (optionsMatch) {
+ const options = JSON.parse(optionsMatch[1]);
+ console.log(`TaskMasterApi: Background refreshing cache key: ${key}`);
+
+ // Perform the refresh (this will update the cache)
+ await this.getTasks(options);
+ refreshedCount++;
+ this.cacheAnalytics.refreshes++;
+ }
+ } catch (error) {
+ console.warn(
+ `TaskMasterApi: Background refresh failed for key ${key}:`,
+ error
+ );
+ }
+ }
+
+ const duration = Date.now() - startTime;
+ console.log(
+ `TaskMasterApi: Background refresh completed in ${duration}ms, refreshed ${refreshedCount} entries`
+ );
+ }
+
+ /**
+ * Reset cache analytics
+ */
+ private resetCacheAnalytics(): void {
+ this.cacheAnalytics = {
+ hits: 0,
+ misses: 0,
+ evictions: 0,
+ refreshes: 0,
+ totalSize: 0,
+ averageAccessTime: 0,
+ hitRate: 0
+ };
+ }
+
+ /**
+ * Update cache analytics calculations
+ */
+ private updateAnalytics(): void {
+ const total = this.cacheAnalytics.hits + this.cacheAnalytics.misses;
+ this.cacheAnalytics.hitRate =
+ total > 0 ? this.cacheAnalytics.hits / total : 0;
+ this.cacheAnalytics.totalSize = this.cache.size;
+
+ if (this.cache.size > 0) {
+ const totalAccessTime = Array.from(this.cache.values()).reduce(
+ (sum, entry) => sum + (entry.lastAccessed - entry.timestamp),
+ 0
+ );
+ this.cacheAnalytics.averageAccessTime = totalAccessTime / this.cache.size;
+ }
+ }
+
+ /**
+ * Clear cache entries matching a pattern
+ */
+ private clearCachePattern(pattern: string): void {
+ let evictedCount = 0;
+ for (const key of this.cache.keys()) {
+ if (key.includes(pattern)) {
+ this.cache.delete(key);
+ evictedCount++;
+ }
+ }
+
+ if (evictedCount > 0) {
+ this.cacheAnalytics.evictions += evictedCount;
+ console.log(
+ `TaskMasterApi: Evicted ${evictedCount} cache entries matching pattern: ${pattern}`
+ );
+ }
+ }
+
+ /**
+ * Get data from cache if not expired with analytics tracking
+ */
+ private getFromCache(key: string): any {
+ const startTime = Date.now();
+ const cached = this.cache.get(key);
+
+ if (cached) {
+ const isExpired =
+ Date.now() - cached.timestamp >=
+ (cached.ttl || this.config.cacheDuration);
+
+ if (!isExpired) {
+ // Update access statistics
+ cached.accessCount++;
+ cached.lastAccessed = Date.now();
+
+ if (this.config.cache?.enableAnalytics) {
+ this.cacheAnalytics.hits++;
+ }
+
+ const accessTime = Date.now() - startTime;
+ console.log(
+ `TaskMasterApi: Cache hit for ${key} (${accessTime}ms, ${cached.accessCount} accesses)`
+ );
+ return cached.data;
+ } else {
+ // Remove expired entry
+ this.cache.delete(key);
+ console.log(`TaskMasterApi: Cache entry expired and removed: ${key}`);
+ }
+ }
+
+ if (this.config.cache?.enableAnalytics) {
+ this.cacheAnalytics.misses++;
+ }
+
+ console.log(`TaskMasterApi: Cache miss for ${key}`);
+ return null;
+ }
+
+ /**
+ * Set data in cache with enhanced metadata and LRU eviction
+ */
+ private setCache(
+ key: string,
+ data: any,
+ options?: { ttl?: number; tags?: string[] }
+ ): void {
+ const now = Date.now();
+ const dataSize = this.estimateDataSize(data);
+
+ // Create cache entry
+ const entry: CacheEntry = {
+ data,
+ timestamp: now,
+ accessCount: 1,
+ lastAccessed: now,
+ size: dataSize,
+ ttl: options?.ttl,
+ tags: options?.tags || [key.split('_')[0]] // Default tag based on key prefix
+ };
+
+ // Check if we need to evict entries (LRU strategy)
+ const maxSize = this.config.cache?.maxSize || 100;
+ if (this.cache.size >= maxSize) {
+ this.evictLRUEntries(Math.max(1, Math.floor(maxSize * 0.1))); // Evict 10% of max size
+ }
+
+ this.cache.set(key, entry);
+ console.log(
+ `TaskMasterApi: Cached data for ${key} (size: ${dataSize} bytes, TTL: ${entry.ttl || this.config.cacheDuration}ms)`
+ );
+
+ // Trigger prefetch if enabled
+ if (this.config.cache?.enablePrefetch) {
+ this.scheduleRelatedDataPrefetch(key, data);
+ }
+ }
+
+ /**
+ * Evict least recently used cache entries
+ */
+ private evictLRUEntries(count: number): void {
+ const entries = Array.from(this.cache.entries())
+ .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed) // Oldest first
+ .slice(0, count);
+
+ for (const [key] of entries) {
+ this.cache.delete(key);
+ this.cacheAnalytics.evictions++;
+ }
+
+ if (entries.length > 0) {
+ console.log(`TaskMasterApi: Evicted ${entries.length} LRU cache entries`);
+ }
+ }
+
+ /**
+ * Estimate data size for cache analytics
+ */
+ private estimateDataSize(data: any): number {
+ try {
+ return JSON.stringify(data).length * 2; // Rough estimate (2 bytes per character)
+ } catch {
+ return 1000; // Default fallback size
+ }
+ }
+
+ /**
+ * Schedule prefetch of related data
+ */
+ private scheduleRelatedDataPrefetch(key: string, data: any): void {
+ // This is a simple implementation - in a more sophisticated system,
+ // we might prefetch related tasks, subtasks, or dependency data
+ if (key.includes('get_tasks') && Array.isArray(data)) {
+ console.log(
+ `TaskMasterApi: Scheduled prefetch for ${data.length} tasks related to ${key}`
+ );
+ // Future enhancement: prefetch individual task details, related dependencies, etc.
+ }
+ }
+
+ /**
+ * Cleanup method to clear timers
+ */
+ destroy(): void {
+ if (this.backgroundRefreshTimer) {
+ clearInterval(this.backgroundRefreshTimer);
+ this.backgroundRefreshTimer = undefined;
+ }
+ this.clearCache();
+ console.log('TaskMasterApi: Destroyed and cleaned up resources');
+ }
+
+ /**
+ * Call MCP tool with retry logic
+ */
+ private async callMCPTool(
+ toolName: string,
+ args: Record
+ ): Promise {
+ let lastError: Error | null = null;
+
+ for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
+ try {
+ const rawResponse = await this.mcpClient.callTool(toolName, args);
+ console.log(
+ `🔍 DEBUGGING: Raw MCP response for ${toolName}:`,
+ JSON.stringify(rawResponse, null, 2)
+ );
+
+ // Parse MCP response format: { content: [{ type: 'text', text: '{"data": {...}}' }] }
+ if (
+ rawResponse &&
+ rawResponse.content &&
+ Array.isArray(rawResponse.content) &&
+ rawResponse.content[0]
+ ) {
+ const contentItem = rawResponse.content[0];
+ if (contentItem.type === 'text' && contentItem.text) {
+ try {
+ const parsedData = JSON.parse(contentItem.text);
+ console.log(
+ `🔍 DEBUGGING: Parsed MCP data for ${toolName}:`,
+ parsedData
+ );
+ return parsedData;
+ } catch (parseError) {
+ console.error(
+ `TaskMasterApi: Failed to parse MCP response text for ${toolName}:`,
+ parseError
+ );
+ console.error(`TaskMasterApi: Raw text was:`, contentItem.text);
+ return rawResponse; // Fall back to original response
+ }
+ }
+ }
+
+ // If not in expected format, return as-is
+ console.warn(
+ `TaskMasterApi: Unexpected MCP response format for ${toolName}, returning raw response`
+ );
+ return rawResponse;
+ } catch (error) {
+ lastError = error instanceof Error ? error : new Error('Unknown error');
+ console.warn(
+ `TaskMasterApi: Attempt ${attempt}/${this.config.retryAttempts} failed for ${toolName}:`,
+ lastError.message
+ );
+
+ if (attempt < this.config.retryAttempts) {
+ // Exponential backoff
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ }
+ }
+ }
+
+ throw (
+ lastError ||
+ new Error(
+ `Failed to call ${toolName} after ${this.config.retryAttempts} attempts`
+ )
+ );
+ }
+
+ /**
+ * Transform MCP tasks response to our internal format with comprehensive validation
+ */
+ private transformMCPTasksResponse(mcpResponse: any): TaskMasterTask[] {
+ const transformStartTime = Date.now();
+
+ try {
+ // Validate response structure
+ const validationResult = this.validateMCPResponse(mcpResponse);
+ if (!validationResult.isValid) {
+ console.warn(
+ 'TaskMasterApi: MCP response validation failed:',
+ validationResult.errors
+ );
+ return [];
+ }
+
+ const tasks = mcpResponse.data.tasks || [];
+ console.log(
+ `TaskMasterApi: Transforming ${tasks.length} tasks from MCP response`
+ );
+
+ const transformedTasks: TaskMasterTask[] = [];
+ const transformationErrors: Array<{
+ taskId: any;
+ error: string;
+ task: any;
+ }> = [];
+
+ for (let i = 0; i < tasks.length; i++) {
+ try {
+ const task = tasks[i];
+ const transformedTask = this.transformSingleTask(task, i);
+ if (transformedTask) {
+ transformedTasks.push(transformedTask);
+ }
+ } catch (error) {
+ const errorMsg =
+ error instanceof Error
+ ? error.message
+ : 'Unknown transformation error';
+ transformationErrors.push({
+ taskId: tasks[i]?.id || `unknown_${i}`,
+ error: errorMsg,
+ task: tasks[i]
+ });
+ console.error(
+ `TaskMasterApi: Failed to transform task at index ${i}:`,
+ errorMsg,
+ tasks[i]
+ );
+ }
+ }
+
+ // Log transformation summary
+ const transformDuration = Date.now() - transformStartTime;
+ console.log(
+ `TaskMasterApi: Transformation completed in ${transformDuration}ms`,
+ {
+ totalTasks: tasks.length,
+ successfulTransformations: transformedTasks.length,
+ errors: transformationErrors.length,
+ errorSummary: transformationErrors.map((e) => ({
+ id: e.taskId,
+ error: e.error
+ }))
+ }
+ );
+
+ return transformedTasks;
+ } catch (error) {
+ console.error(
+ 'TaskMasterApi: Critical error during response transformation:',
+ error
+ );
+ return [];
+ }
+ }
+
+ /**
+ * Validate MCP response structure
+ */
+ private validateMCPResponse(mcpResponse: any): {
+ isValid: boolean;
+ errors: string[];
+ } {
+ const errors: string[] = [];
+
+ if (!mcpResponse) {
+ errors.push('Response is null or undefined');
+ return { isValid: false, errors };
+ }
+
+ if (typeof mcpResponse !== 'object') {
+ errors.push('Response is not an object');
+ return { isValid: false, errors };
+ }
+
+ if (mcpResponse.error) {
+ errors.push(`MCP error: ${mcpResponse.error}`);
+ }
+
+ if (!mcpResponse.data) {
+ errors.push('Response missing data property');
+ } else if (typeof mcpResponse.data !== 'object') {
+ errors.push('Response data is not an object');
+ }
+
+ if (mcpResponse.data && !Array.isArray(mcpResponse.data.tasks)) {
+ // Allow null/undefined tasks array, but not wrong type
+ if (
+ mcpResponse.data.tasks !== null &&
+ mcpResponse.data.tasks !== undefined
+ ) {
+ errors.push('Response data.tasks is not an array');
+ }
+ }
+
+ return { isValid: errors.length === 0, errors };
+ }
+
+ /**
+ * Transform a single task with comprehensive validation
+ */
+ private transformSingleTask(task: any, index: number): TaskMasterTask | null {
+ if (!task || typeof task !== 'object') {
+ console.warn(
+ `TaskMasterApi: Task at index ${index} is not a valid object:`,
+ task
+ );
+ return null;
+ }
+
+ try {
+ // Validate required fields
+ const taskId = this.validateAndNormalizeId(task.id, index);
+ const title =
+ this.validateAndNormalizeString(
+ task.title,
+ 'Untitled Task',
+ `title for task ${taskId}`
+ ) || 'Untitled Task';
+ const description =
+ this.validateAndNormalizeString(
+ task.description,
+ '',
+ `description for task ${taskId}`
+ ) || '';
+
+ // Normalize and validate status/priority
+ const status = this.normalizeStatus(task.status);
+ const priority = this.normalizePriority(task.priority);
+
+ // Handle optional fields
+ const details = this.validateAndNormalizeString(
+ task.details,
+ undefined,
+ `details for task ${taskId}`
+ );
+ const testStrategy = this.validateAndNormalizeString(
+ task.testStrategy,
+ undefined,
+ `testStrategy for task ${taskId}`
+ );
+
+ // Handle complexity score
+ const complexityScore =
+ typeof task.complexityScore === 'number'
+ ? task.complexityScore
+ : undefined;
+
+ // Transform dependencies
+ const dependencies = this.transformDependencies(
+ task.dependencies,
+ taskId
+ );
+
+ // Transform subtasks
+ const subtasks = this.transformSubtasks(task.subtasks, taskId);
+
+ const transformedTask: TaskMasterTask = {
+ id: taskId,
+ title,
+ description,
+ status,
+ priority,
+ details,
+ testStrategy,
+ complexityScore,
+ dependencies,
+ subtasks
+ };
+
+ // Log successful transformation for complex tasks
+ if (
+ subtasks.length > 0 ||
+ dependencies.length > 0 ||
+ complexityScore !== undefined
+ ) {
+ console.log(
+ `TaskMasterApi: Successfully transformed complex task ${taskId}:`,
+ {
+ subtaskCount: subtasks.length,
+ dependencyCount: dependencies.length,
+ status,
+ priority,
+ complexityScore
+ }
+ );
+ }
+
+ return transformedTask;
+ } catch (error) {
+ console.error(
+ `TaskMasterApi: Error transforming task at index ${index}:`,
+ error,
+ task
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Validate and normalize task ID
+ */
+ private validateAndNormalizeId(id: any, fallbackIndex: number): string {
+ if (id === null || id === undefined) {
+ const generatedId = `generated_${fallbackIndex}_${Date.now()}`;
+ console.warn(`TaskMasterApi: Task missing ID, generated: ${generatedId}`);
+ return generatedId;
+ }
+
+ const stringId = String(id).trim();
+ if (stringId === '') {
+ const generatedId = `empty_${fallbackIndex}_${Date.now()}`;
+ console.warn(
+ `TaskMasterApi: Task has empty ID, generated: ${generatedId}`
+ );
+ return generatedId;
+ }
+
+ return stringId;
+ }
+
+ /**
+ * Validate and normalize string fields
+ */
+ private validateAndNormalizeString(
+ value: any,
+ defaultValue: string | undefined,
+ fieldName: string
+ ): string | undefined {
+ if (value === null || value === undefined) {
+ return defaultValue;
+ }
+
+ if (typeof value !== 'string') {
+ console.warn(
+ `TaskMasterApi: ${fieldName} is not a string, converting:`,
+ value
+ );
+ return String(value).trim() || defaultValue;
+ }
+
+ const trimmed = value.trim();
+ if (trimmed === '' && defaultValue !== undefined) {
+ return defaultValue;
+ }
+
+ return trimmed || defaultValue;
+ }
+
+ /**
+ * Transform and validate dependencies
+ */
+ private transformDependencies(dependencies: any, taskId: string): string[] {
+ if (!dependencies) {
+ return [];
+ }
+
+ if (!Array.isArray(dependencies)) {
+ console.warn(
+ `TaskMasterApi: Dependencies for task ${taskId} is not an array:`,
+ dependencies
+ );
+ return [];
+ }
+
+ const validDependencies: string[] = [];
+ for (let i = 0; i < dependencies.length; i++) {
+ const dep = dependencies[i];
+ if (dep === null || dep === undefined) {
+ console.warn(
+ `TaskMasterApi: Null dependency at index ${i} for task ${taskId}`
+ );
+ continue;
+ }
+
+ const stringDep = String(dep).trim();
+ if (stringDep === '') {
+ console.warn(
+ `TaskMasterApi: Empty dependency at index ${i} for task ${taskId}`
+ );
+ continue;
+ }
+
+ // Check for self-dependency
+ if (stringDep === taskId) {
+ console.warn(
+ `TaskMasterApi: Self-dependency detected for task ${taskId}, skipping`
+ );
+ continue;
+ }
+
+ validDependencies.push(stringDep);
+ }
+
+ return validDependencies;
+ }
+
+ /**
+ * Transform and validate subtasks
+ */
+ private transformSubtasks(
+ subtasks: any,
+ parentTaskId: string
+ ): Array<{
+ id: number;
+ title: string;
+ description?: string;
+ status: string;
+ details?: string;
+ dependencies?: Array;
+ }> {
+ if (!subtasks) {
+ return [];
+ }
+
+ if (!Array.isArray(subtasks)) {
+ console.warn(
+ `TaskMasterApi: Subtasks for task ${parentTaskId} is not an array:`,
+ subtasks
+ );
+ return [];
+ }
+
+ const validSubtasks = [];
+ for (let i = 0; i < subtasks.length; i++) {
+ try {
+ const subtask = subtasks[i];
+ if (!subtask || typeof subtask !== 'object') {
+ console.warn(
+ `TaskMasterApi: Invalid subtask at index ${i} for task ${parentTaskId}:`,
+ subtask
+ );
+ continue;
+ }
+
+ const transformedSubtask = {
+ id: typeof subtask.id === 'number' ? subtask.id : i + 1,
+ title:
+ this.validateAndNormalizeString(
+ subtask.title,
+ `Subtask ${i + 1}`,
+ `subtask title for parent ${parentTaskId}`
+ ) || `Subtask ${i + 1}`,
+ description: this.validateAndNormalizeString(
+ subtask.description,
+ undefined,
+ `subtask description for parent ${parentTaskId}`
+ ),
+ status:
+ this.validateAndNormalizeString(
+ subtask.status,
+ 'pending',
+ `subtask status for parent ${parentTaskId}`
+ ) || 'pending',
+ details: this.validateAndNormalizeString(
+ subtask.details,
+ undefined,
+ `subtask details for parent ${parentTaskId}`
+ ),
+ dependencies: subtask.dependencies || []
+ };
+
+ validSubtasks.push(transformedSubtask);
+ } catch (error) {
+ console.error(
+ `TaskMasterApi: Error transforming subtask at index ${i} for task ${parentTaskId}:`,
+ error
+ );
+ }
+ }
+
+ return validSubtasks;
+ }
+
+ /**
+ * Normalize task status to our expected values with detailed logging
+ */
+ private normalizeStatus(status: string): TaskMasterTask['status'] {
+ const original = status;
+ const normalized = status?.toLowerCase()?.trim() || 'pending';
+
+ const statusMap: Record = {
+ pending: 'pending',
+ 'in-progress': 'in-progress',
+ in_progress: 'in-progress',
+ inprogress: 'in-progress',
+ progress: 'in-progress',
+ working: 'in-progress',
+ active: 'in-progress',
+ review: 'review',
+ reviewing: 'review',
+ 'in-review': 'review',
+ in_review: 'review',
+ done: 'done',
+ completed: 'done',
+ complete: 'done',
+ finished: 'done',
+ closed: 'done',
+ resolved: 'done',
+ blocked: 'deferred',
+ block: 'deferred',
+ stuck: 'deferred',
+ waiting: 'deferred',
+ cancelled: 'cancelled',
+ canceled: 'cancelled',
+ cancel: 'cancelled',
+ abandoned: 'cancelled',
+ deferred: 'deferred',
+ defer: 'deferred',
+ postponed: 'deferred',
+ later: 'deferred'
+ };
+
+ const result = statusMap[normalized] || 'pending';
+
+ if (original && original !== result) {
+ console.log(
+ `TaskMasterApi: Normalized status '${original}' -> '${result}'`
+ );
+ }
+
+ return result;
+ }
+
+ /**
+ * Normalize task priority to our expected values with detailed logging
+ */
+ private normalizePriority(priority: string): TaskMasterTask['priority'] {
+ const original = priority;
+ const normalized = priority?.toLowerCase()?.trim() || 'medium';
+
+ let result: TaskMasterTask['priority'] = 'medium';
+
+ if (
+ normalized.includes('high') ||
+ normalized.includes('urgent') ||
+ normalized.includes('critical') ||
+ normalized.includes('important') ||
+ normalized === 'h' ||
+ normalized === '3'
+ ) {
+ result = 'high';
+ } else if (
+ normalized.includes('low') ||
+ normalized.includes('minor') ||
+ normalized.includes('trivial') ||
+ normalized === 'l' ||
+ normalized === '1'
+ ) {
+ result = 'low';
+ } else if (
+ normalized.includes('medium') ||
+ normalized.includes('normal') ||
+ normalized.includes('standard') ||
+ normalized === 'm' ||
+ normalized === '2'
+ ) {
+ result = 'medium';
+ }
+
+ if (original && original !== result) {
+ console.log(
+ `TaskMasterApi: Normalized priority '${original}' -> '${result}'`
+ );
+ }
+
+ return result;
+ }
+
+ /**
+ * Get workspace root path
+ */
+ private getWorkspaceRoot(): string {
+ return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
+ }
+}
diff --git a/apps/extension/src/webview/index.css b/apps/extension/src/webview/index.css
index d15d6ad5..5ecf2eb2 100644
--- a/apps/extension/src/webview/index.css
+++ b/apps/extension/src/webview/index.css
@@ -2,189 +2,203 @@
/* shadcn/ui CSS variables */
@theme {
- /* VS Code CSS variables will be injected here */
- /* color-scheme: var(--vscode-theme-kind, light); */
-
- /* shadcn/ui variables - adapted for VS Code */
- --color-background: var(--vscode-editor-background);
- --color-sidebar-background: var(--vscode-sideBar-background);
- --color-foreground: var(--vscode-foreground);
- --color-card: var(--vscode-editor-background);
- --color-card-foreground: var(--vscode-foreground);
- --color-popover: var(--vscode-editor-background);
- --color-popover-foreground: var(--vscode-foreground);
- --color-primary: var(--vscode-button-background);
- --color-primary-foreground: var(--vscode-button-foreground);
- --color-secondary: var(--vscode-button-secondaryBackground);
- --color-secondary-foreground: var(--vscode-button-secondaryForeground);
- --color-widget-background: var(--vscode-editorWidget-background);
- --color-widget-border: var(--vscode-editorWidget-border);
- --color-code-snippet-background: var(--vscode-textPreformat-background);
- --color-code-snippet-text: var(--vscode-textPreformat-foreground);
- --font-editor-font: var(--vscode-editor-font-family);
- --font-editor-size: var(--vscode-editor-font-size);
- --color-input-background: var(--vscode-input-background);
- --color-input-foreground: var(--vscode-input-foreground);
- --color-accent: var(--vscode-focusBorder);
- --color-accent-foreground: var(--vscode-foreground);
- --color-destructive: var(--vscode-errorForeground);
- --color-destructive-foreground: var(--vscode-foreground);
- --color-border: var(--vscode-panel-border);
- --color-ring: var(--vscode-focusBorder);
- --color-link: var(--vscode-editorLink-foreground);
- --color-link-hover: var(--vscode-editorLink-activeForeground);
- --color-textSeparator-foreground: var(--vscode-textSeparator-foreground);
- --radius: 0.5rem;
- }
+ /* VS Code CSS variables will be injected here */
+ /* color-scheme: var(--vscode-theme-kind, light); */
+ /* shadcn/ui variables - adapted for VS Code */
+ --color-background: var(--vscode-editor-background);
+ --color-sidebar-background: var(--vscode-sideBar-background);
+ --color-foreground: var(--vscode-foreground);
+ --color-card: var(--vscode-editor-background);
+ --color-card-foreground: var(--vscode-foreground);
+ --color-popover: var(--vscode-editor-background);
+ --color-popover-foreground: var(--vscode-foreground);
+ --color-primary: var(--vscode-button-background);
+ --color-primary-foreground: var(--vscode-button-foreground);
+ --color-secondary: var(--vscode-button-secondaryBackground);
+ --color-secondary-foreground: var(--vscode-button-secondaryForeground);
+ --color-widget-background: var(--vscode-editorWidget-background);
+ --color-widget-border: var(--vscode-editorWidget-border);
+ --color-code-snippet-background: var(--vscode-textPreformat-background);
+ --color-code-snippet-text: var(--vscode-textPreformat-foreground);
+ --font-editor-font: var(--vscode-editor-font-family);
+ --font-editor-size: var(--vscode-editor-font-size);
+ --color-input-background: var(--vscode-input-background);
+ --color-input-foreground: var(--vscode-input-foreground);
+ --color-accent: var(--vscode-focusBorder);
+ --color-accent-foreground: var(--vscode-foreground);
+ --color-destructive: var(--vscode-errorForeground);
+ --color-destructive-foreground: var(--vscode-foreground);
+ --color-border: var(--vscode-panel-border);
+ --color-ring: var(--vscode-focusBorder);
+ --color-link: var(--vscode-editorLink-foreground);
+ --color-link-hover: var(--vscode-editorLink-activeForeground);
+ --color-textSeparator-foreground: var(--vscode-textSeparator-foreground);
+ --radius: 0.5rem;
+}
/* Reset body to match VS Code styles instead of Tailwind defaults */
@layer base {
- html, body {
- height: 100%;
- margin: 0 !important;
- padding: 0 !important;
- overflow: hidden;
- }
+ html,
+ body {
+ height: 100%;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: hidden;
+ }
- body {
- background-color: var(--vscode-editor-background) !important;
- color: var(--vscode-foreground) !important;
- font-family: var(--vscode-font-family) !important;
- font-size: var(--vscode-font-size) !important;
- font-weight: var(--vscode-font-weight) !important;
- line-height: 1.4 !important;
- }
+ body {
+ background-color: var(--vscode-editor-background) !important;
+ color: var(--vscode-foreground) !important;
+ font-family: var(--vscode-font-family) !important;
+ font-size: var(--vscode-font-size) !important;
+ font-weight: var(--vscode-font-weight) !important;
+ line-height: 1.4 !important;
+ }
- /* Ensure root container takes full space */
- #root {
- height: 100vh;
- width: 100vw;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
+ /* Ensure root container takes full space */
+ #root {
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
- /* Override any conflicting Tailwind defaults for VS Code integration */
- * {
- box-sizing: border-box;
- }
+ /* Override any conflicting Tailwind defaults for VS Code integration */
+ * {
+ box-sizing: border-box;
+ }
- /* Ensure buttons and inputs use VS Code styling */
- button, input, select, textarea {
- font-family: inherit;
- }
+ /* Ensure buttons and inputs use VS Code styling */
+ button,
+ input,
+ select,
+ textarea {
+ font-family: inherit;
+ }
}
/* Enhanced scrollbar styling for Kanban board */
::-webkit-scrollbar {
- width: 8px;
- height: 8px;
+ width: 8px;
+ height: 8px;
}
::-webkit-scrollbar-track {
- background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
- border-radius: 4px;
+ background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
+ border-radius: 4px;
}
::-webkit-scrollbar-thumb {
- background: var(--vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2));
- border-radius: 4px;
- border: 1px solid transparent;
- background-clip: padding-box;
+ background: var(
+ --vscode-scrollbarSlider-hoverBackground,
+ rgba(255, 255, 255, 0.2)
+ );
+ border-radius: 4px;
+ border: 1px solid transparent;
+ background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
- background: var(--vscode-scrollbarSlider-activeBackground, rgba(255, 255, 255, 0.3));
+ background: var(
+ --vscode-scrollbarSlider-activeBackground,
+ rgba(255, 255, 255, 0.3)
+ );
}
::-webkit-scrollbar-corner {
- background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
+ background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
}
/* Kanban specific styles */
@layer components {
- .kanban-container {
- scrollbar-gutter: stable;
- }
-
- /* Smooth scrolling for better UX */
- .kanban-container {
- scroll-behavior: smooth;
- }
-
- /* Ensure proper touch scrolling on mobile */
- .kanban-container {
- -webkit-overflow-scrolling: touch;
- }
-
- /* Add subtle shadow for depth */
- .kanban-column {
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- }
-
- /* Enhanced scrolling for column content areas */
- .kanban-column > div[style*="overflow-y"] {
- scrollbar-width: thin;
- scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2))
- var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
- }
-
- /* Card hover effects */
- .kanban-card {
- transition: all 0.2s ease-in-out;
- }
-
- .kanban-card:hover {
- transform: translateY(-1px);
- }
-
- /* Focus indicators for accessibility */
- .kanban-card:focus-visible {
- outline: 2px solid var(--vscode-focusBorder);
- outline-offset: 2px;
- }
-
+ .kanban-container {
+ scrollbar-gutter: stable;
+ }
+ /* Smooth scrolling for better UX */
+ .kanban-container {
+ scroll-behavior: smooth;
+ }
+
+ /* Ensure proper touch scrolling on mobile */
+ .kanban-container {
+ -webkit-overflow-scrolling: touch;
+ }
+
+ /* Add subtle shadow for depth */
+ .kanban-column {
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ }
+
+ /* Enhanced scrolling for column content areas */
+ .kanban-column > div[style*="overflow-y"] {
+ scrollbar-width: thin;
+ scrollbar-color: var(
+ --vscode-scrollbarSlider-hoverBackground,
+ rgba(255, 255, 255, 0.2)
+ )
+ var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
+ }
+
+ /* Card hover effects */
+ .kanban-card {
+ transition: all 0.2s ease-in-out;
+ }
+
+ .kanban-card:hover {
+ transform: translateY(-1px);
+ }
+
+ /* Focus indicators for accessibility */
+ .kanban-card:focus-visible {
+ outline: 2px solid var(--vscode-focusBorder);
+ outline-offset: 2px;
+ }
}
/* Line clamp utility for text truncation */
@layer utilities {
- .line-clamp-2 {
- overflow: hidden;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- }
-
- .line-clamp-3 {
- overflow: hidden;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 3;
- }
-
- /* Custom scrollbar utilities */
- .scrollbar-thin {
- scrollbar-width: thin;
- }
-
- .scrollbar-track-transparent {
- scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2)) transparent;
- }
+ .line-clamp-2 {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ }
+
+ .line-clamp-3 {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ }
+
+ /* Custom scrollbar utilities */
+ .scrollbar-thin {
+ scrollbar-width: thin;
+ }
+
+ .scrollbar-track-transparent {
+ scrollbar-color: var(
+ --vscode-scrollbarSlider-hoverBackground,
+ rgba(255, 255, 255, 0.2)
+ )
+ transparent;
+ }
}
/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
- ::-webkit-scrollbar-track {
- background: rgba(255, 255, 255, 0.05);
- }
-
- ::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.15);
- }
-
- ::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.25);
- }
-}
\ No newline at end of file
+ ::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.15);
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.25);
+ }
+}
diff --git a/apps/extension/src/webview/index.tsx b/apps/extension/src/webview/index.tsx
index 4692a384..ed2aa8c5 100644
--- a/apps/extension/src/webview/index.tsx
+++ b/apps/extension/src/webview/index.tsx
@@ -1,16 +1,23 @@
-import React, { useState, useEffect, useReducer, useContext, createContext, useCallback } from 'react';
+import React, {
+ useState,
+ useEffect,
+ useReducer,
+ useContext,
+ createContext,
+ useCallback
+} from 'react';
import { createRoot } from 'react-dom/client';
// Import shadcn Kanban components
-import {
- KanbanProvider,
- KanbanBoard,
- KanbanHeader,
- KanbanCards,
- KanbanCard,
- type DragEndEvent,
- type Status,
- type Feature,
+import {
+ KanbanProvider,
+ KanbanBoard,
+ KanbanHeader,
+ KanbanCards,
+ KanbanCard,
+ type DragEndEvent,
+ type Status,
+ type Feature
} from '@/components/ui/shadcn-io/kanban';
// Import TaskDetailsView component
@@ -18,973 +25,1178 @@ import TaskDetailsView from '../components/TaskDetailsView';
// TypeScript interfaces for Task Master integration
export interface TaskMasterTask {
- id: string;
- title: string;
- description: string;
- status: 'pending' | 'in-progress' | 'done' | 'deferred' | 'review';
- priority: 'high' | 'medium' | 'low';
- dependencies?: string[];
- details?: string;
- testStrategy?: string;
- subtasks?: TaskMasterTask[];
- complexityScore?: number;
+ id: string;
+ title: string;
+ description: string;
+ status: 'pending' | 'in-progress' | 'done' | 'deferred' | 'review';
+ priority: 'high' | 'medium' | 'low';
+ dependencies?: string[];
+ details?: string;
+ testStrategy?: string;
+ subtasks?: TaskMasterTask[];
+ complexityScore?: number;
}
interface WebviewMessage {
- type: string;
- requestId?: string;
- data?: any;
- success?: boolean;
- [key: string]: any;
+ type: string;
+ requestId?: string;
+ data?: any;
+ success?: boolean;
+ [key: string]: any;
}
// VS Code API declaration
declare global {
- interface Window {
- acquireVsCodeApi?: () => {
- postMessage: (message: any) => void;
- setState: (state: any) => void;
- getState: () => any;
- };
- }
+ interface Window {
+ acquireVsCodeApi?: () => {
+ postMessage: (message: any) => void;
+ setState: (state: any) => void;
+ getState: () => any;
+ };
+ }
}
// State management types
interface AppState {
- tasks: TaskMasterTask[];
- loading: boolean;
- error?: string;
- requestId: number;
- isConnected: boolean;
- connectionStatus: string;
- editingTask?: { taskId: string | null; editData?: TaskMasterTask };
- polling: {
- isActive: boolean;
- errorCount: number;
- lastUpdate?: number;
- isUserInteracting: boolean;
- // Network status
- isOfflineMode: boolean;
- reconnectAttempts: number;
- maxReconnectAttempts: number;
- lastSuccessfulConnection?: number;
- connectionStatus: 'online' | 'offline' | 'reconnecting';
- };
- // Toast notifications
- toastNotifications: ToastNotification[];
- // Navigation state
- currentView: 'kanban' | 'task-details';
- selectedTaskId?: string;
+ tasks: TaskMasterTask[];
+ loading: boolean;
+ error?: string;
+ requestId: number;
+ isConnected: boolean;
+ connectionStatus: string;
+ editingTask?: { taskId: string | null; editData?: TaskMasterTask };
+ polling: {
+ isActive: boolean;
+ errorCount: number;
+ lastUpdate?: number;
+ isUserInteracting: boolean;
+ // Network status
+ isOfflineMode: boolean;
+ reconnectAttempts: number;
+ maxReconnectAttempts: number;
+ lastSuccessfulConnection?: number;
+ connectionStatus: 'online' | 'offline' | 'reconnecting';
+ };
+ // Toast notifications
+ toastNotifications: ToastNotification[];
+ // Navigation state
+ currentView: 'kanban' | 'task-details';
+ selectedTaskId?: string;
}
// Add interface for task updates
export interface TaskUpdates {
- title?: string;
- description?: string;
- details?: string;
- priority?: TaskMasterTask['priority'];
- testStrategy?: string;
- dependencies?: string[];
+ title?: string;
+ description?: string;
+ details?: string;
+ priority?: TaskMasterTask['priority'];
+ testStrategy?: string;
+ dependencies?: string[];
}
// Add state for task editing
type AppAction =
- | { type: 'SET_TASKS'; payload: TaskMasterTask[] }
- | { type: 'SET_LOADING'; payload: boolean }
- | { type: 'SET_ERROR'; payload: string }
- | { type: 'CLEAR_ERROR' }
- | { type: 'INCREMENT_REQUEST_ID' }
- | { type: 'UPDATE_TASK_STATUS'; payload: { taskId: string; newStatus: TaskMasterTask['status'] } }
- | { type: 'UPDATE_TASK_CONTENT'; payload: { taskId: string; updates: TaskUpdates } }
- | { type: 'SET_CONNECTION_STATUS'; payload: { isConnected: boolean; status: string } }
- | { type: 'SET_EDITING_TASK'; payload: { taskId: string | null; editData?: TaskMasterTask } }
- | { type: 'SET_POLLING_STATUS'; payload: { isActive: boolean; errorCount?: number } }
- | { type: 'SET_USER_INTERACTING'; payload: boolean }
- | { type: 'TASKS_UPDATED_FROM_POLLING'; payload: TaskMasterTask[] }
- | { type: 'SET_NETWORK_STATUS'; payload: { isOfflineMode: boolean; connectionStatus: 'online' | 'offline' | 'reconnecting'; reconnectAttempts?: number; maxReconnectAttempts?: number; lastSuccessfulConnection?: number } }
- | { type: 'LOAD_CACHED_TASKS'; payload: TaskMasterTask[] }
- | { type: 'ADD_TOAST'; payload: ToastNotification }
- | { type: 'REMOVE_TOAST'; payload: string }
- | { type: 'CLEAR_ALL_TOASTS' }
- | { type: 'NAVIGATE_TO_TASK'; payload: string }
- | { type: 'NAVIGATE_TO_KANBAN' };
+ | { type: 'SET_TASKS'; payload: TaskMasterTask[] }
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'SET_ERROR'; payload: string }
+ | { type: 'CLEAR_ERROR' }
+ | { type: 'INCREMENT_REQUEST_ID' }
+ | {
+ type: 'UPDATE_TASK_STATUS';
+ payload: { taskId: string; newStatus: TaskMasterTask['status'] };
+ }
+ | {
+ type: 'UPDATE_TASK_CONTENT';
+ payload: { taskId: string; updates: TaskUpdates };
+ }
+ | {
+ type: 'SET_CONNECTION_STATUS';
+ payload: { isConnected: boolean; status: string };
+ }
+ | {
+ type: 'SET_EDITING_TASK';
+ payload: { taskId: string | null; editData?: TaskMasterTask };
+ }
+ | {
+ type: 'SET_POLLING_STATUS';
+ payload: { isActive: boolean; errorCount?: number };
+ }
+ | { type: 'SET_USER_INTERACTING'; payload: boolean }
+ | { type: 'TASKS_UPDATED_FROM_POLLING'; payload: TaskMasterTask[] }
+ | {
+ type: 'SET_NETWORK_STATUS';
+ payload: {
+ isOfflineMode: boolean;
+ connectionStatus: 'online' | 'offline' | 'reconnecting';
+ reconnectAttempts?: number;
+ maxReconnectAttempts?: number;
+ lastSuccessfulConnection?: number;
+ };
+ }
+ | { type: 'LOAD_CACHED_TASKS'; payload: TaskMasterTask[] }
+ | { type: 'ADD_TOAST'; payload: ToastNotification }
+ | { type: 'REMOVE_TOAST'; payload: string }
+ | { type: 'CLEAR_ALL_TOASTS' }
+ | { type: 'NAVIGATE_TO_TASK'; payload: string }
+ | { type: 'NAVIGATE_TO_KANBAN' };
// Toast notification interfaces
interface ToastNotification {
- id: string;
- type: 'success' | 'info' | 'warning' | 'error';
- title: string;
- message: string;
- duration?: number;
- timestamp: number;
+ id: string;
+ type: 'success' | 'info' | 'warning' | 'error';
+ title: string;
+ message: string;
+ duration?: number;
+ timestamp: number;
}
interface ErrorBoundaryState {
- hasError: boolean;
- error?: Error;
- errorInfo?: React.ErrorInfo;
+ hasError: boolean;
+ error?: Error;
+ errorInfo?: React.ErrorInfo;
}
// Error Boundary Component
class ErrorBoundary extends React.Component<
- { children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void },
- ErrorBoundaryState
+ {
+ children: React.ReactNode;
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
+ },
+ ErrorBoundaryState
> {
- constructor(props: { children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void }) {
- super(props);
- this.state = { hasError: false };
- }
+ constructor(props: {
+ children: React.ReactNode;
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
+ }) {
+ super(props);
+ this.state = { hasError: false };
+ }
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
- return { hasError: true, error };
- }
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
+ return { hasError: true, error };
+ }
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- console.error('React Error Boundary caught an error:', error, errorInfo);
-
- this.setState({ error, errorInfo });
-
- // Notify parent component of error
- if (this.props.onError) {
- this.props.onError(error, errorInfo);
- }
-
- // Send error to extension for centralized handling
- if (window.acquireVsCodeApi) {
- const vscode = window.acquireVsCodeApi();
- vscode.postMessage({
- type: 'reactError',
- data: {
- message: error.message,
- stack: error.stack,
- componentStack: errorInfo.componentStack,
- timestamp: Date.now()
- }
- });
- }
- }
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+ console.error('React Error Boundary caught an error:', error, errorInfo);
- render() {
- if (this.state.hasError) {
- return (
-
-
-
-
Something went wrong
-
- The Task Master Kanban board encountered an unexpected error.
-
-
- this.setState({ hasError: false, error: undefined, errorInfo: undefined })}
- className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
- >
- Try Again
-
- window.location.reload()}
- className="w-full px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-md transition-colors"
- >
- Reload Extension
-
-
- {this.state.error && (
-
- Error Details
-
- {this.state.error.message}
- {this.state.error.stack && `\n\n${this.state.error.stack}`}
-
-
- )}
-
-
- );
- }
+ this.setState({ error, errorInfo });
- return this.props.children;
- }
+ // Notify parent component of error
+ if (this.props.onError) {
+ this.props.onError(error, errorInfo);
+ }
+
+ // Send error to extension for centralized handling
+ if (window.acquireVsCodeApi) {
+ const vscode = window.acquireVsCodeApi();
+ vscode.postMessage({
+ type: 'reactError',
+ data: {
+ message: error.message,
+ stack: error.stack,
+ componentStack: errorInfo.componentStack,
+ timestamp: Date.now()
+ }
+ });
+ }
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+
+ Something went wrong
+
+
+ The Task Master Kanban board encountered an unexpected error.
+
+
+
+ this.setState({
+ hasError: false,
+ error: undefined,
+ errorInfo: undefined
+ })
+ }
+ className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
+ >
+ Try Again
+
+ window.location.reload()}
+ className="w-full px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-md transition-colors"
+ >
+ Reload Extension
+
+
+ {this.state.error && (
+
+
+ Error Details
+
+
+ {this.state.error.message}
+ {this.state.error.stack && `\n\n${this.state.error.stack}`}
+
+
+ )}
+
+
+ );
+ }
+
+ return this.props.children;
+ }
}
// Toast Notification Component
const ToastNotification: React.FC<{
- notification: ToastNotification;
- onDismiss: (id: string) => void;
+ notification: ToastNotification;
+ onDismiss: (id: string) => void;
}> = ({ notification, onDismiss }) => {
- const [isVisible, setIsVisible] = useState(true);
- const [progress, setProgress] = useState(100);
+ const [isVisible, setIsVisible] = useState(true);
+ const [progress, setProgress] = useState(100);
- const duration = notification.duration || 5000; // 5 seconds default
+ const duration = notification.duration || 5000; // 5 seconds default
- useEffect(() => {
- const progressInterval = setInterval(() => {
- setProgress(prev => {
- const decrease = (100 / duration) * 100; // Update every 100ms
- return Math.max(0, prev - decrease);
- });
- }, 100);
+ useEffect(() => {
+ const progressInterval = setInterval(() => {
+ setProgress((prev) => {
+ const decrease = (100 / duration) * 100; // Update every 100ms
+ return Math.max(0, prev - decrease);
+ });
+ }, 100);
- const timeoutId = setTimeout(() => {
- setIsVisible(false);
- setTimeout(() => onDismiss(notification.id), 300); // Wait for animation
- }, duration);
+ const timeoutId = setTimeout(() => {
+ setIsVisible(false);
+ setTimeout(() => onDismiss(notification.id), 300); // Wait for animation
+ }, duration);
- return () => {
- clearInterval(progressInterval);
- clearTimeout(timeoutId);
- };
- }, [notification.id, duration, onDismiss]);
+ return () => {
+ clearInterval(progressInterval);
+ clearTimeout(timeoutId);
+ };
+ }, [notification.id, duration, onDismiss]);
- const getIcon = () => {
- switch (notification.type) {
- case 'success':
- return (
-
-
-
- );
- case 'warning':
- return (
-
-
-
- );
- case 'error':
- return (
-
-
-
- );
- default:
- return (
-
-
-
- );
- }
- };
+ const getIcon = () => {
+ switch (notification.type) {
+ case 'success':
+ return (
+
+
+
+ );
+ case 'warning':
+ return (
+
+
+
+ );
+ case 'error':
+ return (
+
+
+
+ );
+ default:
+ return (
+
+
+
+ );
+ }
+ };
- const getColorClasses = () => {
- switch (notification.type) {
- case 'success':
- return 'bg-green-500/10 border-green-500/30 text-green-400';
- case 'warning':
- return 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400';
- case 'error':
- return 'bg-red-500/10 border-red-500/30 text-red-400';
- default:
- return 'bg-blue-500/10 border-blue-500/30 text-blue-400';
- }
- };
+ const getColorClasses = () => {
+ switch (notification.type) {
+ case 'success':
+ return 'bg-green-500/10 border-green-500/30 text-green-400';
+ case 'warning':
+ return 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400';
+ case 'error':
+ return 'bg-red-500/10 border-red-500/30 text-red-400';
+ default:
+ return 'bg-blue-500/10 border-blue-500/30 text-blue-400';
+ }
+ };
- return (
-
-
-
- {getIcon()}
-
-
-
- {notification.title}
-
-
- {notification.message}
-
-
-
onDismiss(notification.id)}
- className="flex-shrink-0 ml-2 text-vscode-foreground/50 hover:text-vscode-foreground transition-colors"
- >
-
-
-
-
-
-
- {/* Progress bar */}
-
-
- );
+ >
+
+
{getIcon()}
+
+
+ {notification.title}
+
+
+ {notification.message}
+
+
+
onDismiss(notification.id)}
+ className="flex-shrink-0 ml-2 text-vscode-foreground/50 hover:text-vscode-foreground transition-colors"
+ >
+
+
+
+
+
+
+ {/* Progress bar */}
+
+
+ );
};
// Toast Container Component
const ToastContainer: React.FC<{
- notifications: ToastNotification[];
- onDismiss: (id: string) => void;
+ notifications: ToastNotification[];
+ onDismiss: (id: string) => void;
}> = ({ notifications, onDismiss }) => {
- return (
-
-
- {notifications.map(notification => (
-
-
-
- ))}
-
-
- );
+ return (
+
+
+ {notifications.map((notification) => (
+
+
+
+ ))}
+
+
+ );
};
// Toast helper functions
const createToast = (
- type: ToastNotification['type'],
- title: string,
- message: string,
- duration?: number
+ type: ToastNotification['type'],
+ title: string,
+ message: string,
+ duration?: number
): ToastNotification => {
- return {
- id: `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
- type,
- title,
- message,
- duration,
- timestamp: Date.now()
- };
+ return {
+ id: `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ type,
+ title,
+ message,
+ duration,
+ timestamp: Date.now()
+ };
};
-const showSuccessToast = (dispatch: React.Dispatch
) => (title: string, message: string, duration?: number) => {
- dispatch({ type: 'ADD_TOAST', payload: createToast('success', title, message, duration) });
-};
+const showSuccessToast =
+ (dispatch: React.Dispatch) =>
+ (title: string, message: string, duration?: number) => {
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast('success', title, message, duration)
+ });
+ };
-const showInfoToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => {
- dispatch({ type: 'ADD_TOAST', payload: createToast('info', title, message, duration) });
-};
+const showInfoToast =
+ (dispatch: React.Dispatch) =>
+ (title: string, message: string, duration?: number) => {
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast('info', title, message, duration)
+ });
+ };
-const showWarningToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => {
- dispatch({ type: 'ADD_TOAST', payload: createToast('warning', title, message, duration) });
-};
+const showWarningToast =
+ (dispatch: React.Dispatch) =>
+ (title: string, message: string, duration?: number) => {
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast('warning', title, message, duration)
+ });
+ };
-const showErrorToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => {
- dispatch({ type: 'ADD_TOAST', payload: createToast('error', title, message, duration) });
-};
+const showErrorToast =
+ (dispatch: React.Dispatch) =>
+ (title: string, message: string, duration?: number) => {
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast('error', title, message, duration)
+ });
+ };
// Kanban column configuration
const kanbanStatuses: Status[] = [
- { id: 'pending', name: 'To Do', color: '#6B7280' },
- { id: 'in-progress', name: 'In Progress', color: '#F59E0B' },
- { id: 'review', name: 'Review', color: '#8B5CF6' },
- { id: 'done', name: 'Done', color: '#10B981' },
- { id: 'deferred', name: 'Deferred', color: '#EF4444' },
+ { id: 'pending', name: 'To Do', color: '#6B7280' },
+ { id: 'in-progress', name: 'In Progress', color: '#F59E0B' },
+ { id: 'review', name: 'Review', color: '#8B5CF6' },
+ { id: 'done', name: 'Done', color: '#10B981' },
+ { id: 'deferred', name: 'Deferred', color: '#EF4444' }
];
// State reducer
const appReducer = (state: AppState, action: AppAction): AppState => {
- switch (action.type) {
- case 'SET_TASKS':
- return { ...state, tasks: action.payload, loading: false, error: undefined };
- case 'SET_LOADING':
- return { ...state, loading: action.payload };
- case 'SET_ERROR':
- return { ...state, error: action.payload, loading: false };
- case 'CLEAR_ERROR':
- return { ...state, error: undefined };
- case 'INCREMENT_REQUEST_ID':
- return { ...state, requestId: state.requestId + 1 };
- case 'UPDATE_TASK_STATUS':
- const updatedTasks = state.tasks.map(task =>
- task.id === action.payload.taskId
- ? { ...task, status: action.payload.newStatus }
- : task
- );
- return { ...state, tasks: updatedTasks };
- case 'UPDATE_TASK_CONTENT':
- const updatedTasksContent = state.tasks.map(task =>
- task.id === action.payload.taskId
- ? { ...task, ...action.payload.updates }
- : task
- );
- return { ...state, tasks: updatedTasksContent };
- case 'SET_CONNECTION_STATUS':
- return {
- ...state,
- isConnected: action.payload.isConnected,
- connectionStatus: action.payload.status
- };
- case 'SET_EDITING_TASK':
- return { ...state, editingTask: action.payload };
- case 'SET_POLLING_STATUS':
- return { ...state, polling: { ...state.polling, ...action.payload } };
- case 'SET_USER_INTERACTING':
- return { ...state, polling: { ...state.polling, isUserInteracting: action.payload } };
- case 'TASKS_UPDATED_FROM_POLLING':
- return { ...state, tasks: action.payload };
- case 'SET_NETWORK_STATUS':
- return { ...state, polling: { ...state.polling, ...action.payload } };
- case 'LOAD_CACHED_TASKS':
- return { ...state, tasks: action.payload };
- case 'ADD_TOAST':
- return { ...state, toastNotifications: [...state.toastNotifications, action.payload] };
- case 'REMOVE_TOAST':
- return { ...state, toastNotifications: state.toastNotifications.filter(n => n.id !== action.payload) };
- case 'CLEAR_ALL_TOASTS':
- return { ...state, toastNotifications: [] };
- case 'NAVIGATE_TO_TASK':
- console.log('📍 Reducer: Navigating to task', action.payload);
- return { ...state, currentView: 'task-details', selectedTaskId: action.payload };
- case 'NAVIGATE_TO_KANBAN':
- console.log('📍 Reducer: Navigating to kanban');
- return { ...state, currentView: 'kanban', selectedTaskId: undefined };
- default:
- return state;
- }
+ switch (action.type) {
+ case 'SET_TASKS':
+ return {
+ ...state,
+ tasks: action.payload,
+ loading: false,
+ error: undefined
+ };
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+ case 'SET_ERROR':
+ return { ...state, error: action.payload, loading: false };
+ case 'CLEAR_ERROR':
+ return { ...state, error: undefined };
+ case 'INCREMENT_REQUEST_ID':
+ return { ...state, requestId: state.requestId + 1 };
+ case 'UPDATE_TASK_STATUS':
+ const updatedTasks = state.tasks.map((task) =>
+ task.id === action.payload.taskId
+ ? { ...task, status: action.payload.newStatus }
+ : task
+ );
+ return { ...state, tasks: updatedTasks };
+ case 'UPDATE_TASK_CONTENT':
+ const updatedTasksContent = state.tasks.map((task) =>
+ task.id === action.payload.taskId
+ ? { ...task, ...action.payload.updates }
+ : task
+ );
+ return { ...state, tasks: updatedTasksContent };
+ case 'SET_CONNECTION_STATUS':
+ return {
+ ...state,
+ isConnected: action.payload.isConnected,
+ connectionStatus: action.payload.status
+ };
+ case 'SET_EDITING_TASK':
+ return { ...state, editingTask: action.payload };
+ case 'SET_POLLING_STATUS':
+ return { ...state, polling: { ...state.polling, ...action.payload } };
+ case 'SET_USER_INTERACTING':
+ return {
+ ...state,
+ polling: { ...state.polling, isUserInteracting: action.payload }
+ };
+ case 'TASKS_UPDATED_FROM_POLLING':
+ return { ...state, tasks: action.payload };
+ case 'SET_NETWORK_STATUS':
+ return { ...state, polling: { ...state.polling, ...action.payload } };
+ case 'LOAD_CACHED_TASKS':
+ return { ...state, tasks: action.payload };
+ case 'ADD_TOAST':
+ return {
+ ...state,
+ toastNotifications: [...state.toastNotifications, action.payload]
+ };
+ case 'REMOVE_TOAST':
+ return {
+ ...state,
+ toastNotifications: state.toastNotifications.filter(
+ (n) => n.id !== action.payload
+ )
+ };
+ case 'CLEAR_ALL_TOASTS':
+ return { ...state, toastNotifications: [] };
+ case 'NAVIGATE_TO_TASK':
+ console.log('📍 Reducer: Navigating to task', action.payload);
+ return {
+ ...state,
+ currentView: 'task-details',
+ selectedTaskId: action.payload
+ };
+ case 'NAVIGATE_TO_KANBAN':
+ console.log('📍 Reducer: Navigating to kanban');
+ return { ...state, currentView: 'kanban', selectedTaskId: undefined };
+ default:
+ return state;
+ }
};
// Context for VS Code API
export const VSCodeContext = createContext<{
- vscode?: ReturnType>;
- state: AppState;
- dispatch: React.Dispatch;
- sendMessage: (message: WebviewMessage) => Promise;
- availableHeight: number;
- // Toast notification functions
- showSuccessToast: (title: string, message: string, duration?: number) => void;
- showInfoToast: (title: string, message: string, duration?: number) => void;
- showWarningToast: (title: string, message: string, duration?: number) => void;
- showErrorToast: (title: string, message: string, duration?: number) => void;
+ vscode?: ReturnType>;
+ state: AppState;
+ dispatch: React.Dispatch;
+ sendMessage: (message: WebviewMessage) => Promise;
+ availableHeight: number;
+ // Toast notification functions
+ showSuccessToast: (title: string, message: string, duration?: number) => void;
+ showInfoToast: (title: string, message: string, duration?: number) => void;
+ showWarningToast: (title: string, message: string, duration?: number) => void;
+ showErrorToast: (title: string, message: string, duration?: number) => void;
} | null>(null);
// Priority Badge Component
-const PriorityBadge: React.FC<{ priority: TaskMasterTask['priority'] }> = ({ priority }) => {
- const colorMap = {
- high: 'bg-red-500/20 text-red-400 border-red-500/30',
- medium: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
- low: 'bg-green-500/20 text-green-400 border-green-500/30',
- };
+const PriorityBadge: React.FC<{ priority: TaskMasterTask['priority'] }> = ({
+ priority
+}) => {
+ const colorMap = {
+ high: 'bg-red-500/20 text-red-400 border-red-500/30',
+ medium: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
+ low: 'bg-green-500/20 text-green-400 border-green-500/30'
+ };
- return (
-
- {priority}
-
- );
+ title={priority}
+ >
+ {priority}
+
+ );
};
// Task Edit Modal Component
const TaskEditModal: React.FC<{
- task: TaskMasterTask;
- onSave: (taskId: string, updates: TaskUpdates) => void;
- onCancel: () => void;
+ task: TaskMasterTask;
+ onSave: (taskId: string, updates: TaskUpdates) => void;
+ onCancel: () => void;
}> = ({ task, onSave, onCancel }) => {
- const [formData, setFormData] = useState({
- title: task.title,
- description: task.description,
- details: task.details || '',
- priority: task.priority,
- testStrategy: task.testStrategy || '',
- dependencies: task.dependencies || []
- });
+ const [formData, setFormData] = useState({
+ title: task.title,
+ description: task.description,
+ details: task.details || '',
+ priority: task.priority,
+ testStrategy: task.testStrategy || '',
+ dependencies: task.dependencies || []
+ });
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
-
- // Only include changed fields
- const updates: TaskUpdates = {};
- if (formData.title !== task.title) updates.title = formData.title;
- if (formData.description !== task.description) updates.description = formData.description;
- if (formData.details !== task.details) updates.details = formData.details;
- if (formData.priority !== task.priority) updates.priority = formData.priority;
- if (formData.testStrategy !== task.testStrategy) updates.testStrategy = formData.testStrategy;
- if (JSON.stringify(formData.dependencies) !== JSON.stringify(task.dependencies)) {
- updates.dependencies = formData.dependencies;
- }
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
- if (Object.keys(updates).length > 0) {
- onSave(task.id, updates);
- } else {
- onCancel(); // No changes made
- }
- };
+ // Only include changed fields
+ const updates: TaskUpdates = {};
+ if (formData.title !== task.title) updates.title = formData.title;
+ if (formData.description !== task.description)
+ updates.description = formData.description;
+ if (formData.details !== task.details) updates.details = formData.details;
+ if (formData.priority !== task.priority)
+ updates.priority = formData.priority;
+ if (formData.testStrategy !== task.testStrategy)
+ updates.testStrategy = formData.testStrategy;
+ if (
+ JSON.stringify(formData.dependencies) !==
+ JSON.stringify(task.dependencies)
+ ) {
+ updates.dependencies = formData.dependencies;
+ }
- const handleDependenciesChange = (value: string) => {
- const deps = value.split(',').map(dep => dep.trim()).filter(dep => dep.length > 0);
- setFormData(prev => ({ ...prev, dependencies: deps }));
- };
+ if (Object.keys(updates).length > 0) {
+ onSave(task.id, updates);
+ } else {
+ onCancel(); // No changes made
+ }
+ };
- return (
-
-
-
-
Edit Task #{task.id}
-
-
-
+
+
+ );
};
// Task Card Component
const TaskCard: React.FC<{
- task: TaskMasterTask;
- index: number;
- status: string;
- onEdit?: (task: TaskMasterTask) => void;
- onViewDetails?: (taskId: string) => void;
+ task: TaskMasterTask;
+ index: number;
+ status: string;
+ onEdit?: (task: TaskMasterTask) => void;
+ onViewDetails?: (taskId: string) => void;
}> = ({ task, index, status, onEdit, onViewDetails }) => {
- const handleClick = (e: React.MouseEvent) => {
- onViewDetails?.(task.id);
- };
-
- const handleDoubleClick = (e: React.MouseEvent) => {
- onViewDetails?.(task.id);
- };
+ const handleClick = (e: React.MouseEvent) => {
+ onViewDetails?.(task.id);
+ };
- return (
-
-
-
-
- {task.description && (
-
- {task.description}
-
- )}
-
-
-
- #{task.id}
-
- {task.dependencies && task.dependencies.length > 0 && (
-
- Deps: {task.dependencies.length}
-
- )}
-
-
-
- );
+ >
+
+
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+
+
+
+ #{task.id}
+
+ {task.dependencies && task.dependencies.length > 0 && (
+
+ Deps: {task.dependencies.length}
+
+ )}
+
+
+
+ );
};
// Main Kanban Board Component
const TaskMasterKanban: React.FC = () => {
- const context = useContext(VSCodeContext);
- if (!context) throw new Error('TaskMasterKanban must be used within VSCodeContext');
+ const context = useContext(VSCodeContext);
+ if (!context)
+ throw new Error('TaskMasterKanban must be used within VSCodeContext');
- const { state, dispatch, sendMessage, availableHeight } = context;
- const { tasks, loading, error, editingTask, polling, currentView, selectedTaskId } = state;
- const [activeTask, setActiveTask] = React.useState(null);
+ const { state, dispatch, sendMessage, availableHeight } = context;
+ const {
+ tasks,
+ loading,
+ error,
+ editingTask,
+ polling,
+ currentView,
+ selectedTaskId
+ } = state;
+ const [activeTask, setActiveTask] = React.useState(
+ null
+ );
- // Calculate header height for proper kanban board sizing
- const headerHeight = 73; // Header with padding and border
- const kanbanHeight = availableHeight - headerHeight;
+ // Calculate header height for proper kanban board sizing
+ const headerHeight = 73; // Header with padding and border
+ const kanbanHeight = availableHeight - headerHeight;
- // Group tasks by status
- const tasksByStatus = kanbanStatuses.reduce((acc, status) => {
- acc[status.id] = tasks.filter(task => task.status === status.id);
- return acc;
- }, {} as Record);
+ // Group tasks by status
+ const tasksByStatus = kanbanStatuses.reduce(
+ (acc, status) => {
+ acc[status.id] = tasks.filter((task) => task.status === status.id);
+ return acc;
+ },
+ {} as Record
+ );
- // Handle task update
- const handleUpdateTask = async (taskId: string, updates: TaskUpdates) => {
- console.log(`🔄 Updating task ${taskId} content:`, updates);
-
- // Optimistic update
- dispatch({
- type: 'UPDATE_TASK_CONTENT',
- payload: { taskId, updates }
- });
+ // Handle task update
+ const handleUpdateTask = async (taskId: string, updates: TaskUpdates) => {
+ console.log(`🔄 Updating task ${taskId} content:`, updates);
- try {
- // Send update to extension
- await sendMessage({
- type: 'updateTask',
- data: {
- taskId,
- updates,
- options: { append: false, research: false }
- }
- });
-
- console.log(`✅ Task ${taskId} content updated successfully`);
-
- // Close the edit modal
- dispatch({
- type: 'SET_EDITING_TASK',
- payload: { taskId: null }
- });
-
- } catch (error) {
- console.error(`❌ Failed to update task ${taskId}:`, error);
-
- // Revert the optimistic update on error
- const originalTask = editingTask?.editData;
- if (originalTask) {
- dispatch({
- type: 'UPDATE_TASK_CONTENT',
- payload: {
- taskId,
- updates: {
- title: originalTask.title,
- description: originalTask.description,
- details: originalTask.details,
- priority: originalTask.priority,
- testStrategy: originalTask.testStrategy,
- dependencies: originalTask.dependencies
- }
- }
- });
- }
-
- dispatch({
- type: 'SET_ERROR',
- payload: `Failed to update task: ${error}`
- });
- }
- };
+ // Optimistic update
+ dispatch({
+ type: 'UPDATE_TASK_CONTENT',
+ payload: { taskId, updates }
+ });
- // Handle drag start - mark user as interacting and set active task
- const handleDragStart = (event: DragEndEvent) => {
- console.log('🖱️ User started dragging, pausing updates');
- dispatch({ type: 'SET_USER_INTERACTING', payload: true });
-
- const taskId = event.active.id as string;
- const task = tasks.find(t => t.id === taskId);
- setActiveTask(task || null);
- };
+ try {
+ // Send update to extension
+ await sendMessage({
+ type: 'updateTask',
+ data: {
+ taskId,
+ updates,
+ options: { append: false, research: false }
+ }
+ });
- // Handle drag end - allow updates again after a delay
- const handleDragEnd = async (event: DragEndEvent) => {
- const { active, over } = event;
-
- // Clear active task
- setActiveTask(null);
-
- // Re-enable updates after drag completes
- setTimeout(() => {
- console.log('✅ Drag completed, resuming updates');
- dispatch({ type: 'SET_USER_INTERACTING', payload: false });
- }, 1000); // 1 second delay to ensure smooth completion
-
- if (!over) return;
-
- const taskId = active.id as string;
- const newStatus = over.id as TaskMasterTask['status'];
-
- // Find the task that was moved
- const task = tasks.find(t => t.id === taskId);
- if (!task || task.status === newStatus) return;
+ console.log(`✅ Task ${taskId} content updated successfully`);
- console.log(`🔄 Moving task ${taskId} from ${task.status} to ${newStatus}`);
-
- // Update task status locally (optimistic update)
- dispatch({
- type: 'UPDATE_TASK_STATUS',
- payload: { taskId, newStatus }
- });
+ // Close the edit modal
+ dispatch({
+ type: 'SET_EDITING_TASK',
+ payload: { taskId: null }
+ });
+ } catch (error) {
+ console.error(`❌ Failed to update task ${taskId}:`, error);
- try {
- // Send update to extension
- await sendMessage({
- type: 'updateTaskStatus',
- data: { taskId, newStatus, oldStatus: task.status }
- });
-
- console.log(`✅ Task ${taskId} status updated successfully`);
- } catch (error) {
- console.error(`❌ Failed to update task ${taskId}:`, error);
-
- // Revert the optimistic update on error
- dispatch({
- type: 'UPDATE_TASK_STATUS',
- payload: { taskId, newStatus: task.status }
- });
-
- dispatch({
- type: 'SET_ERROR',
- payload: `Failed to update task status: ${error}`
- });
- }
- };
+ // Revert the optimistic update on error
+ const originalTask = editingTask?.editData;
+ if (originalTask) {
+ dispatch({
+ type: 'UPDATE_TASK_CONTENT',
+ payload: {
+ taskId,
+ updates: {
+ title: originalTask.title,
+ description: originalTask.description,
+ details: originalTask.details,
+ priority: originalTask.priority,
+ testStrategy: originalTask.testStrategy,
+ dependencies: originalTask.dependencies
+ }
+ }
+ });
+ }
- // Get polling status indicator
- const getPollingStatusIndicator = () => {
- const { isActive, errorCount, isOfflineMode, connectionStatus, reconnectAttempts, maxReconnectAttempts } = polling;
-
- if (isOfflineMode || connectionStatus === 'offline') {
- return (
-
-
-
{
- try {
- await sendMessage({ type: 'attemptReconnection' });
- } catch (error) {
- console.error('Failed to request reconnection:', error);
- }
- }}
- className="px-2 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-500/30 transition-colors"
- title="Attempt to reconnect"
- >
- Reconnect
-
-
- );
- } else if (connectionStatus === 'reconnecting') {
- return (
-
- );
- } else if (isActive) {
- return (
-
- );
- } else if (errorCount > 0) {
- return (
-
- );
- } else {
- return (
-
- );
- }
- };
+ dispatch({
+ type: 'SET_ERROR',
+ payload: `Failed to update task: ${error}`
+ });
+ }
+ };
- if (loading) {
- return (
-
- );
- }
+ // Handle drag start - mark user as interacting and set active task
+ const handleDragStart = (event: DragEndEvent) => {
+ console.log('🖱️ User started dragging, pausing updates');
+ dispatch({ type: 'SET_USER_INTERACTING', payload: true });
- if (error) {
- return (
-
-
Error: {error}
-
dispatch({ type: 'CLEAR_ERROR' })}
- className="mt-2 text-sm text-red-400 hover:text-red-300 underline"
- >
- Dismiss
-
-
- );
- }
+ const taskId = event.active.id as string;
+ const task = tasks.find((t) => t.id === taskId);
+ setActiveTask(task || null);
+ };
+ // Handle drag end - allow updates again after a delay
+ const handleDragEnd = async (event: DragEndEvent) => {
+ const { active, over } = event;
+ // Clear active task
+ setActiveTask(null);
- return (
- <>
-
-
-
-
Task Master Kanban
-
- {getPollingStatusIndicator()}
-
-
-
- {state.connectionStatus}
-
-
-
-
-
-
-
-
+
+ {
+ try {
+ await sendMessage({ type: 'attemptReconnection' });
+ } catch (error) {
+ console.error('Failed to request reconnection:', error);
+ }
+ }}
+ className="px-2 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-500/30 transition-colors"
+ title="Attempt to reconnect"
+ >
+ Reconnect
+
+
+ );
+ } else if (connectionStatus === 'reconnecting') {
+ return (
+
+ );
+ } else if (isActive) {
+ return (
+
+ );
+ } else if (errorCount > 0) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error: {error}
+
dispatch({ type: 'CLEAR_ERROR' })}
+ className="mt-2 text-sm text-red-400 hover:text-red-300 underline"
+ >
+ Dismiss
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ Task Master Kanban
+
+
+ {getPollingStatusIndicator()}
+
+
+
+ {state.connectionStatus}
+
+
+
+
+
+
+
+
{}}
- onViewDetails={() => {}}
- />
- ) : null
- }
- >
-
- {kanbanStatuses.map(status => {
- const columnHeaderHeight = 49; // Header with padding and border
- const columnPadding = 16; // p-2 = 8px top + 8px bottom
- const availableColumnHeight = kanbanHeight - columnHeaderHeight - columnPadding;
-
- return (
-
+ {kanbanStatuses.map((status) => {
+ const columnHeaderHeight = 49; // Header with padding and border
+ const columnPadding = 16; // p-2 = 8px top + 8px bottom
+ const availableColumnHeight =
+ kanbanHeight - columnHeaderHeight - columnPadding;
+
+ return (
+
-
-
+
-
- {tasksByStatus[status.id]?.map((task, index) => (
- {
- dispatch({
- type: 'SET_EDITING_TASK',
- payload: { taskId: task.id, editData: task }
- });
- }}
- onViewDetails={(taskId) => {
- console.log('🔍 Navigating to task details:', taskId);
- dispatch({
- type: 'NAVIGATE_TO_TASK',
- payload: taskId
- });
- }}
- />
- ))}
-
-
-
- );
- })}
-
-
-
-
+ style={{
+ height: `${availableColumnHeight}px`,
+ maxHeight: `${availableColumnHeight}px`
+ }}
+ >
+
+ {tasksByStatus[status.id]?.map((task, index) => (
+ {
+ dispatch({
+ type: 'SET_EDITING_TASK',
+ payload: { taskId: task.id, editData: task }
+ });
+ }}
+ onViewDetails={(taskId) => {
+ console.log(
+ '🔍 Navigating to task details:',
+ taskId
+ );
+ dispatch({
+ type: 'NAVIGATE_TO_TASK',
+ payload: taskId
+ });
+ }}
+ />
+ ))}
+
+
+
+ );
+ })}
+
+
+
+
- {/* Task Edit Modal */}
- {editingTask?.taskId && editingTask.editData && (
- {
- await handleUpdateTask(taskId, updates);
- }}
- onCancel={() => {
- dispatch({
- type: 'SET_EDITING_TASK',
- payload: { taskId: null }
- });
- }}
- />
- )}
- >
- );
+ {/* Task Edit Modal */}
+ {editingTask?.taskId && editingTask.editData && (
+ {
+ await handleUpdateTask(taskId, updates);
+ }}
+ onCancel={() => {
+ dispatch({
+ type: 'SET_EDITING_TASK',
+ payload: { taskId: null }
+ });
+ }}
+ />
+ )}
+ >
+ );
};
// Main App Component
const App: React.FC = () => {
- const [state, dispatch] = useReducer(appReducer, {
- tasks: [],
- loading: true,
- requestId: 0,
- isConnected: false,
- connectionStatus: 'Connecting...',
- editingTask: { taskId: null },
- polling: {
- isActive: false,
- errorCount: 0,
- lastUpdate: undefined,
- isUserInteracting: false,
- // Network status
- isOfflineMode: false,
- reconnectAttempts: 0,
- maxReconnectAttempts: 0,
- lastSuccessfulConnection: undefined,
- connectionStatus: 'online'
- },
- toastNotifications: [],
- currentView: 'kanban',
- selectedTaskId: undefined,
- });
+ const [state, dispatch] = useReducer(appReducer, {
+ tasks: [],
+ loading: true,
+ requestId: 0,
+ isConnected: false,
+ connectionStatus: 'Connecting...',
+ editingTask: { taskId: null },
+ polling: {
+ isActive: false,
+ errorCount: 0,
+ lastUpdate: undefined,
+ isUserInteracting: false,
+ // Network status
+ isOfflineMode: false,
+ reconnectAttempts: 0,
+ maxReconnectAttempts: 0,
+ lastSuccessfulConnection: undefined,
+ connectionStatus: 'online'
+ },
+ toastNotifications: [],
+ currentView: 'kanban',
+ selectedTaskId: undefined
+ });
- const [vscode] = useState(() => {
- return window.acquireVsCodeApi?.();
- });
+ const [vscode] = useState(() => {
+ return window.acquireVsCodeApi?.();
+ });
- const [pendingRequests] = useState(new Map());
+ const [pendingRequests] = useState(
+ new Map<
+ string,
+ { resolve: Function; reject: Function; timeout: NodeJS.Timeout }
+ >()
+ );
- // Dynamic height calculation state
- const [availableHeight, setAvailableHeight] = useState(window.innerHeight);
+ // Dynamic height calculation state
+ const [availableHeight, setAvailableHeight] = useState(
+ window.innerHeight
+ );
- // Calculate available height for kanban board
- const updateAvailableHeight = useCallback(() => {
- // Use window.innerHeight to get the actual available space
- // This automatically accounts for VS Code panels like terminal, problems, etc.
- const height = window.innerHeight;
- console.log('📏 Available height updated:', height);
- setAvailableHeight(height);
- }, []);
+ // Calculate available height for kanban board
+ const updateAvailableHeight = useCallback(() => {
+ // Use window.innerHeight to get the actual available space
+ // This automatically accounts for VS Code panels like terminal, problems, etc.
+ const height = window.innerHeight;
+ console.log('📏 Available height updated:', height);
+ setAvailableHeight(height);
+ }, []);
- // Listen to resize events to handle VS Code panel changes
- useEffect(() => {
- updateAvailableHeight();
-
- const handleResize = () => {
- updateAvailableHeight();
- };
+ // Listen to resize events to handle VS Code panel changes
+ useEffect(() => {
+ updateAvailableHeight();
- window.addEventListener('resize', handleResize);
-
- // Also listen for VS Code specific events if available
- const handleVisibilityChange = () => {
- // Small delay to ensure VS Code has finished resizing
- setTimeout(updateAvailableHeight, 100);
- };
-
- document.addEventListener('visibilitychange', handleVisibilityChange);
-
- return () => {
- window.removeEventListener('resize', handleResize);
- document.removeEventListener('visibilitychange', handleVisibilityChange);
- };
- }, [updateAvailableHeight]);
+ const handleResize = () => {
+ updateAvailableHeight();
+ };
- // Send message to extension with promise-based response handling
- const sendMessage = useCallback(async (message: WebviewMessage): Promise => {
- if (!vscode) {
- throw new Error('VS Code API not available');
- }
+ window.addEventListener('resize', handleResize);
- return new Promise((resolve, reject) => {
- const requestId = `${Date.now()}-${Math.random()}`;
- const messageWithId = { ...message, requestId };
+ // Also listen for VS Code specific events if available
+ const handleVisibilityChange = () => {
+ // Small delay to ensure VS Code has finished resizing
+ setTimeout(updateAvailableHeight, 100);
+ };
- // Set up timeout
- const timeout = setTimeout(() => {
- pendingRequests.delete(requestId);
- reject(new Error('Request timeout'));
- }, 10000); // 10 second timeout
+ document.addEventListener('visibilitychange', handleVisibilityChange);
- // Store the promise resolvers
- pendingRequests.set(requestId, { resolve, reject, timeout });
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, [updateAvailableHeight]);
- // Send the message
- vscode.postMessage(messageWithId);
- });
- }, [vscode, pendingRequests]);
+ // Send message to extension with promise-based response handling
+ const sendMessage = useCallback(
+ async (message: WebviewMessage): Promise => {
+ if (!vscode) {
+ throw new Error('VS Code API not available');
+ }
- // Handle messages from extension
- useEffect(() => {
- if (!vscode) return;
+ return new Promise((resolve, reject) => {
+ const requestId = `${Date.now()}-${Math.random()}`;
+ const messageWithId = { ...message, requestId };
- const handleMessage = (event: MessageEvent) => {
- const message: WebviewMessage = event.data;
- console.log('📨 Received message from extension:', message);
+ // Set up timeout
+ const timeout = setTimeout(() => {
+ pendingRequests.delete(requestId);
+ reject(new Error('Request timeout'));
+ }, 10000); // 10 second timeout
- // Handle response to a pending request
- if (message.requestId && pendingRequests.has(message.requestId)) {
- const { resolve, reject, timeout } = pendingRequests.get(message.requestId)!;
- clearTimeout(timeout);
- pendingRequests.delete(message.requestId);
+ // Store the promise resolvers
+ pendingRequests.set(requestId, { resolve, reject, timeout });
- if (message.type === 'error') {
- reject(new Error(message.error || 'Unknown error'));
- } else {
- resolve(message.data || message);
- }
- return;
- }
+ // Send the message
+ vscode.postMessage(messageWithId);
+ });
+ },
+ [vscode, pendingRequests]
+ );
- // Handle different message types
- switch (message.type) {
- case 'init':
- console.log('🚀 Extension initialized:', message.data);
- dispatch({
- type: 'SET_CONNECTION_STATUS',
- payload: { isConnected: true, status: 'Connected' }
- });
- break;
+ // Handle messages from extension
+ useEffect(() => {
+ if (!vscode) return;
- case 'tasksData':
- console.log('📋 Received tasks data:', message.data);
- dispatch({ type: 'SET_TASKS', payload: message.data });
- break;
+ const handleMessage = (event: MessageEvent) => {
+ const message: WebviewMessage = event.data;
+ console.log('📨 Received message from extension:', message);
- case 'taskStatusUpdated':
- console.log('✅ Task status updated:', message);
- // Status is already updated optimistically, no need to update again
- break;
+ // Handle response to a pending request
+ if (message.requestId && pendingRequests.has(message.requestId)) {
+ const { resolve, reject, timeout } = pendingRequests.get(
+ message.requestId
+ )!;
+ clearTimeout(timeout);
+ pendingRequests.delete(message.requestId);
- case 'taskUpdated':
- console.log('✅ Task content updated:', message);
- // Content is already updated optimistically, no need to update again
- break;
+ if (message.type === 'error') {
+ reject(new Error(message.error || 'Unknown error'));
+ } else {
+ resolve(message.data || message);
+ }
+ return;
+ }
- case 'tasksUpdated':
- console.log('📡 Tasks updated from polling:', message);
- // Only update if user is not currently interacting
- if (!state.polling.isUserInteracting) {
- dispatch({
- type: 'TASKS_UPDATED_FROM_POLLING',
- payload: message.data
- });
- dispatch({
- type: 'SET_POLLING_STATUS',
- payload: { isActive: true, errorCount: 0 }
- });
- } else {
- console.log('⏸️ Skipping update due to user interaction');
- }
- break;
+ // Handle different message types
+ switch (message.type) {
+ case 'init':
+ console.log('🚀 Extension initialized:', message.data);
+ dispatch({
+ type: 'SET_CONNECTION_STATUS',
+ payload: { isConnected: true, status: 'Connected' }
+ });
+ break;
- case 'pollingError':
- console.log('❌ Polling error:', message);
- dispatch({
- type: 'SET_POLLING_STATUS',
- payload: { isActive: false, errorCount: (state.polling.errorCount || 0) + 1 }
- });
- dispatch({
- type: 'SET_ERROR',
- payload: `Auto-refresh stopped: ${message.error}`
- });
- break;
+ case 'tasksData':
+ console.log('📋 Received tasks data:', message.data);
+ dispatch({ type: 'SET_TASKS', payload: message.data });
+ break;
- case 'pollingStarted':
- console.log('🔄 Polling started');
- dispatch({
- type: 'SET_POLLING_STATUS',
- payload: { isActive: true, errorCount: 0 }
- });
- break;
+ case 'taskStatusUpdated':
+ console.log('✅ Task status updated:', message);
+ // Status is already updated optimistically, no need to update again
+ break;
- case 'pollingStopped':
- console.log('⏹️ Polling stopped');
- dispatch({
- type: 'SET_POLLING_STATUS',
- payload: { isActive: false }
- });
- break;
+ case 'taskUpdated':
+ console.log('✅ Task content updated:', message);
+ // Content is already updated optimistically, no need to update again
+ break;
- case 'connectionStatusUpdate':
- console.log('📡 Connection status update:', message);
- dispatch({
- type: 'SET_NETWORK_STATUS',
- payload: {
- isOfflineMode: message.data.isOfflineMode,
- connectionStatus: message.data.status,
- reconnectAttempts: message.data.reconnectAttempts,
- maxReconnectAttempts: message.data.maxReconnectAttempts
- }
- });
- break;
+ case 'tasksUpdated':
+ console.log('📡 Tasks updated from polling:', message);
+ // Only update if user is not currently interacting
+ if (!state.polling.isUserInteracting) {
+ dispatch({
+ type: 'TASKS_UPDATED_FROM_POLLING',
+ payload: message.data
+ });
+ dispatch({
+ type: 'SET_POLLING_STATUS',
+ payload: { isActive: true, errorCount: 0 }
+ });
+ } else {
+ console.log('⏸️ Skipping update due to user interaction');
+ }
+ break;
- case 'networkOffline':
- console.log('🔌 Network offline, loading cached tasks:', message);
- dispatch({
- type: 'SET_NETWORK_STATUS',
- payload: {
- isOfflineMode: true,
- connectionStatus: 'offline',
- reconnectAttempts: message.data.reconnectAttempts,
- lastSuccessfulConnection: message.data.lastSuccessfulConnection
- }
- });
-
- // Load cached tasks if available
- if (message.data.cachedTasks && message.data.cachedTasks.length > 0) {
- dispatch({
- type: 'LOAD_CACHED_TASKS',
- payload: message.data.cachedTasks
- });
- }
- break;
+ case 'pollingError':
+ console.log('❌ Polling error:', message);
+ dispatch({
+ type: 'SET_POLLING_STATUS',
+ payload: {
+ isActive: false,
+ errorCount: (state.polling.errorCount || 0) + 1
+ }
+ });
+ dispatch({
+ type: 'SET_ERROR',
+ payload: `Auto-refresh stopped: ${message.error}`
+ });
+ break;
- case 'reconnectionAttempted':
- console.log('🔄 Reconnection attempted:', message);
- if (message.success) {
- dispatch({
- type: 'CLEAR_ERROR'
- });
- }
- break;
+ case 'pollingStarted':
+ console.log('🔄 Polling started');
+ dispatch({
+ type: 'SET_POLLING_STATUS',
+ payload: { isActive: true, errorCount: 0 }
+ });
+ break;
- case 'errorNotification':
- console.log('⚠️ Error notification from extension:', message);
- const errorData = message.data;
-
- // Map error severity to toast type
- let toastType: ToastNotification['type'] = 'error';
- if (errorData.severity === 'low') toastType = 'info';
- else if (errorData.severity === 'medium') toastType = 'warning';
- else if (errorData.severity === 'high' || errorData.severity === 'critical') toastType = 'error';
-
- // Create appropriate toast based on error category
- const title = errorData.category === 'network' ? 'Network Error' :
- errorData.category === 'mcp_connection' ? 'Connection Error' :
- errorData.category === 'task_loading' ? 'Task Loading Error' :
- errorData.category === 'ui_rendering' ? 'UI Error' :
- 'Error';
-
- dispatch({
- type: 'ADD_TOAST',
- payload: createToast(
- toastType,
- title,
- errorData.message,
- errorData.duration || (toastType === 'error' ? 8000 : 5000) // Use preference duration or fallback
- )
- });
- break;
+ case 'pollingStopped':
+ console.log('⏹️ Polling stopped');
+ dispatch({
+ type: 'SET_POLLING_STATUS',
+ payload: { isActive: false }
+ });
+ break;
- case 'error':
- console.log('❌ General error from extension:', message);
- const errorTitle = message.errorType === 'connection' ? 'Connection Error' : 'Error';
- const errorMessage = message.error || 'An unknown error occurred';
-
- dispatch({
- type: 'SET_ERROR',
- payload: errorMessage
- });
-
- dispatch({
- type: 'ADD_TOAST',
- payload: createToast(
- 'error',
- errorTitle,
- errorMessage,
- 8000
- )
- });
-
- // Set offline mode for connection errors
- if (message.errorType === 'connection') {
- dispatch({
- type: 'SET_NETWORK_STATUS',
- payload: {
- isOfflineMode: true,
- connectionStatus: 'offline',
- reconnectAttempts: 0
- }
- });
- }
- break;
+ case 'connectionStatusUpdate':
+ console.log('📡 Connection status update:', message);
+ dispatch({
+ type: 'SET_NETWORK_STATUS',
+ payload: {
+ isOfflineMode: message.data.isOfflineMode,
+ connectionStatus: message.data.status,
+ reconnectAttempts: message.data.reconnectAttempts,
+ maxReconnectAttempts: message.data.maxReconnectAttempts
+ }
+ });
+ break;
- case 'reactError':
- console.log('🔥 React error reported to extension:', message);
- // Show a toast for React errors too
- dispatch({
- type: 'ADD_TOAST',
- payload: createToast(
- 'error',
- 'UI Error',
- 'A component error occurred. The extension may need to be reloaded.',
- 10000
- )
- });
- break;
+ case 'networkOffline':
+ console.log('🔌 Network offline, loading cached tasks:', message);
+ dispatch({
+ type: 'SET_NETWORK_STATUS',
+ payload: {
+ isOfflineMode: true,
+ connectionStatus: 'offline',
+ reconnectAttempts: message.data.reconnectAttempts,
+ lastSuccessfulConnection: message.data.lastSuccessfulConnection
+ }
+ });
- default:
- console.log('❓ Unknown message type:', message.type);
- }
- };
+ // Load cached tasks if available
+ if (message.data.cachedTasks && message.data.cachedTasks.length > 0) {
+ dispatch({
+ type: 'LOAD_CACHED_TASKS',
+ payload: message.data.cachedTasks
+ });
+ }
+ break;
- window.addEventListener('message', handleMessage);
- return () => window.removeEventListener('message', handleMessage);
- }, [vscode, pendingRequests, state.polling]);
+ case 'reconnectionAttempted':
+ console.log('🔄 Reconnection attempted:', message);
+ if (message.success) {
+ dispatch({
+ type: 'CLEAR_ERROR'
+ });
+ }
+ break;
- // Initialize the webview
- useEffect(() => {
- if (!vscode) {
- console.warn('⚠️ VS Code API not available - running in standalone mode');
- dispatch({
- type: 'SET_CONNECTION_STATUS',
- payload: { isConnected: false, status: 'Standalone Mode' }
- });
- return;
- }
+ case 'errorNotification':
+ console.log('⚠️ Error notification from extension:', message);
+ const errorData = message.data;
- console.log('🔄 Initializing webview...');
-
- // Notify extension that webview is ready
- vscode.postMessage({ type: 'ready' });
+ // Map error severity to toast type
+ let toastType: ToastNotification['type'] = 'error';
+ if (errorData.severity === 'low') toastType = 'info';
+ else if (errorData.severity === 'medium') toastType = 'warning';
+ else if (
+ errorData.severity === 'high' ||
+ errorData.severity === 'critical'
+ )
+ toastType = 'error';
- // Request initial tasks data
- sendMessage({ type: 'getTasks' })
- .then((tasksData) => {
- console.log('📋 Initial tasks loaded:', tasksData);
- dispatch({ type: 'SET_TASKS', payload: tasksData });
- })
- .catch((error) => {
- console.error('❌ Failed to load initial tasks:', error);
- dispatch({ type: 'SET_ERROR', payload: `Failed to load tasks: ${error.message}` });
- });
+ // Create appropriate toast based on error category
+ const title =
+ errorData.category === 'network'
+ ? 'Network Error'
+ : errorData.category === 'mcp_connection'
+ ? 'Connection Error'
+ : errorData.category === 'task_loading'
+ ? 'Task Loading Error'
+ : errorData.category === 'ui_rendering'
+ ? 'UI Error'
+ : 'Error';
- }, [vscode, sendMessage]);
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast(
+ toastType,
+ title,
+ errorData.message,
+ errorData.duration || (toastType === 'error' ? 8000 : 5000) // Use preference duration or fallback
+ )
+ });
+ break;
- const contextValue = {
- vscode,
- state,
- dispatch,
- sendMessage,
- availableHeight,
- // Toast notification functions
- showSuccessToast: showSuccessToast(dispatch),
- showInfoToast: showInfoToast(dispatch),
- showWarningToast: showWarningToast(dispatch),
- showErrorToast: showErrorToast(dispatch),
- };
+ case 'error':
+ console.log('❌ General error from extension:', message);
+ const errorTitle =
+ message.errorType === 'connection' ? 'Connection Error' : 'Error';
+ const errorMessage = message.error || 'An unknown error occurred';
- return (
-
- {
- // Handle React errors and show appropriate toast
- dispatch({
- type: 'ADD_TOAST',
- payload: createToast(
- 'error',
- 'Component Error',
- `A React component crashed: ${error.message}`,
- 10000
- )
- });
- }}>
- {/* Conditional rendering for different views */}
- {(() => {
- console.log('🎯 App render - currentView:', state.currentView, 'selectedTaskId:', state.selectedTaskId);
- return state.currentView === 'task-details' && state.selectedTaskId ? (
- dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
- onNavigateToTask={(taskId: string) => dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId })}
- />
- ) : (
-
- );
- })()}
- dispatch({ type: 'REMOVE_TOAST', payload: id })}
- />
-
-
- );
+ dispatch({
+ type: 'SET_ERROR',
+ payload: errorMessage
+ });
+
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast('error', errorTitle, errorMessage, 8000)
+ });
+
+ // Set offline mode for connection errors
+ if (message.errorType === 'connection') {
+ dispatch({
+ type: 'SET_NETWORK_STATUS',
+ payload: {
+ isOfflineMode: true,
+ connectionStatus: 'offline',
+ reconnectAttempts: 0
+ }
+ });
+ }
+ break;
+
+ case 'reactError':
+ console.log('🔥 React error reported to extension:', message);
+ // Show a toast for React errors too
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast(
+ 'error',
+ 'UI Error',
+ 'A component error occurred. The extension may need to be reloaded.',
+ 10000
+ )
+ });
+ break;
+
+ default:
+ console.log('❓ Unknown message type:', message.type);
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+ return () => window.removeEventListener('message', handleMessage);
+ }, [vscode, pendingRequests, state.polling]);
+
+ // Initialize the webview
+ useEffect(() => {
+ if (!vscode) {
+ console.warn('⚠️ VS Code API not available - running in standalone mode');
+ dispatch({
+ type: 'SET_CONNECTION_STATUS',
+ payload: { isConnected: false, status: 'Standalone Mode' }
+ });
+ return;
+ }
+
+ console.log('🔄 Initializing webview...');
+
+ // Notify extension that webview is ready
+ vscode.postMessage({ type: 'ready' });
+
+ // Request initial tasks data
+ sendMessage({ type: 'getTasks' })
+ .then((tasksData) => {
+ console.log('📋 Initial tasks loaded:', tasksData);
+ dispatch({ type: 'SET_TASKS', payload: tasksData });
+ })
+ .catch((error) => {
+ console.error('❌ Failed to load initial tasks:', error);
+ dispatch({
+ type: 'SET_ERROR',
+ payload: `Failed to load tasks: ${error.message}`
+ });
+ });
+ }, [vscode, sendMessage]);
+
+ const contextValue = {
+ vscode,
+ state,
+ dispatch,
+ sendMessage,
+ availableHeight,
+ // Toast notification functions
+ showSuccessToast: showSuccessToast(dispatch),
+ showInfoToast: showInfoToast(dispatch),
+ showWarningToast: showWarningToast(dispatch),
+ showErrorToast: showErrorToast(dispatch)
+ };
+
+ return (
+
+ {
+ // Handle React errors and show appropriate toast
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: createToast(
+ 'error',
+ 'Component Error',
+ `A React component crashed: ${error.message}`,
+ 10000
+ )
+ });
+ }}
+ >
+ {/* Conditional rendering for different views */}
+ {(() => {
+ console.log(
+ '🎯 App render - currentView:',
+ state.currentView,
+ 'selectedTaskId:',
+ state.selectedTaskId
+ );
+ return state.currentView === 'task-details' &&
+ state.selectedTaskId ? (
+ dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
+ onNavigateToTask={(taskId: string) =>
+ dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId })
+ }
+ />
+ ) : (
+
+ );
+ })()}
+ dispatch({ type: 'REMOVE_TOAST', payload: id })}
+ />
+
+
+ );
};
// Initialize React app
const container = document.getElementById('root');
if (container) {
- const root = createRoot(container);
- root.render( );
+ const root = createRoot(container);
+ root.render( );
} else {
- console.error('❌ Root container not found');
-}
\ No newline at end of file
+ console.error('❌ Root container not found');
+}
diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json
index fb1f467f..d6700948 100644
--- a/apps/extension/tsconfig.json
+++ b/apps/extension/tsconfig.json
@@ -3,13 +3,10 @@
"module": "ESNext",
"target": "ES2022",
"outDir": "out",
- "lib": [
- "ES2022",
- "DOM"
- ],
+ "lib": ["ES2022", "DOM"],
"sourceMap": true,
"rootDir": "src",
- "strict": true, /* enable all strict type-checking options */
+ "strict": true /* enable all strict type-checking options */,
"moduleResolution": "Node",
"esModuleInterop": true,
"skipLibCheck": true,
@@ -26,10 +23,5 @@
"@/lib/*": ["./src/lib/*"]
}
},
- "exclude": [
- "node_modules",
- ".vscode-test",
- "out",
- "dist"
- ]
+ "exclude": ["node_modules", ".vscode-test", "out", "dist"]
}
diff --git a/package-lock.json b/package-lock.json
index 11928ecd..0b4a73aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -86,7 +86,7 @@
},
"apps/extension": {
"name": "taskr",
- "version": "1.0.1",
+ "version": "1.0.0",
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",