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:
DavidMaliglowka
2025-07-17 05:07:26 -05:00
parent 8e59647229
commit 8209f8dbcc
32 changed files with 8584 additions and 7268 deletions

View 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

View File

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

View File

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

View File

@@ -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'
}
}
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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