chore(extension): apply code formatting and add changeset
- Format all extension files according to project standards - Add changeset for VS Code extension implementation - Update package-lock.json dependencies
This commit is contained in:
4
.changeset/upset-places-take.md
Normal file
4
.changeset/upset-places-take.md
Normal file
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
"$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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}];
|
||||
curly: 'warn',
|
||||
eqeqeq: 'warn',
|
||||
'no-throw-literal': 'warn',
|
||||
semi: 'warn'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
console.error('\n❌ Packaging failed!');
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -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 <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn('hover:text-foreground transition-colors', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn('text-foreground font-normal', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('[&>svg]:size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis
|
||||
};
|
||||
|
||||
@@ -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<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent
|
||||
};
|
||||
|
||||
@@ -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<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
||||
@@ -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<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: 'default' | 'destructive';
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
||||
@@ -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<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
className,
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
||||
@@ -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<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full min-h-40 flex-col gap-2 rounded-md border bg-secondary p-2 text-xs shadow-sm outline transition-all',
|
||||
isOver ? 'outline-primary' : 'outline-transparent',
|
||||
className
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full min-h-40 flex-col gap-2 rounded-md border bg-secondary p-2 text-xs shadow-sm outline transition-all',
|
||||
isOver ? 'outline-primary' : 'outline-transparent',
|
||||
className
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type KanbanCardProps = Pick<Feature, 'id' | 'name'> & {
|
||||
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 (
|
||||
<Card
|
||||
className={cn(
|
||||
'rounded-md p-3 shadow-sm',
|
||||
isDragging && 'cursor-grabbing opacity-0',
|
||||
!isDragging && 'cursor-pointer',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
transform: transform
|
||||
? `translateX(${transform.x}px) translateY(${transform.y}px)`
|
||||
: 'none',
|
||||
}}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={(e) => !isDragging && onClick?.(e)}
|
||||
onDoubleClick={onDoubleClick}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'rounded-md p-3 shadow-sm',
|
||||
isDragging && 'cursor-grabbing opacity-0',
|
||||
!isDragging && 'cursor-pointer',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
transform: transform
|
||||
? `translateX(${transform.x}px) translateY(${transform.y}px)`
|
||||
: 'none'
|
||||
}}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={(e) => !isDragging && onClick?.(e)}
|
||||
onDoubleClick={onDoubleClick}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{children ?? <p className="m-0 font-medium text-sm">{name}</p>}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export type KanbanCardsProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const KanbanCards = ({ children, className }: KanbanCardsProps) => (
|
||||
<div className={cn('flex flex-1 flex-col gap-2', className)}>{children}</div>
|
||||
<div className={cn('flex flex-1 flex-col gap-2', className)}>{children}</div>
|
||||
);
|
||||
|
||||
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
|
||||
) : (
|
||||
<div className={cn('flex shrink-0 items-center gap-2', props.className)}>
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: props.color }}
|
||||
/>
|
||||
<p className="m-0 font-semibold text-sm">{props.name}</p>
|
||||
</div>
|
||||
);
|
||||
'children' in props ? (
|
||||
props.children
|
||||
) : (
|
||||
<div className={cn('flex shrink-0 items-center gap-2', props.className)}>
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: props.color }}
|
||||
/>
|
||||
<p className="m-0 font-semibold text-sm">{props.name}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={rectIntersection}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStart={onDragStart}
|
||||
>
|
||||
<div
|
||||
className={cn('grid w-full auto-cols-fr grid-flow-col gap-4', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<DragOverlay>
|
||||
{dragOverlay}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={rectIntersection}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStart={onDragStart}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'grid w-full auto-cols-fr grid-flow-col gap-4',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<DragOverlay>{dragOverlay}</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
export { Textarea };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
}
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
@@ -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<string, string>;
|
||||
timeout: number;
|
||||
maxReconnectAttempts: number;
|
||||
reconnectBackoffMs: number;
|
||||
maxBackoffMs: number;
|
||||
healthCheckIntervalMs: number;
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
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<TaskMasterConfig>): Promise<void> {
|
||||
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<TaskMasterConfig>): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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>): 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>
|
||||
): 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();
|
||||
}
|
||||
return ConfigManager.getInstance();
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
try {
|
||||
if (!this.mcpClient) {
|
||||
throw new Error('MCP client not initialized');
|
||||
}
|
||||
/**
|
||||
* Connect with automatic retry and health monitoring
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
this.stopHealthMonitoring();
|
||||
this.isReconnecting = false;
|
||||
|
||||
/**
|
||||
* Call MCP tool with automatic retry and health monitoring
|
||||
*/
|
||||
async callTool(toolName: string, arguments_: Record<string, unknown>): Promise<any> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
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<string, unknown>
|
||||
): Promise<any> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<string, string>;
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
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<void> | null = null;
|
||||
private client: Client | null = null;
|
||||
private transport: StdioClientTransport | null = null;
|
||||
private config: MCPConfig;
|
||||
private status: MCPServerStatus = { isRunning: false };
|
||||
private connectionPromise: Promise<void> | 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<void> {
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
/**
|
||||
* Start the MCP server process and establish client connection
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.connectionPromise = this._doConnect();
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.connectionPromise = this._doConnect();
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
private async _doConnect(): Promise<void> {
|
||||
try {
|
||||
// Clean up any existing connections
|
||||
await this.disconnect();
|
||||
private async _doConnect(): Promise<void> {
|
||||
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<string, string>,
|
||||
...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<string, string>),
|
||||
...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<void> {
|
||||
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<string, unknown>): Promise<any> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<string, unknown>
|
||||
): Promise<any> {
|
||||
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<boolean> {
|
||||
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<string>('mcp.command', 'npx');
|
||||
const args = config.get<string[]>('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<string>('mcp.cwd', defaultCwd);
|
||||
const env = config.get<Record<string, string>>('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<string>('mcp.command', 'npx');
|
||||
const args = config.get<string[]>('mcp.args', [
|
||||
'-y',
|
||||
'--package=task-master-ai',
|
||||
'task-master-ai'
|
||||
]);
|
||||
|
||||
return {
|
||||
command,
|
||||
args,
|
||||
cwd: cwd || defaultCwd,
|
||||
env
|
||||
};
|
||||
}
|
||||
// Use proper VS Code workspace detection
|
||||
const defaultCwd =
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
|
||||
const cwd = config.get<string>('mcp.cwd', defaultCwd);
|
||||
const env = config.get<Record<string, string>>('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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
// 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<NotificationPreferences>): Promise<void> {
|
||||
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<void> {
|
||||
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<NotificationPreferences>
|
||||
): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration(this.configSection);
|
||||
|
||||
/**
|
||||
* 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 },
|
||||
};
|
||||
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<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 },
|
||||
};
|
||||
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<void> {
|
||||
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<NotificationPreferences>): Promise<void> {
|
||||
return NotificationPreferencesManager.getInstance().updatePreferences(preferences);
|
||||
export function updateNotificationPreferences(
|
||||
preferences: Partial<NotificationPreferences>
|
||||
): Promise<void> {
|
||||
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);
|
||||
}
|
||||
return NotificationPreferencesManager.getInstance().getToastDuration(
|
||||
severity
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<TaskFileData> {
|
||||
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<TaskFileData> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
::-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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
}
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user